Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automates the enrichment of raw indicators of compromise with multi-source threat intelligence context using SOAR platforms, Python pipelines, or TIP playbooks to reduce analyst triage time and standardize enrichment outputs. Use when building automated enrichment workflows integrated with SIEM alerts, email submission pipelines, or bulk IOC processing from threat feeds. Activates for requests involving SOAR enrichment, Cortex XSOAR, Splunk SOAR, TheHive, Python enrichment pipelines, or automated IOC processing.
.claude/skills/automating-ioc-enrichment/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-18 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
Use this skill when:
Do not use this skill for fully automated blocking decisions without human review — enrichment automation should inform decisions, not execute blocks autonomously for high-impact actions.
Define the enrichment flow for each IOC type:
SIEM Alert → Extract IOCs → Classify Type → Route to enrichment functions
IP Address → AbuseIPDB + Shodan + VirusTotal IP + MISP
Domain → VirusTotal Domain + PassiveTotal + Shodan + MISP
URL → URLScan.io + VirusTotal URL + Google Safe Browse
File Hash → VirusTotal Files + MalwareBazaar + MISP
→ Aggregate results → Calculate confidence score → Update alert → Notify analystpythonimport requests import time from dataclasses import dataclass, field from typing import Optional RATE_LIMIT_DELAY = 0.25 # 4 requests/second for VT free tier @dataclass class EnrichmentResult: ioc_value: str ioc_type: str vt_malicious: int = 0 vt_total: int = 0 abuse_confidence: int = 0 shodan_ports: list = field(default_factory=list) misp_events: list = field(default_factory=list) confidence_score: int = 0 def enrich_ip(ip: str, vt_key: str, abuse_key: str, shodan_key: str) -> EnrichmentResult: result = EnrichmentResult(ip, "ip") # VirusTotal IP lookup vt_resp = requests.get( f"https://www.virustotal.com/api/v3/ip_addresses/{ip}", headers={"x-apikey": vt_key} ) if vt_resp.status_code == 200: stats = vt_resp.json()["data"]["attributes"]["last_analysis_stats"] result.vt_malicious = stats.get("malicious", 0) result.vt_total = sum(stats.values()) time.sleep(RATE_LIMIT_DELAY) # AbuseIPDB abuse_resp = requests.get( "https://api.abuseipdb.com/api/v2/check", headers={"Key": abuse_key, "Accept": "application/json"}, params={"ipAddress": ip, "maxAgeInDays": 90} ) if abuse_resp.status_code == 200: result.abuse_confidence = abuse_resp.json()["data"]["abuseConfidenceScore"] # Calculate composite confidence score result.confidence_score = min( (result.vt_malicious / max(result.vt_total, 1)) * 60 + (result.abuse_confidence / 100) * 40, 100 ) return result def enrich_hash(sha256: str, vt_key: str) -> EnrichmentResult: result = EnrichmentResult(sha256, "sha256") vt_resp = requests.get( f"https://www.virustotal.com/api/v3/files/{sha256}", headers={"x-apikey": vt_key} ) if vt_resp.status_code == 200: stats = vt_resp.json()["data"]["attributes"]["last_analysis_stats"] result.vt_malicious = stats.get("malicious", 0) result.vt_total = sum(stats.values()) result.confidence_score = int((result.vt_malicious / max(result.vt_total, 1)) * 100) return result
In Cortex XSOAR, create an enrichment playbook:
!vt-file-scan or !vt-ip-scan commands!abuseipdb-check-ip command!misp-search for cross-referencingpythonimport time from functools import wraps def rate_limited(max_per_second): min_interval = 1.0 / max_per_second def decorator(func): last_called = [0.0] @wraps(func) def wrapper(*args, **kwargs): elapsed = time.time() - last_called[0] wait = min_interval - elapsed if wait > 0: time.sleep(wait) result = func(*args, **kwargs) last_called[0] = time.time() return result return wrapper return decorator def retry_on_429(max_retries=3): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): response = func(*args, **kwargs) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 60)) time.sleep(retry_after) else: return response return wrapper return decorator
Track pipeline performance weekly:
| Term | Definition | |------|-----------| | SOAR | Security Orchestration, Automation, and Response — platform for automating security workflows and integrating disparate tools | | Enrichment Playbook | Automated workflow sequence that adds contextual intelligence to raw security events | | Rate Limiting | API provider restrictions on request frequency (e.g., VT free: 4 requests/minute); pipelines must respect these limits | | Composite Confidence Score | Single score aggregating signals from multiple enrichment sources using weighted formula | | Fan-out Pattern | Parallel execution of multiple enrichment queries simultaneously to minimize total enrichment latency |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +36 percentage points is the difference between those two pass rates over the 20 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.