IOC Extraction
Turn messy text into a trustworthy indicator list. The failure modes that matter in a SOC are
(1) missing an indicator that was hiding in a defanged or split form, (2) shipping a
false-positive indicator that blocks legitimate traffic, and (3) losing the context that tells
an analyst why the indicator matters. Everything below is aimed at those three problems.
Workflow
- Get the raw text. If the input is a file, read it. For PDFs, extract the text first. If
the user pasted text directly, use it as-is. Never summarize the source before extracting;
extraction works on the full text.
- Run the extractor script rather than eyeballing regexes. It handles defanged forms
(
hxxp, [.], (dot), {.}, dot, [@], [://]) and dedupes:python scripts/extract_iocs.py <input-file> --format json
# or from stdin
cat report.txt | python scripts/extract_iocs.py - --format csv
# defang for safe sharing in tickets/chat
python scripts/extract_iocs.py report.txt --format md --defang
Use --context to include the surrounding sentence for each hit; this is what lets you
assign a role (C2, payload host, sender, dropped file) in the next step.
- Triage the output. The script is deliberately greedy. Now apply judgment:
- Remove noise: the vendor's own domain, documentation links, example.com, RFC 1918/loopback
addresses, Microsoft/Google/CDN infrastructure that appears only as a legitimate reference,
version numbers that look like IPs, hashes of known-good files the report mentions for
contrast. See
references/ioc-types.md for the full noise checklist.
- Classify each surviving indicator by role (C2, download/staging, phishing sender,
phishing landing, exfil destination, dropped file, persistence artifact, scanner source) and
confidence (high = the report attributes it directly to the actor; medium = observed but
shared/hosting infra; low = mentioned without attribution).
- Note shelf life: attacker IPs on cloud/VPS rotate in days; hashes are durable but
trivially changed; domains sit in between. Say this in the output so the consumer knows
what to expire.
- Produce the deliverable in the format the user's tooling needs (see Output formats).
Default to a Markdown table for humans plus a CSV or JSON block for machines.
- Recommend enrichment and action. For each role, state what to do: block at proxy/DNS,
add to EDR watchlist, retro-hunt in SIEM for the last N days, sinkhole, etc. Suggest the
enrichment sources from
references/enrichment.md, but do not invent enrichment results.
If you can actually query a source (via an MCP tool or the user's API), do it and cite it;
otherwise mark enrichment as "pending".
Output formats
Always include the source (report title/URL/filename) and extraction date in the output
so the list is auditable. Pick the machine format from the user's request:
| Consumer |
Format |
Notes |
| Ticket, Slack, email |
Markdown table, defanged |
Never paste live URLs into chat |
| SIEM watchlist / lookup |
CSV: indicator,type,role,confidence,first_seen,source |
Refanged, lower-cased domains, no scheme on URLs unless the SIEM matches full URLs |
| EDR / firewall / DNS block |
Plain list, one per line, grouped by type |
Refanged; exclude low-confidence |
| TIP / STIX consumers |
STIX 2.1 bundle (--format stix) |
Script emits indicator objects with STIX patterns; add labels and valid_until by hand |
| Sigma/KQL/SPL retro-hunt |
Use ioc-extraction output as the value list in a query built with the siem-query-authoring skill |
Split by type; hashes go to file events, domains to DNS/proxy |
Markdown table template:
| Indicator (defanged) | Type | Role | Confidence | Notes |
|---|---|---|---|---|
| hxxp://evil[.]example/x.php | url | C2 | high | Cobalt Strike beacon, p.4 |
Things that go wrong
- Split indicators. Reports wrap long URLs across lines and PDF extraction inserts spaces.
If the script returns a suspicious fragment (a domain with no TLD, a hash of 63 chars), look
at the surrounding text and reassemble by hand.
- Hash type confusion. 32 hex = MD5, 40 = SHA-1, 64 = SHA-256, 128 = SHA-512. A 64-char
string can also be an ssdeep-looking blob or a JA3/JA4 fingerprint (JA3 is 32 hex, so it
collides with MD5). If the report labels it, trust the label over length.
- Tables of both malicious and benign. Many vendor reports list legitimate tools
(PsExec, AnyDesk, rclone) with their hashes. Those are tooling indicators, not malicious
files; label them
tool and do not recommend blocking without a conversation.
- IPv6, CIDR, and ports. Keep the port with the URL/socket (
1.2.3.4:4444) as a note, but
the indicator itself is the IP. CIDR ranges are rarely safe to block wholesale; flag them.
- Email addresses as senders vs. victims. A report may include the targeted mailbox.
Only sender/reply-to addresses are indicators.
- YARA/Sigma snippets in the report contain strings that look like indicators (mutex names,
user agents, paths). Those are detection content, and belong to the
detection-engineering
skill, but do capture mutexes, named pipes, user agents, and registry keys as
host artifacts with type artifact.
Worked example
Input (from a vendor blog post the user pasted):
The loader beacons to hxxps://cdn-sync[.]example/api/v2 and drops C:\Users\Public\svc.exe
(SHA256 3a7b...4f1b). Victims received mail from billing[@]invoices-fake[.]com. The malware
checks connectivity to www.microsoft.com before running. Analysts can find samples on
bazaar.abuse.ch.
Deliverable:
**Source:** "Loader campaign" blog post, 2026-09-17 **Extracted:** 2026-09-17T14:02Z
| Indicator (defanged) | Type | Role | Confidence | Valid until | Notes |
|---|---|---|---|---|---|
| hxxps://cdn-sync[.]example/api/v2 | url | c2 | high | +30d | Beacon endpoint; block full URL, hunt domain |
| cdn-sync[.]example | domain | c2 | high | +90d | Actor-registered per report |
| 3a7b...4f1b | sha256 | dropped-file | high | indefinite | Loader; add to EDR watchlist |
| C:\Users\Public\svc.exe | windows_path | dropped-file | medium | n/a | Hunt only; path is generic |
| billing[@]invoices-fake[.]com | email | phishing-sender | medium | +30d | Mail gateway retro-search 30d |
Dropped as noise: www.microsoft.com (connectivity check), bazaar.abuse.ch (reference).
**Actions**
1. Retro-hunt proxy/DNS for the domain (30d) and EDR for the hash (90d): build the query with `siem-query-authoring`.
2. Mail gateway: search sender 30d; report recipient list to `phishing-analysis` if any delivered.
3. Block URL at proxy; do not sinkhole the domain until passive DNS confirms it is actor-owned.
**Enrichment:** pending (no VT/GreyNoise access in this session).
The point of the example is the shape: every row carries a role, a confidence, and a
lifetime, the noise is listed so the reader knows it was considered, and the actions name the
skill that does the next step.
Hand-offs
- Hashes and suspicious files:
malware-triage for static analysis and YARA.
- CVEs:
vulnerability-triage for prioritization; a CVE is not blockable.
- Host artifacts (paths, registry keys, mutexes):
detection-engineering for a Sigma rule or
threat-hunting for a one-off hunt.
- Retro-search queries across the SIEM:
siem-query-authoring.
- The TTPs described around the indicators:
mitre-attack-mapping and threat-intel-analysis.
Customization
Teams should edit references/environment.md to list their own allow-listed domains/IP ranges
(so they are never emitted as indicators), the exact CSV column order their SIEM lookup expects,
and which enrichment sources they have API access to. The script reads an optional
references/allowlist.txt (one pattern per line, globs allowed) and drops matches. Add your
corporate domains, mail domains, and public ranges there before the first real use; it is
the single most effective way to stop your own infrastructure showing up in a blocklist.
1---2name: ioc-extraction3description: Extract, normalize, defang/refang, classify, and de-duplicate indicators of compromise (IPs, domains, URLs, hashes, emails, file paths, registry keys, CVEs, wallet addresses) from any unstructured text such as threat intel reports, vendor advisories, phishing emails, pasted logs, PDFs, or chat messages, then produce a clean, machine-readable indicator list with context and an enrichment plan. Use this whenever the user pastes or points at a report, advisory, email, or blob of text and wants the indicators out of it, asks to "pull the IOCs", "defang these", "make a blocklist", "turn this into a watchlist", "what should we block from this report", or needs indicators formatted for a SIEM, EDR, firewall, TIP, or STIX bundle. Also use it when someone asks whether a list of indicators is well-formed or contains noise.4---56# IOC Extraction78Turn messy text into a trustworthy indicator list. The failure modes that matter in a SOC are9(1) missing an indicator that was hiding in a defanged or split form, (2) shipping a10false-positive indicator that blocks legitimate traffic, and (3) losing the context that tells11an analyst *why* the indicator matters. Everything below is aimed at those three problems.1213## Workflow14151. **Get the raw text.** If the input is a file, read it. For PDFs, extract the text first. If16 the user pasted text directly, use it as-is. Never summarize the source before extracting;17 extraction works on the full text.182. **Run the extractor script** rather than eyeballing regexes. It handles defanged forms19 (`hxxp`, `[.]`, `(dot)`, `{.}`, ` dot `, `[@]`, `[://]`) and dedupes:20 ```bash21 python scripts/extract_iocs.py <input-file> --format json22 # or from stdin23 cat report.txt | python scripts/extract_iocs.py - --format csv24 # defang for safe sharing in tickets/chat25 python scripts/extract_iocs.py report.txt --format md --defang26 ```27 Use `--context` to include the surrounding sentence for each hit; this is what lets you28 assign a role (C2, payload host, sender, dropped file) in the next step.293. **Triage the output.** The script is deliberately greedy. Now apply judgment:30 - Remove noise: the vendor's own domain, documentation links, example.com, RFC 1918/loopback31 addresses, Microsoft/Google/CDN infrastructure that appears only as a legitimate reference,32 version numbers that look like IPs, hashes of *known-good* files the report mentions for33 contrast. See `references/ioc-types.md` for the full noise checklist.34 - Classify each surviving indicator by **role** (C2, download/staging, phishing sender,35 phishing landing, exfil destination, dropped file, persistence artifact, scanner source) and36 **confidence** (high = the report attributes it directly to the actor; medium = observed but37 shared/hosting infra; low = mentioned without attribution).38 - Note **shelf life**: attacker IPs on cloud/VPS rotate in days; hashes are durable but39 trivially changed; domains sit in between. Say this in the output so the consumer knows40 what to expire.414. **Produce the deliverable** in the format the user's tooling needs (see Output formats).42 Default to a Markdown table for humans plus a CSV or JSON block for machines.435. **Recommend enrichment and action.** For each role, state what to do: block at proxy/DNS,44 add to EDR watchlist, retro-hunt in SIEM for the last N days, sinkhole, etc. Suggest the45 enrichment sources from `references/enrichment.md`, but do not invent enrichment results.46 If you can actually query a source (via an MCP tool or the user's API), do it and cite it;47 otherwise mark enrichment as "pending".4849## Output formats5051Always include the **source** (report title/URL/filename) and **extraction date** in the output52so the list is auditable. Pick the machine format from the user's request:5354| Consumer | Format | Notes |55|---|---|---|56| Ticket, Slack, email | Markdown table, **defanged** | Never paste live URLs into chat |57| SIEM watchlist / lookup | CSV: `indicator,type,role,confidence,first_seen,source` | Refanged, lower-cased domains, no scheme on URLs unless the SIEM matches full URLs |58| EDR / firewall / DNS block | Plain list, one per line, grouped by type | Refanged; exclude low-confidence |59| TIP / STIX consumers | STIX 2.1 bundle (`--format stix`) | Script emits `indicator` objects with STIX patterns; add `labels` and `valid_until` by hand |60| Sigma/KQL/SPL retro-hunt | Use `ioc-extraction` output as the value list in a query built with the `siem-query-authoring` skill | Split by type; hashes go to file events, domains to DNS/proxy |6162Markdown table template:6364```markdown65| Indicator (defanged) | Type | Role | Confidence | Notes |66|---|---|---|---|---|67| hxxp://evil[.]example/x.php | url | C2 | high | Cobalt Strike beacon, p.4 |68```6970## Things that go wrong7172- **Split indicators.** Reports wrap long URLs across lines and PDF extraction inserts spaces.73 If the script returns a suspicious fragment (a domain with no TLD, a hash of 63 chars), look74 at the surrounding text and reassemble by hand.75- **Hash type confusion.** 32 hex = MD5, 40 = SHA-1, 64 = SHA-256, 128 = SHA-512. A 64-char76 string can also be an ssdeep-looking blob or a JA3/JA4 fingerprint (JA3 is 32 hex, so it77 collides with MD5). If the report labels it, trust the label over length.78- **Tables of *both* malicious and benign.** Many vendor reports list legitimate tools79 (PsExec, AnyDesk, rclone) with their hashes. Those are *tooling* indicators, not malicious80 files; label them `tool` and do not recommend blocking without a conversation.81- **IPv6, CIDR, and ports.** Keep the port with the URL/socket (`1.2.3.4:4444`) as a note, but82 the indicator itself is the IP. CIDR ranges are rarely safe to block wholesale; flag them.83- **Email addresses as senders vs. victims.** A report may include the *targeted* mailbox.84 Only sender/reply-to addresses are indicators.85- **YARA/Sigma snippets in the report** contain strings that look like indicators (mutex names,86 user agents, paths). Those are detection content, and belong to the `detection-engineering`87 skill, but do capture mutexes, named pipes, user agents, and registry keys as88 *host artifacts* with type `artifact`.8990## Worked example9192Input (from a vendor blog post the user pasted):9394> The loader beacons to hxxps://cdn-sync[.]example/api/v2 and drops `C:\Users\Public\svc.exe`95> (SHA256 3a7b...4f1b). Victims received mail from billing[@]invoices-fake[.]com. The malware96> checks connectivity to www.microsoft.com before running. Analysts can find samples on97> bazaar.abuse.ch.9899Deliverable:100101```markdown102**Source:** "Loader campaign" blog post, 2026-09-17 **Extracted:** 2026-09-17T14:02Z103104| Indicator (defanged) | Type | Role | Confidence | Valid until | Notes |105|---|---|---|---|---|---|106| hxxps://cdn-sync[.]example/api/v2 | url | c2 | high | +30d | Beacon endpoint; block full URL, hunt domain |107| cdn-sync[.]example | domain | c2 | high | +90d | Actor-registered per report |108| 3a7b...4f1b | sha256 | dropped-file | high | indefinite | Loader; add to EDR watchlist |109| C:\Users\Public\svc.exe | windows_path | dropped-file | medium | n/a | Hunt only; path is generic |110| billing[@]invoices-fake[.]com | email | phishing-sender | medium | +30d | Mail gateway retro-search 30d |111112Dropped as noise: www.microsoft.com (connectivity check), bazaar.abuse.ch (reference).113114**Actions**1151. Retro-hunt proxy/DNS for the domain (30d) and EDR for the hash (90d): build the query with `siem-query-authoring`.1162. Mail gateway: search sender 30d; report recipient list to `phishing-analysis` if any delivered.1173. Block URL at proxy; do not sinkhole the domain until passive DNS confirms it is actor-owned.118**Enrichment:** pending (no VT/GreyNoise access in this session).119```120121The point of the example is the *shape*: every row carries a role, a confidence, and a122lifetime, the noise is listed so the reader knows it was considered, and the actions name the123skill that does the next step.124125## Hand-offs126127- Hashes and suspicious files: `malware-triage` for static analysis and YARA.128- CVEs: `vulnerability-triage` for prioritization; a CVE is not blockable.129- Host artifacts (paths, registry keys, mutexes): `detection-engineering` for a Sigma rule or130 `threat-hunting` for a one-off hunt.131- Retro-search queries across the SIEM: `siem-query-authoring`.132- The TTPs described around the indicators: `mitre-attack-mapping` and `threat-intel-analysis`.133134## Customization135136Teams should edit `references/environment.md` to list their own allow-listed domains/IP ranges137(so they are never emitted as indicators), the exact CSV column order their SIEM lookup expects,138and which enrichment sources they have API access to. The script reads an optional139`references/allowlist.txt` (one pattern per line, globs allowed) and drops matches. Add your140corporate domains, mail domains, and public ranges there before the first real use; it is141the single most effective way to stop your own infrastructure showing up in a blocklist.