#!/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 from collections import Counter from datetime import datetime from pathlib import Path try: from pymisp import PyMISP, MISPEvent 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()