#!/usr/bin/env python3 """Trivy Container Security Agent - scans images for vulnerabilities, misconfigs, and secrets.""" import json import argparse import logging import subprocess from collections import defaultdict from datetime import datetime logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) def run_trivy_scan(image, scanners="vuln,secret", severity="CRITICAL,HIGH,MEDIUM"): """Run Trivy image scan and return JSON results.""" cmd = [ "trivy", "image", "--format", "json", "--scanners", scanners, "--severity", severity, "--quiet", image, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) if result.returncode != 0 and not result.stdout: logger.error("Trivy scan failed: %s", result.stderr) return {} return json.loads(result.stdout) if result.stdout else {} def run_trivy_misconfig(target_path): """Run Trivy misconfiguration scan on Dockerfile/K8s manifests.""" cmd = [ "trivy", "config", "--format", "json", "--severity", "CRITICAL,HIGH,MEDIUM", "--quiet", target_path, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) return json.loads(result.stdout) if result.stdout else {} def generate_sbom(image, sbom_format="cyclonedx"): """Generate SBOM from container image.""" cmd = ["trivy", "image", "--format", sbom_format, "--quiet", image] result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) return result.stdout if result.returncode == 0 else "" def analyze_vulnerabilities(scan_results): """Analyze vulnerability scan results and produce summary.""" by_severity = defaultdict(int) by_pkg_type = defaultdict(int) fixable = 0 total = 0 cve_list = [] for target in scan_results.get("Results", []): pkg_type = target.get("Type", "unknown") for vuln in target.get("Vulnerabilities", []): severity = vuln.get("Severity", "UNKNOWN") by_severity[severity] += 1 by_pkg_type[pkg_type] += 1 total += 1 if vuln.get("FixedVersion"): fixable += 1 if severity in ("CRITICAL", "HIGH"): cve_list.append({ "cve_id": vuln.get("VulnerabilityID", ""), "package": vuln.get("PkgName", ""), "installed": vuln.get("InstalledVersion", ""), "fixed": vuln.get("FixedVersion", ""), "severity": severity, "cvss_score": vuln.get("CVSS", {}).get("nvd", {}).get("V3Score", 0), "title": vuln.get("Title", "")[:100], }) return { "total_vulnerabilities": total, "by_severity": dict(by_severity), "by_package_type": dict(by_pkg_type), "fixable_count": fixable, "fix_rate": round(fixable / max(total, 1) * 100, 1), "critical_high_cves": sorted(cve_list, key=lambda x: x.get("cvss_score", 0), reverse=True)[:20], } def analyze_secrets(scan_results): """Analyze secret detection results.""" secrets = [] for target in scan_results.get("Results", []): for secret in target.get("Secrets", []): secrets.append({ "rule_id": secret.get("RuleID", ""), "category": secret.get("Category", ""), "title": secret.get("Title", ""), "severity": secret.get("Severity", ""), "target_file": target.get("Target", ""), }) return {"total_secrets": len(secrets), "secrets": secrets[:15]} def analyze_misconfigs(misconfig_results): """Analyze misconfiguration scan results.""" findings = [] for target in misconfig_results.get("Results", []): for mc in target.get("Misconfigurations", []): findings.append({ "avd_id": mc.get("AVDID", ""), "title": mc.get("Title", ""), "severity": mc.get("Severity", ""), "target": target.get("Target", ""), "resolution": mc.get("Resolution", "")[:150], }) by_sev = defaultdict(int) for f in findings: by_sev[f["severity"]] += 1 return {"total_misconfigs": len(findings), "by_severity": dict(by_sev), "findings": findings[:15]} def generate_report(image, vuln_analysis, secret_analysis, misconfig_analysis): critical = vuln_analysis["by_severity"].get("CRITICAL", 0) high = vuln_analysis["by_severity"].get("HIGH", 0) return { "timestamp": datetime.utcnow().isoformat(), "image": image, "vulnerability_summary": vuln_analysis, "secret_summary": secret_analysis, "misconfiguration_summary": misconfig_analysis, "gate_result": "FAIL" if critical > 0 else "WARN" if high > 0 else "PASS", } def main(): parser = argparse.ArgumentParser(description="Trivy Container Security Scanning Agent") parser.add_argument("--image", required=True, help="Container image to scan (e.g., nginx:latest)") parser.add_argument("--config-path", help="Path to Dockerfile/K8s manifests for misconfig scan") parser.add_argument("--severity", default="CRITICAL,HIGH,MEDIUM", help="Severity filter") parser.add_argument("--sbom", action="store_true", help="Generate CycloneDX SBOM") parser.add_argument("--output", default="trivy_scan_report.json") args = parser.parse_args() scan_results = run_trivy_scan(args.image, severity=args.severity) vuln_analysis = analyze_vulnerabilities(scan_results) secret_analysis = analyze_secrets(scan_results) misconfig_analysis = {} if args.config_path: misconfig_results = run_trivy_misconfig(args.config_path) misconfig_analysis = analyze_misconfigs(misconfig_results) if args.sbom: sbom_data = generate_sbom(args.image) if sbom_data: with open(args.output.replace(".json", "_sbom.json"), "w") as f: f.write(sbom_data) report = generate_report(args.image, vuln_analysis, secret_analysis, misconfig_analysis) with open(args.output, "w") as f: json.dump(report, f, indent=2, default=str) logger.info("Trivy: %s - %d vulns (%d critical), %d secrets, gate: %s", args.image, vuln_analysis["total_vulnerabilities"], vuln_analysis["by_severity"].get("CRITICAL", 0), secret_analysis["total_secrets"], report["gate_result"]) print(json.dumps(report, indent=2, default=str)) if __name__ == "__main__": main()