Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

289 lines
10 KiB
Python

#!/usr/bin/env python3
"""CAPE Sandbox automated malware analysis agent.
Submits malware samples to a CAPE Sandbox instance via its REST API,
polls for analysis completion, and retrieves behavioral reports including
process trees, network activity, dropped files, and extracted configs.
"""
import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone
try:
import requests
except ImportError:
print("[!] 'requests' library required: pip install requests", file=sys.stderr)
sys.exit(1)
def get_cape_config():
"""Return CAPE server URL and optional API token."""
server = os.environ.get("CAPE_URL", "http://localhost:8090")
token = os.environ.get("CAPE_API_TOKEN", "")
return server.rstrip("/"), token
def submit_file(server, token, file_path, timeout_minutes=5, machine=None):
"""Submit a file to CAPE for analysis."""
url = f"{server}/apiv2/tasks/create/file/"
headers = {"Authorization": f"Token {token}"} if token else {}
if not os.path.isfile(file_path):
print(f"[!] File not found: {file_path}", file=sys.stderr)
sys.exit(1)
data = {"timeout": timeout_minutes * 60}
if machine:
data["machine"] = machine
print(f"[*] Submitting {os.path.basename(file_path)} to CAPE...")
with open(file_path, "rb") as f:
resp = requests.post(
url,
files={"file": (os.path.basename(file_path), f)},
data=data,
headers=headers,
timeout=120,
)
resp.raise_for_status()
result = resp.json()
if result.get("error"):
print(f"[!] Submission error: {result.get('error_value', result['error'])}", file=sys.stderr)
sys.exit(1)
task_id = result.get("data", {}).get("task_ids", [None])[0]
if not task_id:
task_id = result.get("task_id") or result.get("data", {}).get("task_id")
print(f"[+] Submitted successfully, task ID: {task_id}")
return task_id
def submit_url(server, token, target_url, timeout_minutes=5):
"""Submit a URL to CAPE for analysis."""
url = f"{server}/apiv2/tasks/create/url/"
headers = {"Authorization": f"Token {token}"} if token else {}
data = {"url": target_url, "timeout": timeout_minutes * 60}
print(f"[*] Submitting URL: {target_url}")
resp = requests.post(url, data=data, headers=headers, timeout=60)
resp.raise_for_status()
result = resp.json()
task_id = result.get("data", {}).get("task_ids", [None])[0]
if not task_id:
task_id = result.get("task_id")
print(f"[+] Submitted, task ID: {task_id}")
return task_id
def poll_task_status(server, token, task_id, max_wait=600, interval=15):
"""Poll CAPE until the analysis task completes or times out."""
url = f"{server}/apiv2/tasks/status/{task_id}/"
headers = {"Authorization": f"Token {token}"} if token else {}
print(f"[*] Waiting for analysis to complete (max {max_wait}s)...")
elapsed = 0
while elapsed < max_wait:
try:
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
data = resp.json()
status = data.get("data", data.get("status", "unknown"))
if isinstance(status, dict):
status = status.get("status", "unknown")
if status in ("reported", "completed"):
print(f"[+] Analysis complete (status: {status})")
return True
if status in ("failed_analysis", "failed_processing"):
print(f"[!] Analysis failed: {status}", file=sys.stderr)
return False
print(f" Status: {status} ({elapsed}s elapsed)")
except requests.RequestException as e:
print(f" Connection error: {e}")
time.sleep(interval)
elapsed += interval
print("[!] Timed out waiting for analysis", file=sys.stderr)
return False
def get_report(server, token, task_id):
"""Retrieve the full analysis report for a completed task."""
url = f"{server}/apiv2/tasks/get/report/{task_id}/"
headers = {"Authorization": f"Token {token}"} if token else {}
resp = requests.get(url, headers=headers, timeout=120)
resp.raise_for_status()
return resp.json()
def extract_findings(report):
"""Extract structured findings from CAPE report."""
findings = []
# Signatures (behavioral detections)
for sig in report.get("signatures", []):
findings.append({
"category": "signature",
"name": sig.get("name", "Unknown"),
"severity": sig.get("severity", 0),
"description": sig.get("description", ""),
"families": sig.get("families", []),
"ttp": sig.get("ttp", {}),
})
# Network IOCs
network = report.get("network", {})
for dns_entry in network.get("dns", []):
findings.append({
"category": "network_dns",
"request": dns_entry.get("request", ""),
"type": dns_entry.get("type", ""),
"answers": [a.get("data", "") for a in dns_entry.get("answers", [])],
})
for http_entry in network.get("http", []):
findings.append({
"category": "network_http",
"method": http_entry.get("method", ""),
"uri": http_entry.get("uri", ""),
"host": http_entry.get("host", ""),
"port": http_entry.get("port", 80),
})
# Dropped files
for dropped in report.get("dropped", []):
findings.append({
"category": "dropped_file",
"name": dropped.get("name", ""),
"path": dropped.get("filepath", ""),
"type": dropped.get("type", ""),
"size": dropped.get("size", 0),
"sha256": dropped.get("sha256", ""),
})
# CAPE extracted payloads and configs
cape_data = report.get("CAPE", {})
if isinstance(cape_data, dict):
for cape_item in cape_data.get("payloads", []):
findings.append({
"category": "cape_payload",
"name": cape_item.get("name", ""),
"module": cape_item.get("module_path", ""),
"sha256": cape_item.get("sha256", ""),
"cape_type": cape_item.get("cape_type", ""),
})
for config in cape_data.get("configs", []):
findings.append({
"category": "cape_config",
"family": config.get("family", "unknown"),
"config_data": config,
})
return findings
def format_summary(report, findings):
"""Print human-readable analysis summary."""
info = report.get("info", {})
target = report.get("target", {})
score = report.get("malscore", info.get("score", 0))
print(f"\n{'='*60}")
print(f" CAPE Sandbox Analysis Report")
print(f"{'='*60}")
print(f" Task ID : {info.get('id', 'N/A')}")
print(f" Duration : {info.get('duration', 0)}s")
print(f" Malscore : {score}/10")
file_info = target.get("file", {})
if file_info:
print(f" File : {file_info.get('name', 'N/A')}")
print(f" SHA256 : {file_info.get('sha256', 'N/A')}")
print(f" Type : {file_info.get('type', 'N/A')}")
sig_findings = [f for f in findings if f["category"] == "signature"]
net_dns = [f for f in findings if f["category"] == "network_dns"]
net_http = [f for f in findings if f["category"] == "network_http"]
dropped = [f for f in findings if f["category"] == "dropped_file"]
configs = [f for f in findings if f["category"] == "cape_config"]
print(f"\n Signatures : {len(sig_findings)}")
print(f" DNS Queries : {len(net_dns)}")
print(f" HTTP Reqs : {len(net_http)}")
print(f" Dropped : {len(dropped)}")
print(f" CAPE Configs: {len(configs)}")
if sig_findings:
print(f"\n Top Signatures:")
for s in sorted(sig_findings, key=lambda x: x.get("severity", 0), reverse=True)[:10]:
sev = s.get("severity", 0)
label = "HIGH" if sev >= 3 else "MEDIUM" if sev >= 2 else "LOW"
print(f" [{label:6s}] {s['name']}: {s['description'][:80]}")
if configs:
print(f"\n Extracted Configs:")
for c in configs:
print(f" Family: {c.get('family', 'unknown')}")
return score
def main():
parser = argparse.ArgumentParser(
description="CAPE Sandbox malware analysis agent"
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--file", help="Path to malware sample to submit")
group.add_argument("--url", help="URL to submit for analysis")
group.add_argument("--task-id", type=int, help="Retrieve report for existing task")
parser.add_argument("--output", "-o", help="Output JSON report path")
parser.add_argument("--server", help="CAPE server URL (or set CAPE_URL env var)")
parser.add_argument("--token", help="API token (or set CAPE_API_TOKEN env var)")
parser.add_argument("--machine", help="Specific VM to use for analysis")
parser.add_argument("--timeout", type=int, default=5, help="Analysis timeout in minutes")
parser.add_argument("--wait", type=int, default=600, help="Max seconds to wait for completion")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if args.server:
os.environ["CAPE_URL"] = args.server
if args.token:
os.environ["CAPE_API_TOKEN"] = args.token
server, token = get_cape_config()
if args.task_id:
task_id = args.task_id
elif args.file:
task_id = submit_file(server, token, args.file, args.timeout, args.machine)
else:
task_id = submit_url(server, token, args.url, args.timeout)
if not args.task_id:
if not poll_task_status(server, token, task_id, args.wait):
sys.exit(1)
report = get_report(server, token, task_id)
findings = extract_findings(report)
score = format_summary(report, findings)
output_report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "CAPE Sandbox",
"task_id": task_id,
"malscore": score,
"findings_count": len(findings),
"findings": findings,
"risk_level": (
"CRITICAL" if score >= 8
else "HIGH" if score >= 5
else "MEDIUM" if score >= 3
else "LOW"
),
}
if args.output:
with open(args.output, "w") as f:
json.dump(output_report, f, indent=2)
print(f"\n[+] Report saved to {args.output}")
elif args.verbose:
print(json.dumps(output_report, indent=2))
if __name__ == "__main__":
main()