mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-31 00:17:41 +03:00
445 lines
17 KiB
Python
445 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
OpenCTI IOC Enrichment Pipeline Script
|
|
|
|
Automates IOC enrichment using OpenCTI's pycti library and external APIs:
|
|
- Queries VirusTotal, Shodan, AbuseIPDB, GreyNoise for IOC context
|
|
- Creates STIX 2.1 bundles with enrichment results
|
|
- Updates OpenCTI observables with enrichment data
|
|
- Generates enrichment reports with confidence scoring
|
|
|
|
Requirements:
|
|
pip install pycti stix2 requests
|
|
|
|
Usage:
|
|
python process.py --url http://localhost:8080 --token YOUR_TOKEN --enrich-ip 1.2.3.4
|
|
python process.py --url http://localhost:8080 --token YOUR_TOKEN --enrich-domain evil.com
|
|
python process.py --url http://localhost:8080 --token YOUR_TOKEN --bulk-enrich --days 1
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional
|
|
|
|
try:
|
|
from pycti import OpenCTIApiClient
|
|
except ImportError:
|
|
print("ERROR: pycti not installed. Run: pip install pycti")
|
|
sys.exit(1)
|
|
|
|
import requests
|
|
|
|
|
|
class OpenCTIEnrichmentPipeline:
|
|
"""Automated IOC enrichment pipeline for OpenCTI."""
|
|
|
|
def __init__(self, url: str, token: str):
|
|
self.client = OpenCTIApiClient(url, token)
|
|
self.vt_key = os.environ.get("VIRUSTOTAL_API_KEY", "")
|
|
self.shodan_key = os.environ.get("SHODAN_API_KEY", "")
|
|
self.abuseipdb_key = os.environ.get("ABUSEIPDB_API_KEY", "")
|
|
self.greynoise_key = os.environ.get("GREYNOISE_API_KEY", "")
|
|
self.stats = {
|
|
"enriched": 0,
|
|
"failed": 0,
|
|
"skipped": 0,
|
|
"vt_queries": 0,
|
|
"shodan_queries": 0,
|
|
"abuseipdb_queries": 0,
|
|
"greynoise_queries": 0,
|
|
}
|
|
|
|
def enrich_ip(self, ip_address: str) -> dict:
|
|
"""Enrich an IP address with multiple external sources."""
|
|
results = {"ip": ip_address, "sources": {}, "confidence_score": 0}
|
|
|
|
# VirusTotal enrichment
|
|
if self.vt_key:
|
|
vt_data = self._query_virustotal_ip(ip_address)
|
|
if vt_data:
|
|
results["sources"]["virustotal"] = vt_data
|
|
malicious = vt_data.get("malicious_count", 0)
|
|
total = vt_data.get("total_engines", 0)
|
|
if total > 0:
|
|
results["confidence_score"] += int((malicious / total) * 30)
|
|
|
|
# Shodan enrichment
|
|
if self.shodan_key:
|
|
shodan_data = self._query_shodan(ip_address)
|
|
if shodan_data:
|
|
results["sources"]["shodan"] = shodan_data
|
|
vulns = len(shodan_data.get("vulns", []))
|
|
results["confidence_score"] += min(vulns * 5, 20)
|
|
|
|
# AbuseIPDB enrichment
|
|
if self.abuseipdb_key:
|
|
abuse_data = self._query_abuseipdb(ip_address)
|
|
if abuse_data:
|
|
results["sources"]["abuseipdb"] = abuse_data
|
|
abuse_score = abuse_data.get("abuse_confidence_score", 0)
|
|
results["confidence_score"] += int(abuse_score * 0.3)
|
|
|
|
# GreyNoise enrichment
|
|
if self.greynoise_key:
|
|
gn_data = self._query_greynoise(ip_address)
|
|
if gn_data:
|
|
results["sources"]["greynoise"] = gn_data
|
|
classification = gn_data.get("classification", "unknown")
|
|
if classification == "malicious":
|
|
results["confidence_score"] += 20
|
|
elif classification == "benign":
|
|
results["confidence_score"] = max(0, results["confidence_score"] - 20)
|
|
|
|
results["confidence_score"] = min(100, results["confidence_score"])
|
|
self.stats["enriched"] += 1
|
|
return results
|
|
|
|
def enrich_domain(self, domain: str) -> dict:
|
|
"""Enrich a domain with VirusTotal context."""
|
|
results = {"domain": domain, "sources": {}, "confidence_score": 0}
|
|
|
|
if self.vt_key:
|
|
vt_data = self._query_virustotal_domain(domain)
|
|
if vt_data:
|
|
results["sources"]["virustotal"] = vt_data
|
|
malicious = vt_data.get("malicious_count", 0)
|
|
total = vt_data.get("total_engines", 0)
|
|
if total > 0:
|
|
results["confidence_score"] += int((malicious / total) * 50)
|
|
|
|
results["confidence_score"] = min(100, results["confidence_score"])
|
|
self.stats["enriched"] += 1
|
|
return results
|
|
|
|
def enrich_hash(self, file_hash: str) -> dict:
|
|
"""Enrich a file hash with VirusTotal context."""
|
|
results = {"hash": file_hash, "sources": {}, "confidence_score": 0}
|
|
|
|
if self.vt_key:
|
|
vt_data = self._query_virustotal_hash(file_hash)
|
|
if vt_data:
|
|
results["sources"]["virustotal"] = vt_data
|
|
malicious = vt_data.get("malicious_count", 0)
|
|
total = vt_data.get("total_engines", 0)
|
|
if total > 0:
|
|
results["confidence_score"] += int((malicious / total) * 80)
|
|
|
|
results["confidence_score"] = min(100, results["confidence_score"])
|
|
self.stats["enriched"] += 1
|
|
return results
|
|
|
|
def _query_virustotal_ip(self, ip: str) -> Optional[dict]:
|
|
"""Query VirusTotal for IP address information."""
|
|
try:
|
|
resp = requests.get(
|
|
f"https://www.virustotal.com/api/v3/ip_addresses/{ip}",
|
|
headers={"x-apikey": self.vt_key},
|
|
timeout=30,
|
|
)
|
|
self.stats["vt_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json().get("data", {}).get("attributes", {})
|
|
stats = data.get("last_analysis_stats", {})
|
|
return {
|
|
"malicious_count": stats.get("malicious", 0),
|
|
"suspicious_count": stats.get("suspicious", 0),
|
|
"harmless_count": stats.get("harmless", 0),
|
|
"total_engines": sum(stats.values()) if stats else 0,
|
|
"as_owner": data.get("as_owner", ""),
|
|
"country": data.get("country", ""),
|
|
"reputation": data.get("reputation", 0),
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] VirusTotal query failed for {ip}: {e}")
|
|
return None
|
|
|
|
def _query_virustotal_domain(self, domain: str) -> Optional[dict]:
|
|
"""Query VirusTotal for domain information."""
|
|
try:
|
|
resp = requests.get(
|
|
f"https://www.virustotal.com/api/v3/domains/{domain}",
|
|
headers={"x-apikey": self.vt_key},
|
|
timeout=30,
|
|
)
|
|
self.stats["vt_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json().get("data", {}).get("attributes", {})
|
|
stats = data.get("last_analysis_stats", {})
|
|
return {
|
|
"malicious_count": stats.get("malicious", 0),
|
|
"suspicious_count": stats.get("suspicious", 0),
|
|
"total_engines": sum(stats.values()) if stats else 0,
|
|
"registrar": data.get("registrar", ""),
|
|
"creation_date": data.get("creation_date", ""),
|
|
"reputation": data.get("reputation", 0),
|
|
"categories": data.get("categories", {}),
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] VirusTotal query failed for {domain}: {e}")
|
|
return None
|
|
|
|
def _query_virustotal_hash(self, file_hash: str) -> Optional[dict]:
|
|
"""Query VirusTotal for file hash information."""
|
|
try:
|
|
resp = requests.get(
|
|
f"https://www.virustotal.com/api/v3/files/{file_hash}",
|
|
headers={"x-apikey": self.vt_key},
|
|
timeout=30,
|
|
)
|
|
self.stats["vt_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json().get("data", {}).get("attributes", {})
|
|
stats = data.get("last_analysis_stats", {})
|
|
return {
|
|
"malicious_count": stats.get("malicious", 0),
|
|
"suspicious_count": stats.get("suspicious", 0),
|
|
"total_engines": sum(stats.values()) if stats else 0,
|
|
"type_description": data.get("type_description", ""),
|
|
"size": data.get("size", 0),
|
|
"names": data.get("names", [])[:5],
|
|
"tags": data.get("tags", [])[:10],
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] VirusTotal query failed for {file_hash}: {e}")
|
|
return None
|
|
|
|
def _query_shodan(self, ip: str) -> Optional[dict]:
|
|
"""Query Shodan for IP host information."""
|
|
try:
|
|
resp = requests.get(
|
|
f"https://api.shodan.io/shodan/host/{ip}?key={self.shodan_key}",
|
|
timeout=30,
|
|
)
|
|
self.stats["shodan_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return {
|
|
"ports": data.get("ports", []),
|
|
"vulns": data.get("vulns", []),
|
|
"os": data.get("os"),
|
|
"isp": data.get("isp", ""),
|
|
"org": data.get("org", ""),
|
|
"country": data.get("country_name", ""),
|
|
"city": data.get("city", ""),
|
|
"hostnames": data.get("hostnames", []),
|
|
"tags": data.get("tags", []),
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] Shodan query failed for {ip}: {e}")
|
|
return None
|
|
|
|
def _query_abuseipdb(self, ip: str) -> Optional[dict]:
|
|
"""Query AbuseIPDB for IP reputation."""
|
|
try:
|
|
resp = requests.get(
|
|
"https://api.abuseipdb.com/api/v2/check",
|
|
headers={
|
|
"Key": self.abuseipdb_key,
|
|
"Accept": "application/json",
|
|
},
|
|
params={"ipAddress": ip, "maxAgeInDays": 90, "verbose": True},
|
|
timeout=30,
|
|
)
|
|
self.stats["abuseipdb_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json().get("data", {})
|
|
return {
|
|
"abuse_confidence_score": data.get("abuseConfidenceScore", 0),
|
|
"total_reports": data.get("totalReports", 0),
|
|
"distinct_users": data.get("numDistinctUsers", 0),
|
|
"country": data.get("countryCode", ""),
|
|
"isp": data.get("isp", ""),
|
|
"usage_type": data.get("usageType", ""),
|
|
"is_tor": data.get("isTor", False),
|
|
"is_whitelisted": data.get("isWhitelisted", False),
|
|
"last_reported": data.get("lastReportedAt", ""),
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] AbuseIPDB query failed for {ip}: {e}")
|
|
return None
|
|
|
|
def _query_greynoise(self, ip: str) -> Optional[dict]:
|
|
"""Query GreyNoise for IP classification."""
|
|
try:
|
|
resp = requests.get(
|
|
f"https://api.greynoise.io/v3/community/{ip}",
|
|
headers={"key": self.greynoise_key},
|
|
timeout=30,
|
|
)
|
|
self.stats["greynoise_queries"] += 1
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return {
|
|
"classification": data.get("classification", "unknown"),
|
|
"noise": data.get("noise", False),
|
|
"riot": data.get("riot", False),
|
|
"name": data.get("name", ""),
|
|
"last_seen": data.get("last_seen", ""),
|
|
"message": data.get("message", ""),
|
|
}
|
|
except Exception as e:
|
|
print(f"[-] GreyNoise query failed for {ip}: {e}")
|
|
return None
|
|
|
|
def update_opencti_observable(self, observable_value: str,
|
|
enrichment: dict) -> bool:
|
|
"""Update OpenCTI observable with enrichment results."""
|
|
try:
|
|
# Search for existing observable
|
|
observables = self.client.stix_cyber_observable.list(
|
|
filters={
|
|
"mode": "and",
|
|
"filters": [{"key": "value", "values": [observable_value]}],
|
|
"filterGroups": [],
|
|
}
|
|
)
|
|
|
|
if not observables:
|
|
print(f"[-] Observable {observable_value} not found in OpenCTI")
|
|
return False
|
|
|
|
obs_id = observables[0]["id"]
|
|
score = enrichment.get("confidence_score", 0)
|
|
|
|
# Update confidence score
|
|
self.client.stix_cyber_observable.update_field(
|
|
id=obs_id,
|
|
input={"key": "x_opencti_score", "value": str(score)},
|
|
)
|
|
|
|
# Add enrichment note
|
|
note_content = json.dumps(enrichment["sources"], indent=2)
|
|
self.client.note.create(
|
|
content=f"## Automated Enrichment Results\n```json\n{note_content}\n```",
|
|
abstract=f"Enrichment Score: {score}/100",
|
|
objects=[obs_id],
|
|
)
|
|
|
|
# Add labels based on score
|
|
if score >= 80:
|
|
self.client.stix_cyber_observable.add_label(
|
|
id=obs_id, label_name="enrichment:critical"
|
|
)
|
|
elif score >= 50:
|
|
self.client.stix_cyber_observable.add_label(
|
|
id=obs_id, label_name="enrichment:high"
|
|
)
|
|
|
|
print(f"[+] Updated {observable_value} in OpenCTI (score: {score})")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"[-] Failed to update OpenCTI: {e}")
|
|
self.stats["failed"] += 1
|
|
return False
|
|
|
|
def bulk_enrich_recent(self, days: int = 1, max_items: int = 100):
|
|
"""Bulk enrich recently created observables."""
|
|
date_from = (datetime.now() - timedelta(days=days)).strftime(
|
|
"%Y-%m-%dT00:00:00.000Z"
|
|
)
|
|
|
|
observables = self.client.stix_cyber_observable.list(
|
|
first=max_items,
|
|
filters={
|
|
"mode": "and",
|
|
"filters": [
|
|
{"key": "created_at", "values": [date_from], "operator": "gt"}
|
|
],
|
|
"filterGroups": [],
|
|
},
|
|
)
|
|
|
|
print(f"[+] Found {len(observables)} observables to enrich")
|
|
|
|
for obs in observables:
|
|
entity_type = obs.get("entity_type", "")
|
|
value = obs.get("observable_value", "")
|
|
|
|
if not value:
|
|
continue
|
|
|
|
if entity_type == "IPv4-Addr":
|
|
enrichment = self.enrich_ip(value)
|
|
elif entity_type == "Domain-Name":
|
|
enrichment = self.enrich_domain(value)
|
|
elif entity_type in ("StixFile", "Artifact"):
|
|
hashes = obs.get("hashes", {})
|
|
sha256 = hashes.get("SHA-256", "")
|
|
if sha256:
|
|
enrichment = self.enrich_hash(sha256)
|
|
else:
|
|
continue
|
|
else:
|
|
self.stats["skipped"] += 1
|
|
continue
|
|
|
|
self.update_opencti_observable(value, enrichment)
|
|
|
|
def print_stats(self):
|
|
"""Print enrichment statistics."""
|
|
print("\n=== Enrichment Pipeline Statistics ===")
|
|
for key, value in self.stats.items():
|
|
print(f" {key.replace('_', ' ').title()}: {value}")
|
|
print("=====================================\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="OpenCTI IOC Enrichment Pipeline"
|
|
)
|
|
parser.add_argument("--url", required=True, help="OpenCTI instance URL")
|
|
parser.add_argument("--token", required=True, help="OpenCTI API token")
|
|
parser.add_argument("--enrich-ip", help="Enrich a single IP address")
|
|
parser.add_argument("--enrich-domain", help="Enrich a single domain")
|
|
parser.add_argument("--enrich-hash", help="Enrich a single file hash")
|
|
parser.add_argument(
|
|
"--bulk-enrich", action="store_true", help="Bulk enrich recent observables"
|
|
)
|
|
parser.add_argument("--days", type=int, default=1, help="Lookback days for bulk")
|
|
parser.add_argument("--max-items", type=int, default=100, help="Max items for bulk")
|
|
parser.add_argument(
|
|
"--update-opencti", action="store_true",
|
|
help="Update results back to OpenCTI",
|
|
)
|
|
parser.add_argument("--output", help="Output file for enrichment results")
|
|
|
|
args = parser.parse_args()
|
|
pipeline = OpenCTIEnrichmentPipeline(args.url, args.token)
|
|
|
|
results = None
|
|
|
|
if args.enrich_ip:
|
|
results = pipeline.enrich_ip(args.enrich_ip)
|
|
if args.update_opencti:
|
|
pipeline.update_opencti_observable(args.enrich_ip, results)
|
|
|
|
elif args.enrich_domain:
|
|
results = pipeline.enrich_domain(args.enrich_domain)
|
|
if args.update_opencti:
|
|
pipeline.update_opencti_observable(args.enrich_domain, results)
|
|
|
|
elif args.enrich_hash:
|
|
results = pipeline.enrich_hash(args.enrich_hash)
|
|
if args.update_opencti:
|
|
pipeline.update_opencti_observable(args.enrich_hash, results)
|
|
|
|
elif args.bulk_enrich:
|
|
pipeline.bulk_enrich_recent(days=args.days, max_items=args.max_items)
|
|
|
|
if results:
|
|
print(json.dumps(results, indent=2, default=str))
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(results, f, indent=2, default=str)
|
|
print(f"[+] Results saved to {args.output}")
|
|
|
|
pipeline.print_stats()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|