Building Automated IOC Extractors
An automated IOC extractor is a Python pipeline that reads a directory of PCAP files, extracts all network indicators (IPs, domains, URLs, TLS SNIs, certificate subjects), enriches each against threat intelligence APIs, and outputs a ranked IOC list with confidence scores. This is a production-quality tool you build once and reuse on every investigation.
An incident has produced 8 PCAP files spanning 4 days. You need a complete IOC list with threat intel enrichment within 30 minutes of receiving the files. You run your IOC extractor. It processes all 8 files, extracts 2,847 unique external IPs and 1,203 domains, queries VirusTotal and AbuseIPDB for each, and produces a ranked output where the top 20 entries are all confirmed C2 or malware-related infrastructure — with zero manual work.
Extractor Architecture
IOC Extractor Pipeline
═══════════════════════════════════════════════════════════════════
Input: directory of PCAP files
Stage 1: Extraction (tshark subprocess)
├── External destination IPs from TCP SYN packets
├── DNS query names (all types)
├── HTTP host headers + request URIs
├── TLS SNI values from ClientHello
├── X.509 certificate CN/SAN fields
└── HTTP User-Agent strings
Stage 2: Deduplication + Allowlist Filtering
├── Deduplicate across all PCAP files
├── Remove RFC 1918 private IP ranges
├── Remove known CDN/cloud IP ranges (Cloudflare, AWS, Google)
├── Remove known-good domains (microsoft.com, google.com, etc.)
└── Output: raw IOC set
Stage 3: Enrichment (parallel API calls)
├── VirusTotal: malicious detection count per IP/domain
├── AbuseIPDB: abuse confidence score per IP
├── Shodan: open ports, service banners (confirms C2 infrastructure)
└── RDAP/WHOIS: registration date (new = suspicious)
Stage 4: Scoring + Ranking
├── Score = VT_malicious * 3 + AbuseIPDB_score/10 + age_penalty
├── Sort by score descending
└── Output: JSON report + CSV + raw IOC text files
Complete IOC Extractor
#!/usr/bin/env python3
"""
Automated IOC extractor with threat intel enrichment.
Usage: python3 ioc-extractor.py [--vt-key KEY] [--abuseipdb-key KEY]
"""
import subprocess, json, time, sys, os, glob, ipaddress, re
from pathlib import Path
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request, urllib.parse
# ── CONFIGURATION ─────────────────────────────────────────────────────
VT_API_KEY = os.getenv("VT_API_KEY", "")
ABUSE_API_KEY = os.getenv("ABUSEIPDB_API_KEY", "")
PRIVATE_NETS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("169.254.0.0/16"),
ipaddress.ip_network("224.0.0.0/4"),
ipaddress.ip_network("240.0.0.0/4"),
]
ALLOWLIST_DOMAINS = {
"microsoft.com", "windows.com", "windowsupdate.com",
"google.com", "googleapis.com", "gstatic.com",
"cloudflare.com", "akamai.com", "akamaiedge.net",
"amazon.com", "amazonaws.com", "s3.amazonaws.com",
"apple.com", "icloud.com",
}
def is_private(ip_str: str) -> bool:
try:
addr = ipaddress.ip_address(ip_str)
return any(addr in net for net in PRIVATE_NETS)
except ValueError:
return True
def is_allowlisted(domain: str) -> bool:
domain = domain.rstrip(".")
for allowed in ALLOWLIST_DOMAINS:
if domain == allowed or domain.endswith("." + allowed):
return True
return False
# ── EXTRACTION ─────────────────────────────────────────────────────────
def extract_from_pcap(pcap: str) -> dict:
"""Extract all IOC types from a single PCAP file."""
def tshark(filter_str: str, fields: list) -> list:
cmd = ["tshark", "-r", pcap, "-n", "-Y", filter_str, "-T", "fields",
"-E", "separator=\t"] + [f for field in fields for f in ["-e", field]]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
return [line.split("\t") for line in result.stdout.splitlines() if line.strip()]
except Exception:
return []
ips = set()
domains = set()
urls = set()
sni = set()
user_agents = set()
# IPs from TCP SYN packets (external only)
for row in tshark("tcp.flags.syn==1 and not tcp.flags.ack", ["ip.dst"]):
ip = row[0].strip() if row else ""
if ip and not is_private(ip):
ips.add(ip)
# DNS query names
for row in tshark("dns.flags.response==0", ["dns.qry.name"]):
name = row[0].strip() if row else ""
if name and not is_allowlisted(name):
domains.add(name)
# HTTP hosts + URIs
for row in tshark("http.request", ["http.host", "http.request.uri"]):
host = row[0].strip() if len(row) > 0 else ""
uri = row[1].strip() if len(row) > 1 else ""
if host and not is_allowlisted(host):
urls.add(f"http://{host}{uri}")
# TLS SNI
for row in tshark("tls.handshake.type==1", ["tls.handshake.extensions_server_name"]):
name = row[0].strip() if row else ""
if name and not is_allowlisted(name):
sni.add(name)
# User agents
for row in tshark("http.user_agent", ["http.user_agent"]):
ua = row[0].strip() if row else ""
if ua:
user_agents.add(ua)
return {"ips": ips, "domains": domains, "urls": urls, "sni": sni, "user_agents": user_agents}
# ── ENRICHMENT ─────────────────────────────────────────────────────────
def vt_lookup(indicator: str, ioc_type: str) -> dict:
if not VT_API_KEY:
return {}
endpoint = {"ip": "ip_addresses", "domain": "domains"}.get(ioc_type, "urls")
url = f"https://www.virustotal.com/api/v3/{endpoint}/{urllib.parse.quote(indicator, safe='')}"
try:
req = urllib.request.Request(url, headers={"x-apikey": VT_API_KEY})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
return {"malicious": stats.get("malicious", 0), "suspicious": stats.get("suspicious", 0)}
except Exception as e:
return {"error": str(e)}
# ── MAIN ──────────────────────────────────────────────────────────────
def main():
pcap_dir = Path(sys.argv[1])
out_dir = Path(sys.argv[2])
out_dir.mkdir(parents=True, exist_ok=True)
pcap_files = sorted(glob.glob(str(pcap_dir / "*.pcap")))
print(f"Processing {len(pcap_files)} PCAP files...")
all_iocs = defaultdict(set)
for pcap in pcap_files:
print(f" Extracting: {os.path.basename(pcap)}")
result = extract_from_pcap(pcap)
for key, values in result.items():
all_iocs[key].update(values)
print(f"\nExtracted:")
for key, values in all_iocs.items():
print(f" {key}: {len(values)}")
# Enrich IPs
print(f"\nEnriching {len(all_iocs['ips'])} IPs against VirusTotal...")
enriched = []
for ip in sorted(all_iocs["ips"]):
vt = vt_lookup(ip, "ip")
score = vt.get("malicious", 0) * 3 + vt.get("suspicious", 0)
enriched.append({"type": "ip", "value": ip, "vt_malicious": vt.get("malicious", 0),
"vt_suspicious": vt.get("suspicious", 0), "score": score})
time.sleep(0.25) # Rate limiting
# Enrich domains
print(f"Enriching {len(all_iocs['domains'])} domains...")
for domain in sorted(all_iocs["domains"]):
vt = vt_lookup(domain, "domain")
score = vt.get("malicious", 0) * 3 + vt.get("suspicious", 0)
enriched.append({"type": "domain", "value": domain, "vt_malicious": vt.get("malicious", 0),
"vt_suspicious": vt.get("suspicious", 0), "score": score})
time.sleep(0.25)
# Write outputs
enriched.sort(key=lambda x: -x["score"])
with open(out_dir / "enriched_iocs.json", "w") as f:
json.dump(enriched, f, indent=2)
print(f"\n=== Top 20 High-Score IOCs ===")
for ioc in enriched[:20]:
print(f" score={ioc['score']:3d} {ioc['type']:8} {ioc['value']}")
print(f"\nOutput: {out_dir}/enriched_iocs.json")
if __name__ == "__main__":
main()
Q & A
Q: The enrichment step takes 4 hours because VirusTotal rate-limits to 4 lookups/second. How do I speed this up?
Several approaches: (1) Prioritize enrichment: don't enrich every IP — filter first. IPs that only appeared once in a capture and have no matching DNS name are likely scanners or CDN IPs; skip them. Enrich only IPs that appeared in DNS responses (meaning something resolved to them) or had sustained sessions. This can reduce the enrich set by 80%. (2) Use a private OSINT API: GreyNoise, Shodan, and commercial threat intel platforms have much higher rate limits and can often answer in bulk (query 1000 IPs at once). (3) Cache previous lookups: maintain a local SQLite database of previous VT lookups with a 7-day TTL. If the same IP was enriched yesterday, skip the API call and use the cached result. Many C2 IPs appear in multiple investigations — caching saves both time and API credits. (4) Parallel requests: use Python's ThreadPoolExecutor to run 4 VT lookups simultaneously (respecting the 4/second limit with a shared semaphore) instead of sequentially. This gives you the same throughput but doesn't stall one slow response from blocking the entire queue.