Skip to content

Instantly share code, notes, and snippets.

@Antonlovesdnb
Last active May 13, 2026 00:18
Show Gist options
  • Select an option

  • Save Antonlovesdnb/346dc89a0fa0affaef1240e12f972726 to your computer and use it in GitHub Desktop.

Select an option

Save Antonlovesdnb/346dc89a0fa0affaef1240e12f972726 to your computer and use it in GitHub Desktop.
threat report skill
argument-hint <threat report URL>
allowed-tools
Bash
Read
Grep
Glob
WebFetch
Write

Threat Report -> Obsidian Defensive Playbook

Ingest a threat intelligence report, map to MITRE ATT&CK, pull Atomic Red Team tests and detection data sources, and write a structured defensive playbook to an Obsidian vault.

Input

$ARGUMENTS contains the URL to the threat report.

If no URL is provided, ask the user for one.

Prerequisites

Before using this skill, you need:

  1. Obsidian 1.12+ with CLI enabled (Settings -> General -> Command line interface). Obsidian must be running.
  2. MITRE ATT&CK STIX data -- download enterprise-attack.json from https://github.com/mitre-cti/enterprise-attack and place it somewhere accessible. Update the path in Step 3 below.
  3. Atomic Red Team index -- clone https://github.com/redcanaryco/atomic-red-team and build an index.yaml (or use the one from the repo's atomics/ directory). Update the path in Step 4 below.
  4. defuddle -- npm install -g defuddle-cli for extracting article content from URLs.
  5. jq -- for querying the ATT&CK STIX JSON.
  6. Python 3 with PyYAML -- pip install pyyaml for parsing Atomic Red Team tests.

Vault Selection

Before doing anything else, ask the user which Obsidian vault to use:

obsidian vaults 2>/dev/null || echo "Could not list vaults"

Present the options and let the user pick. Use vault="{name}" on every subsequent obsidian command.

If the user has previously specified a vault in this conversation, reuse it without asking again.

Workflow

Step 1 -- Extract the article content

Try defuddle first for clean content extraction:

defuddle parse "$URL" --json
defuddle parse "$URL" --markdown

The --json version returns title, description, domain, author, date, and content (HTML). The --markdown version returns readable content for analysis.

If defuddle returns empty content (common with JavaScript SPAs like Huntress, Builder.io sites, etc.), fall back to WebFetch:

WebFetch(url=$URL, prompt="Extract the COMPLETE article content including all sections, IOCs, technique details, and any ATT&CK mappings")

Preserve the full article text -- do not summarize. The playbook should be self-contained.

Step 2 -- Map to ATT&CK techniques

Analyze the extracted content and identify MITRE ATT&CK techniques.

Priority order:

  1. Check for an existing ATT&CK table -- Many vendor reports (CrowdStrike, Microsoft, Mandiant, etc.) include a technique mapping table. If one exists, extract the technique IDs, names, and tactics directly from it.
  2. Infer from content -- If no table exists, map observed behaviors to ATT&CK techniques. Assign confidence:
    • High -- Technique explicitly named or procedure described with tool/API detail
    • Medium -- Behavior clearly maps to a technique
    • Low -- General behavior that could map to a technique

Also extract:

  • Threat actor / campaign name
  • Target industries / regions
  • IOCs -- IPs, domains, hashes, file paths, registry keys, URLs

Step 3 -- Look up ATT&CK data source components

For each technique, resolve the actual ATT&CK Data Components (e.g., "Driver Load", "Process Creation", "Service Creation") -- these are what defenders configure logging for.

IMPORTANT: The data source resolution requires a multi-level chain through the STIX data: technique <- detects <- detection-strategy -> analytics -> analytic -> log_source_references -> data-component

The x_mitre_data_sources field on technique objects may be empty in newer ATT&CK versions. Do NOT rely on it.

Run this as a single jq call (replace technique IDs in the select() with actual ones from Step 2):

jq -r '
  # 1. Build technique lookup
  ([.objects[] | select(.type == "attack-pattern" and .revoked != true) |
    {stix_id: .id, attack_id: (.external_references[]? | select(.source_name == "mitre-attack") | .external_id)} |
    select(.attack_id == "T1566.001" or .attack_id == "T1059.001")
  ]) as $techs |

  # 2. Build data component name lookup
  ([.objects[] | select(.type == "x-mitre-data-component") | {(.id): .name}] | add // {}) as $dc_names |

  # 3. Build analytic lookup: id -> data_component_refs
  ([.objects[] | select(.type == "x-mitre-analytic") |
    {id: .id, dc_refs: [(.x_mitre_log_source_references // [])[] | .x_mitre_data_component_ref // empty]}
  ]) as $analytics |

  # 4. Build detection strategy -> analytic refs
  ([.objects[] | select(.type == "x-mitre-detection-strategy") |
    {id: .id, analytic_refs: (.x_mitre_analytic_refs // [])}
  ]) as $det_strats |

  # 5. Get detects relationships: det_strategy -> technique
  [.objects[] |
    select(.type == "relationship" and .relationship_type == "detects") |
    select(.target_ref as $t | $techs | map(.stix_id) | index($t)) |
    {det_strat: .source_ref, tech_stix: .target_ref}
  ] |

  # 6. Resolve the full chain
  [.[] |
    .tech_stix as $ts | .det_strat as $ds |
    ($techs[] | select(.stix_id == $ts) | .attack_id) as $aid |
    ($det_strats[] | select(.id == $ds) | .analytic_refs[]) as $aref |
    ($analytics[] | select(.id == $aref) | .dc_refs[]) as $dcref |
    {attack_id: $aid, data_component: ($dc_names[$dcref] // $dcref)}
  ] | group_by(.attack_id) | map({(.[0].attack_id): ([.[].data_component] | unique)}) | add // {}
' /path/to/enterprise-attack.json

Step 4 -- Look up Atomic Red Team tests

Run this in parallel with Step 3 -- they are independent.

python3 << 'PYEOF'
import yaml, json, sys

technique_ids = ["T1566.001", "T1059.001"]  # Replace with actual IDs

with open("/path/to/atomic-red-team/index.yaml") as f:
    data = yaml.safe_load(f)

results = {}
for tid in technique_ids:
    for tactic_name, techniques in data.items():
        if isinstance(techniques, dict) and tid in techniques:
            entry = techniques[tid]
            tests = entry.get("atomic_tests", [])
            results[tid] = []
            for i, t in enumerate(tests, 1):
                executor = t.get("executor", {})
                results[tid].append({
                    "number": i,
                    "name": t.get("name", ""),
                    "description": t.get("description", "").split("\n")[0],
                    "platforms": t.get("supported_platforms", []),
                    "executor_type": executor.get("name", ""),
                    "command": executor.get("command", ""),
                    "cleanup": executor.get("cleanup_command", "")
                })
            break

print(json.dumps(results, indent=2))
PYEOF

Replace the technique_ids list with the actual IDs from Step 2.

Step 5 -- Build the Obsidian note

Performance tip: Build the full note body in a temp file, then use a single obsidian append call instead of many small ones. Chain property:set calls with &&.

5a. Generate the filename

Format: {YYYY-MM-DD} - {Campaign or Article Name}.md

Safe-format the name: replace /, \, :, |, ?, *, ", <, > with -.

If no clear campaign name, use the article title from defuddle metadata.

5b. Create the note and set properties

obsidian create vault="{SelectedVault}" path="Threat Reports/{Date} - {SafeName}.md" overwrite open
obsidian property:set vault="{V}" file="{F}" name=source_url value="{URL}" type=text && \
obsidian property:set vault="{V}" file="{F}" name=date_ingested value="{YYYY-MM-DD}" type=date && \
obsidian property:set vault="{V}" file="{F}" name=threat_actor value="{actor or Unknown}" type=text && \
obsidian property:set vault="{V}" file="{F}" name=industries value="{industry1}, {industry2}" type=list && \
obsidian property:set vault="{V}" file="{F}" name=techniques value="{T1566.001}, {T1059.001}" type=list && \
obsidian property:set vault="{V}" file="{F}" name=tags value="threat-report, ATT&CK" type=list

5c. Build and append the body

Write the full note body to a temp file, then append in one call:

cat > /tmp/threat-report-body.md << 'EOF'
... full note content ...
EOF
obsidian append vault="{V}" file="{F}" content="$(cat /tmp/threat-report-body.md)"

Note body structure:

# {Campaign/Threat Name}

**Source:** [{Article Title}]({URL})
**Date Ingested:** {YYYY-MM-DD}
**Author:** {Author} ({Publication})
**Threat Actor:** {actor or "Unknown"}
**Industries:** {targets}

## Full Report

{The COMPLETE article text, preserving all sections and subsections from the original.
Include technical details, malware analysis, infrastructure analysis -- everything.
The playbook should be self-contained so readers don't need the original URL.}

## ATT&CK Mapping

| Technique | Name | Tactic | Confidence | Source |
|-----------|------|--------|------------|--------|
| [[T1566.001 - Spearphishing Attachment\|T1566.001]] | Spearphishing Attachment | Initial Access | High | Report Table |

## ATT&CK Data Sources

| Technique | Data Source: Data Component |
|-----------|---------------------------|
| T1566.001 | File Creation, File Metadata, Process Creation |

## Detection & Simulation

{Repeat this block for each technique, ordered by kill chain phase}

### T1566.001 -- Spearphishing Attachment

**Tactic:** Initial Access | **Confidence:** High

**How it was used:** {1-2 sentences from the report}

**Data Sources:** {Data Component 1} - {Data Component 2}

**Atomic Red Team Tests:**

| # | Test Name | Platforms | Invoke |
|---|-----------|-----------|--------|
| 1 | {Test Name} | Windows | `Invoke-AtomicTest T1566.001 -TestNumbers 1` |

---

## Indicators of Compromise

| Type | Value | Context |
|------|-------|---------|
| IP | {ip} | {how it was used} |
| Domain | {domain} | {context} |
| Hash (SHA256) | {hash} | {what file} |

{If no IOCs found, state: "No specific IOCs were published in this report."}

## Simulation Plan

Recommended purple team exercise order, prioritized by: high confidence + atomic test available + kill chain progression.

| Priority | Technique | Name | Atomic Test | Command |
|----------|-----------|------|-------------|---------|
| 1 | T1566.001 | Spearphishing Attachment | #1 - {Test Name} | `Invoke-AtomicTest T1566.001 -TestNumbers 1` |

**Techniques without Atomic Red Team coverage:**
- {TXXXX} -- {Name} (manual simulation required)

Backlink Rules

Wikilink technique IDs so they connect to technique notes in the vault:

Element Format Backlinked?
Technique in ATT&CK table [[{ID} - {Name}|{ID}]] YES
Technique in Detection sections Header only, not linked NO
Data Components Plain text NO
IOCs Plain text NO

Step 6 -- Summary

After all CLI commands complete, report to the user:

  • Note name and vault location
  • Techniques: {X} total ({Y} from report table, {Z} inferred)
  • Atomic Red Team coverage: {A} of {X} techniques have tests ({B} total tests)
  • Data sources identified: {C} unique data components across all techniques
  • IOCs extracted: {D} indicators
  • Any techniques without atomic test coverage

Obsidian CLI Reference

Source: https://help.obsidian.md/cli (Obsidian 1.12+) This is the authoritative reference. ONLY use commands documented here.

Prerequisites

  • Obsidian 1.12+ with CLI enabled (Settings -> General -> Command line interface)
  • Obsidian must be running

Common Parameters

Parameter Description
vault=<name> Target a specific vault (must be first parameter)
file=<name> Target a file by name
path=<path> Target a file by path

Use quotes for values with spaces: name="My Note"

Commands

create

Parameter Description
name=<name> File name
path=<path> File path (relative to vault root)
content=<text> Initial body content
Flag Description
overwrite Overwrite if exists
open Open file after creating

append

Parameter Description
file=<name> File name
path=<path> File path
content=<text> (required) Content to append
Flag Description
inline Append without preceding newline

property:set

Parameter Description
name=<name> (required) Property name
value=<value> (required) Property value
type=<type> text, list, number, checkbox, date, datetime
file=<name> File name

read

Parameter Description
file=<name> File name
path=<path> File path

search

Parameter Description
query=<text> (required) Search query
path=<folder> Limit to folder
limit=<n> Max files

vaults

Lists all available vaults. No parameters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment