#!/usr/bin/env python3 """Agent for analyzing threat intelligence feeds via TAXII 2.1 and STIX 2.1.""" import os import json import argparse from datetime import datetime, timedelta, timezone from taxii2client.v21 import Server, Collection, as_pages from stix2 import Indicator, Bundle def discover_taxii_server(url, user=None, password=None): """Discover TAXII 2.1 server and list available API roots and collections.""" server = Server(url, user=user, password=password) info = {"title": server.title, "api_roots": []} for api_root in server.api_roots: root_info = {"title": api_root.title, "collections": []} for collection in api_root.collections: root_info["collections"].append({ "id": collection.id, "title": collection.title, "can_read": collection.can_read, "can_write": collection.can_write, }) info["api_roots"].append(root_info) return info def fetch_indicators(collection_url, user=None, password=None, added_after=None): """Fetch STIX indicators from a TAXII 2.1 collection.""" collection = Collection(collection_url, user=user, password=password) kwargs = {} if added_after: kwargs["added_after"] = added_after indicators = [] for bundle_resource in as_pages(collection.get_objects, per_request=50, **kwargs): if "objects" in bundle_resource: for obj in bundle_resource["objects"]: if obj.get("type") == "indicator": indicators.append(obj) return indicators def normalize_to_stix(ioc_value, ioc_type, source_name, confidence=50): """Convert a raw IOC into a STIX 2.1 Indicator object.""" pattern_map = { "ipv4": f"[ipv4-addr:value = '{ioc_value}']", "domain": f"[domain-name:value = '{ioc_value}']", "sha256": f"[file:hashes.'SHA-256' = '{ioc_value}']", "url": f"[url:value = '{ioc_value}']", "email": f"[email-addr:value = '{ioc_value}']", } pattern = pattern_map.get(ioc_type) if not pattern: raise ValueError(f"Unsupported IOC type: {ioc_type}") indicator = Indicator( name=f"{ioc_type.upper()} indicator: {ioc_value}", pattern=pattern, pattern_type="stix", valid_from=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), confidence=confidence, created_by_ref="identity--f165a29e-a997-5f8a-a63b-4b72b9f2f963", labels=["malicious-activity"], external_references=[{ "source_name": source_name, "description": f"IOC from {source_name}", }], ) return indicator def deduplicate_indicators(indicators): """Deduplicate indicators by pattern value.""" seen = set() unique = [] for ind in indicators: pattern = ind.get("pattern") if isinstance(ind, dict) else ind.pattern if pattern not in seen: seen.add(pattern) unique.append(ind) return unique def score_feed_quality(indicators, known_good_iocs=None): """Score feed quality based on indicator attributes.""" total = len(indicators) if total == 0: return {"total": 0, "score": 0} with_confidence = sum(1 for i in indicators if i.get("confidence", 0) > 0) with_labels = sum(1 for i in indicators if i.get("labels")) with_refs = sum(1 for i in indicators if i.get("external_references")) freshness = sum( 1 for i in indicators if i.get("valid_from") and datetime.fromisoformat(i["valid_from"].replace("Z", "+00:00")) > datetime.now(tz=timezone.utc) - timedelta(days=90) ) score = int( (with_confidence / total * 25) + (with_labels / total * 25) + (with_refs / total * 25) + (freshness / total * 25) ) return { "total": total, "with_confidence": with_confidence, "with_labels": with_labels, "with_external_refs": with_refs, "fresh_last_90d": freshness, "quality_score": score, } def export_stix_bundle(indicators, output_path): """Export indicators as a STIX 2.1 bundle JSON file.""" bundle = Bundle(objects=indicators, allow_custom=True) with open(output_path, "w") as f: f.write(bundle.serialize(pretty=True)) return output_path def classify_ioc_type(value): """Auto-detect IOC type from value.""" import re if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value): return "ipv4" elif re.match(r"^[a-fA-F0-9]{64}$", value): return "sha256" elif re.match(r"^https?://", value): return "url" elif re.match(r"^[^@]+@[^@]+\.[^@]+$", value): return "email" else: return "domain" def main(): parser = argparse.ArgumentParser(description="Threat Intelligence Feed Analysis Agent") parser.add_argument("--taxii-url", help="TAXII 2.1 server discovery URL") parser.add_argument("--collection-url", help="TAXII collection URL to fetch from") parser.add_argument("--user", default=os.getenv("TAXII_USER")) parser.add_argument("--password", default=os.getenv("TAXII_PASSWORD")) parser.add_argument("--added-after", help="Fetch indicators added after (ISO date)") parser.add_argument("--ioc-file", help="File with raw IOCs (one per line) to normalize") parser.add_argument("--source", default="custom-feed", help="Source name for IOCs") parser.add_argument("--output", default="stix_bundle.json", help="Output STIX bundle path") parser.add_argument("--action", choices=[ "discover", "fetch", "normalize", "score", "full_pipeline" ], default="full_pipeline") args = parser.parse_args() if args.action == "discover" and args.taxii_url: info = discover_taxii_server(args.taxii_url, args.user, args.password) print(json.dumps(info, indent=2)) return if args.action in ("fetch", "full_pipeline") and args.collection_url: indicators = fetch_indicators(args.collection_url, args.user, args.password, args.added_after) indicators = deduplicate_indicators(indicators) quality = score_feed_quality(indicators) print(f"[+] Fetched {len(indicators)} unique indicators") print(f"[+] Feed quality score: {quality['quality_score']}/100") if args.action in ("normalize", "full_pipeline") and args.ioc_file: stix_objects = [] with open(args.ioc_file) as f: for line in f: value = line.strip() if not value or value.startswith("#"): continue ioc_type = classify_ioc_type(value) indicator = normalize_to_stix(value, ioc_type, args.source) stix_objects.append(indicator) path = export_stix_bundle(stix_objects, args.output) print(f"[+] Normalized {len(stix_objects)} IOCs -> {path}") if __name__ == "__main__": main()