#!/usr/bin/env python3 """Analysis pipeline runner — orchestrate multi-stage malware analysis workflows.""" from __future__ import annotations import argparse import hashlib import json import os import sys import time from pathlib import Path def load_config(config_path: str) -> dict: """Load pipeline configuration from YAML file.""" try: import yaml with open(config_path) as f: return yaml.safe_load(f) except ImportError: print("[!] PyYAML not installed. Install with: pip install pyyaml", file=sys.stderr) sys.exit(1) def compute_sha256(filepath: str) -> str: """Compute SHA-256 hash of a file.""" sha256 = hashlib.sha256() with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): sha256.update(chunk) return sha256.hexdigest() def discover_samples(input_dir: str, file_filter: str = "*") -> list[Path]: """Discover samples in the input directory.""" input_path = Path(input_dir) if not input_path.is_dir(): print(f"[!] Input directory not found: {input_dir}", file=sys.stderr) return [] samples = [] for pattern in file_filter.split(","): samples.extend(input_path.glob(pattern.strip())) return sorted(p for p in samples if p.is_file()) def check_already_processed(sha256: str, output_dir: str) -> bool: """Check if a sample has already been processed (for resume support).""" result_file = Path(output_dir) / sha256 / "unified_report.json" return result_file.exists() def submit_to_sandbox(filepath: str, sandbox: str, sandbox_url: str = "") -> dict: """Submit a sample to a sandbox for dynamic analysis.""" import requests if sandbox == "cape": url = f"{sandbox_url}/apiv2/tasks/create/file/" with open(filepath, "rb") as f: resp = requests.post(url, files={"file": f}, data={"timeout": 300}) return resp.json() elif sandbox == "anyrun": api_key = os.environ.get("ANYRUN_API_KEY", "") with open(filepath, "rb") as f: resp = requests.post( "https://api.any.run/v1/analysis", headers={"Authorization": f"API-Key {api_key}"}, files={"file": f}, ) return resp.json() return {"error": f"Unsupported sandbox: {sandbox}"} def retrieve_sandbox_results(task_id: str, sandbox: str, sandbox_url: str = "") -> dict: """Retrieve analysis results from a sandbox.""" import requests if sandbox == "cape": resp = requests.get(f"{sandbox_url}/apiv2/tasks/get/report/{task_id}/") return resp.json() return {"error": f"Unsupported sandbox: {sandbox}"} def enrich_with_virustotal(sha256: str) -> dict: """Enrich a hash with VirusTotal data.""" import requests api_key = os.environ.get("VT_API_KEY") if not api_key: return {"error": "VT_API_KEY not set"} resp = requests.get( f"https://www.virustotal.com/api/v3/files/{sha256}", headers={"x-apikey": api_key}, ) if resp.status_code == 200: attrs = resp.json()["data"]["attributes"] stats = attrs.get("last_analysis_stats", {}) return { "detections": f"{stats.get('malicious', 0)}/{sum(stats.values())}", "family": attrs.get("popular_threat_classification", {}).get("suggested_threat_label"), "tags": attrs.get("tags", []), } return {"error": f"VT API returned {resp.status_code}"} def enrich_with_malwarebazaar(sha256: str) -> dict: """Enrich a hash with MalwareBazaar data.""" import requests resp = requests.post( "https://mb-api.abuse.ch/api/v1/", data={"query": "get_info", "hash": sha256}, ) if resp.status_code == 200: data = resp.json() if data.get("query_status") == "ok" and data.get("data"): entry = data["data"][0] return { "family": entry.get("signature"), "tags": entry.get("tags"), "first_seen": entry.get("first_seen"), } return {"not_found": True} def aggregate_results(results_dir: str) -> dict: """Aggregate results from all analysis stages into a unified report.""" results_path = Path(results_dir) unified = {"stages": {}} for result_file in results_path.glob("*.json"): stage_name = result_file.stem with open(result_file) as f: unified["stages"][stage_name] = json.load(f) return unified def notify_slack(webhook_url: str, message: dict) -> None: """Send notification to Slack webhook.""" import requests requests.post(webhook_url, json=message, timeout=10) def run_pipeline(config: dict, sample_path: str, output_dir: str) -> dict: """Run the full analysis pipeline on a single sample.""" sha256 = compute_sha256(sample_path) sample_output = Path(output_dir) / sha256 sample_output.mkdir(parents=True, exist_ok=True) results = { "sample": { "filename": Path(sample_path).name, "sha256": sha256, "size_bytes": Path(sample_path).stat().st_size, }, "pipeline": config.get("pipeline", {}).get("name", "unnamed"), "stages": {}, "start_time": time.time(), } for stage in config.get("pipeline", {}).get("stages", []): stage_name = stage["name"] print(f" [{stage_name}] Running...") start = time.time() try: # Stage execution would call the appropriate skill scripts here results["stages"][stage_name] = { "status": "completed", "duration_seconds": round(time.time() - start, 1), } except Exception as e: results["stages"][stage_name] = { "status": "failed", "error": str(e), "duration_seconds": round(time.time() - start, 1), } if stage.get("on_fail") == "stop": break results["processing_time_seconds"] = round(time.time() - results["start_time"], 1) del results["start_time"] # Write unified report report_path = sample_output / "unified_report.json" report_path.write_text(json.dumps(results, indent=2)) return results def main() -> None: parser = argparse.ArgumentParser(description="Malware Analysis Pipeline Runner") parser.add_argument("--config", help="Pipeline configuration YAML file") parser.add_argument("--input", help="Single sample file path") parser.add_argument("--input-dir", help="Directory of samples for batch processing") parser.add_argument("--output-dir", default="./results", help="Output directory for results") parser.add_argument( "--mode", choices=["single", "batch", "submit", "retrieve", "enrich", "aggregate", "notify", "status"], default="single", help="Execution mode", ) parser.add_argument("--sandbox", choices=["cape", "cuckoo", "anyrun", "joesandbox"]) parser.add_argument("--sandbox-url", help="Sandbox API URL") parser.add_argument("--task-id", help="Sandbox task ID for retrieval") parser.add_argument("--source", choices=["virustotal", "malwarebazaar", "otx", "shodan"]) parser.add_argument("--hash", help="Hash for enrichment (format: sha256:value)") parser.add_argument("--results-dir", help="Results directory for aggregation") parser.add_argument("--parallel", type=int, default=1, help="Number of parallel workers") parser.add_argument("--resume", action="store_true", help="Skip already-processed samples") parser.add_argument("--filter", default="*", help="File glob filter for batch mode") parser.add_argument("--notify-email", help="Email address for notifications") parser.add_argument("--notify-threshold", default="high", choices=["low", "medium", "high", "critical"]) parser.add_argument("--output", help="Output file path (overrides output-dir for single results)") parser.add_argument("--format", choices=["json", "csv", "markdown"], default="json") args = parser.parse_args() print("[*] Malware Analysis Pipeline Runner") if args.mode == "batch": if not args.config or not args.input_dir: parser.error("--config and --input-dir required for batch mode") config = load_config(args.config) samples = discover_samples(args.input_dir, args.filter) print(f"[*] Found {len(samples)} samples in {args.input_dir}") for i, sample in enumerate(samples, 1): sha256 = compute_sha256(str(sample)) if args.resume and check_already_processed(sha256, args.output_dir): print(f"[{i}/{len(samples)}] Skipping {sample.name} (already processed)") continue print(f"[{i}/{len(samples)}] Processing {sample.name}...") run_pipeline(config, str(sample), args.output_dir) elif args.mode == "single": if not args.config or not args.input: parser.error("--config and --input required for single mode") config = load_config(args.config) results = run_pipeline(config, args.input, args.output_dir) print(f"[*] Analysis complete. Verdict: {results.get('verdict', 'pending')}") elif args.mode == "submit": if not args.sandbox or not args.input: parser.error("--sandbox and --input required for submit mode") result = submit_to_sandbox(args.input, args.sandbox, args.sandbox_url or "") print(json.dumps(result, indent=2)) elif args.mode == "retrieve": if not args.sandbox or not args.task_id: parser.error("--sandbox and --task-id required for retrieve mode") result = retrieve_sandbox_results(args.task_id, args.sandbox, args.sandbox_url or "") print(json.dumps(result, indent=2)) elif args.mode == "enrich": if not args.hash: parser.error("--hash required for enrich mode") _, hash_value = args.hash.split(":", 1) if ":" in args.hash else ("sha256", args.hash) if args.source == "virustotal": print(json.dumps(enrich_with_virustotal(hash_value), indent=2)) elif args.source == "malwarebazaar": print(json.dumps(enrich_with_malwarebazaar(hash_value), indent=2)) elif args.mode == "aggregate": if not args.results_dir: parser.error("--results-dir required for aggregate mode") unified = aggregate_results(args.results_dir) output = args.output or "unified_report.json" Path(output).write_text(json.dumps(unified, indent=2)) print(f"[*] Aggregated results written to {output}") elif args.mode == "status": print("[*] Pipeline status: OK") print(f"[*] Output directory: {args.output_dir}") print("[*] Done.") if __name__ == "__main__": main()