Files

220 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""MISP Threat Intelligence Sharing agent - creates events, manages attributes, searches IOCs, and validates sharing configuration via PyMISP"""
import argparse
import json
import sys
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
try:
from pymisp import PyMISP, MISPEvent, MISPAttribute, MISPTag
HAS_PYMISP = True
except ImportError:
HAS_PYMISP = False
MISP_ATTRIBUTE_TYPES = {
"ip-dst", "ip-src", "domain", "hostname", "url", "md5", "sha1",
"sha256", "filename", "email-src", "email-dst", "mutex", "regkey",
"user-agent", "vulnerability", "link", "text", "comment",
}
TLP_TAGS = {
"white": "tlp:white",
"green": "tlp:green",
"amber": "tlp:amber",
"amber+strict": "tlp:amber+strict",
"red": "tlp:red",
}
def load_data(path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def connect_misp(url, api_key, ssl=True):
"""Initialize PyMISP connection."""
if not HAS_PYMISP:
return None, "pymisp not installed (pip install pymisp)"
misp = PyMISP(url, api_key, ssl=ssl)
return misp, "connected"
def create_event_from_data(misp, event_data):
"""Create a MISP event with attributes and tags."""
event = MISPEvent()
event.info = event_data.get("info", "Untitled Event")
event.distribution = event_data.get("distribution", 1) # 0=org, 1=community, 2=connected, 3=all
event.threat_level_id = event_data.get("threat_level", 2) # 1=high, 2=medium, 3=low, 4=undefined
event.analysis = event_data.get("analysis", 0) # 0=initial, 1=ongoing, 2=complete
for attr in event_data.get("attributes", []):
attr_type = attr.get("type", "text")
value = attr.get("value", "")
category = attr.get("category", "")
to_ids = attr.get("to_ids", True)
comment = attr.get("comment", "")
if attr_type in MISP_ATTRIBUTE_TYPES and value:
event.add_attribute(type=attr_type, value=value, category=category,
to_ids=to_ids, comment=comment)
for tag_name in event_data.get("tags", []):
event.add_tag(tag_name)
tlp = event_data.get("tlp", "").lower()
if tlp in TLP_TAGS:
event.add_tag(TLP_TAGS[tlp])
if misp:
result = misp.add_event(event)
return result
return event.to_dict()
def validate_event_quality(event_data):
"""Validate event data quality for sharing readiness."""
findings = []
eid = event_data.get("id", event_data.get("info", "unknown"))
if not event_data.get("info"):
findings.append({
"type": "missing_event_info",
"severity": "high",
"resource": str(eid),
"detail": "Event lacks descriptive info/title",
})
attrs = event_data.get("attributes", event_data.get("Attribute", []))
if not attrs:
findings.append({
"type": "no_attributes",
"severity": "high",
"resource": str(eid),
"detail": "Event has no IOC attributes",
})
attr_types = Counter(a.get("type", "unknown") for a in attrs)
if len(attr_types) == 1 and len(attrs) > 1:
findings.append({
"type": "single_attribute_type",
"severity": "low",
"resource": str(eid),
"detail": f"All {len(attrs)} attributes are type '{list(attr_types.keys())[0]}' - consider enriching",
})
tags = event_data.get("tags", event_data.get("Tag", []))
tag_names = [t.get("name", t) if isinstance(t, dict) else t for t in tags]
has_tlp = any("tlp:" in t.lower() for t in tag_names)
if not has_tlp:
findings.append({
"type": "missing_tlp_tag",
"severity": "high",
"resource": str(eid),
"detail": "Event lacks TLP classification tag",
})
has_mitre = any("mitre-attack" in t.lower() or "attack-pattern" in t.lower() for t in tag_names)
if not has_mitre and len(attrs) > 0:
findings.append({
"type": "missing_mitre_mapping",
"severity": "medium",
"resource": str(eid),
"detail": "Event lacks MITRE ATT&CK technique mapping",
})
dist = event_data.get("distribution", -1)
if dist == 3:
findings.append({
"type": "unrestricted_distribution",
"severity": "medium",
"resource": str(eid),
"detail": "Event set to 'All communities' distribution - verify this is intentional",
})
for attr in attrs:
val = attr.get("value", "")
atype = attr.get("type", "")
if atype in ("ip-dst", "ip-src") and val in ("127.0.0.1", "0.0.0.0", "10.0.0.1", "192.168.1.1"):
findings.append({
"type": "private_ip_ioc",
"severity": "high",
"resource": str(eid),
"detail": f"Private/localhost IP '{val}' used as IOC - will generate false positives",
})
if atype in ("md5", "sha1", "sha256") and len(val) < 32:
findings.append({
"type": "invalid_hash_length",
"severity": "high",
"resource": str(eid),
"detail": f"Hash attribute '{val}' is too short for type {atype}",
})
return findings
def validate_sharing_config(config):
"""Validate MISP sharing and feed configuration."""
findings = []
servers = config.get("sync_servers", [])
if not servers:
findings.append({
"type": "no_sync_servers",
"severity": "medium",
"resource": "misp_config",
"detail": "No synchronization servers configured for intelligence sharing",
})
for srv in servers:
if not srv.get("pull", False) and not srv.get("push", False):
findings.append({
"type": "inactive_sync_server",
"severity": "medium",
"resource": srv.get("name", srv.get("url", "")),
"detail": "Sync server has neither pull nor push enabled",
})
feeds = config.get("feeds", [])
enabled_feeds = [f for f in feeds if f.get("enabled", False)]
if not enabled_feeds:
findings.append({
"type": "no_active_feeds",
"severity": "medium",
"resource": "misp_config",
"detail": "No active threat intelligence feeds configured",
})
return findings
def analyze(data):
findings = []
events = data.get("events", [data] if "info" in data or "Attribute" in data else [])
if isinstance(data, list):
events = data
for evt in events:
findings.extend(validate_event_quality(evt))
if "sync_servers" in data or "feeds" in data:
findings.extend(validate_sharing_config(data))
return findings
def generate_report(input_path):
data = load_data(input_path)
findings = analyze(data)
sev = Counter(f["severity"] for f in findings)
cats = Counter(f["type"] for f in findings)
return {
"report": "misp_threat_intelligence_sharing",
"generated_at": datetime.utcnow().isoformat() + "Z",
"total_findings": len(findings),
"severity_summary": dict(sev),
"finding_categories": dict(cats),
"findings": findings,
}
def main():
ap = argparse.ArgumentParser(description="MISP Threat Intelligence Sharing Agent")
ap.add_argument("--input", required=True, help="Input JSON with MISP events or config")
ap.add_argument("--output", help="Output JSON report path")
ap.add_argument("--misp-url", help="MISP instance URL for live operations")
ap.add_argument("--api-key", help="MISP API key")
args = ap.parse_args()
report = generate_report(args.input)
out = json.dumps(report, indent=2)
if args.output:
Path(args.output).write_text(out, encoding="utf-8")
print(f"Report written to {args.output}")
else:
print(out)
if __name__ == "__main__":
main()