| argument-hint | <threat report URL> | ||||||
|---|---|---|---|---|---|---|---|
| allowed-tools |
|
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.
$ARGUMENTS contains the URL to the threat report.
If no URL is provided, ask the user for one.
Before using this skill, you need:
- Obsidian 1.12+ with CLI enabled (Settings -> General -> Command line interface). Obsidian must be running.
- MITRE ATT&CK STIX data -- download
enterprise-attack.jsonfrom https://github.com/mitre-cti/enterprise-attack and place it somewhere accessible. Update the path in Step 3 below. - Atomic Red Team index -- clone https://github.com/redcanaryco/atomic-red-team and build an
index.yaml(or use the one from the repo'satomics/directory). Update the path in Step 4 below. - defuddle --
npm install -g defuddle-clifor extracting article content from URLs. - jq -- for querying the ATT&CK STIX JSON.
- Python 3 with PyYAML --
pip install pyyamlfor parsing Atomic Red Team tests.
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.
Try defuddle first for clean content extraction:
defuddle parse "$URL" --json
defuddle parse "$URL" --markdownThe --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.
Analyze the extracted content and identify MITRE ATT&CK techniques.
Priority order:
- 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.
- 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
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-componentThe
x_mitre_data_sourcesfield 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.jsonRun 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))
PYEOFReplace the technique_ids list with the actual IDs from Step 2.
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 &&.
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.
obsidian create vault="{SelectedVault}" path="Threat Reports/{Date} - {SafeName}.md" overwrite openobsidian 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=listWrite 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)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 |
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
Source: https://help.obsidian.md/cli (Obsidian 1.12+) This is the authoritative reference. ONLY use commands documented here.
- Obsidian 1.12+ with CLI enabled (Settings -> General -> Command line interface)
- Obsidian must be running
| 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"
| 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 |
| Parameter | Description |
|---|---|
file=<name> |
File name |
path=<path> |
File path |
content=<text> |
(required) Content to append |
| Flag | Description |
|---|---|
inline |
Append without preceding newline |
| Parameter | Description |
|---|---|
name=<name> |
(required) Property name |
value=<value> |
(required) Property value |
type=<type> |
text, list, number, checkbox, date, datetime |
file=<name> |
File name |
| Parameter | Description |
|---|---|
file=<name> |
File name |
path=<path> |
File path |
| Parameter | Description |
|---|---|
query=<text> |
(required) Search query |
path=<folder> |
Limit to folder |
limit=<n> |
Max files |
Lists all available vaults. No parameters.