Files
Anthropic-Cybersecurity-Skills/skills/scanning-docker-images-with-trivy/scripts/process.py
T

335 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Trivy Docker Image Scanner - Automated scanning and reporting tool.
Scans Docker images with Trivy, parses results, enforces severity gates,
and generates actionable reports.
"""
import subprocess
import json
import sys
import os
import argparse
from datetime import datetime
from dataclasses import dataclass, field
@dataclass
class ScanPolicy:
fail_on_critical: bool = True
fail_on_high: bool = True
fail_on_medium: bool = False
max_critical: int = 0
max_high: int = 5
max_medium: int = 20
ignore_unfixed: bool = False
scanners: list = field(default_factory=lambda: ["vuln", "secret"])
@dataclass
class VulnSummary:
critical: int = 0
high: int = 0
medium: int = 0
low: int = 0
unknown: int = 0
total: int = 0
def check_trivy_installed() -> bool:
"""Verify Trivy is installed."""
try:
result = subprocess.run(
["trivy", "version"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
version_line = result.stdout.strip().split("\n")[0]
print(f"[*] {version_line}")
return True
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
print("[!] Trivy is not installed. Install from https://trivy.dev")
return False
def update_db():
"""Update Trivy vulnerability database."""
print("[*] Updating vulnerability database...")
result = subprocess.run(
["trivy", "image", "--download-db-only"],
capture_output=True, text=True, timeout=300
)
if result.returncode == 0:
print("[+] Database updated successfully")
else:
print(f"[!] Database update warning: {result.stderr}")
def scan_image(image: str, policy: ScanPolicy) -> dict:
"""Scan a Docker image with Trivy and return JSON results."""
cmd = [
"trivy", "image",
"--format", "json",
"--scanners", ",".join(policy.scanners),
]
if policy.ignore_unfixed:
cmd.append("--ignore-unfixed")
cmd.append(image)
print(f"[*] Scanning image: {image}")
print(f"[*] Scanners: {', '.join(policy.scanners)}")
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=600
)
if result.returncode != 0 and not result.stdout:
print(f"[!] Scan failed: {result.stderr}")
return {}
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
print("[!] Failed to parse Trivy JSON output")
return {}
def parse_results(scan_data: dict) -> tuple:
"""Parse Trivy JSON results into vulnerability summary and details."""
summary = VulnSummary()
vulnerabilities = []
secrets = []
misconfigs = []
results = scan_data.get("Results", [])
for result in results:
target = result.get("Target", "unknown")
result_class = result.get("Class", "")
result_type = result.get("Type", "")
# Parse vulnerabilities
for vuln in result.get("Vulnerabilities", []):
severity = vuln.get("Severity", "UNKNOWN").upper()
if severity == "CRITICAL":
summary.critical += 1
elif severity == "HIGH":
summary.high += 1
elif severity == "MEDIUM":
summary.medium += 1
elif severity == "LOW":
summary.low += 1
else:
summary.unknown += 1
summary.total += 1
vulnerabilities.append({
"target": target,
"type": result_type,
"vuln_id": vuln.get("VulnerabilityID", ""),
"pkg_name": vuln.get("PkgName", ""),
"installed_version": vuln.get("InstalledVersion", ""),
"fixed_version": vuln.get("FixedVersion", ""),
"severity": severity,
"title": vuln.get("Title", ""),
"description": vuln.get("Description", "")[:200],
"cvss_score": vuln.get("CVSS", {}).get("nvd", {}).get("V3Score", 0),
"references": vuln.get("References", [])[:3],
})
# Parse secrets
for secret in result.get("Secrets", []):
secrets.append({
"target": target,
"rule_id": secret.get("RuleID", ""),
"category": secret.get("Category", ""),
"severity": secret.get("Severity", ""),
"title": secret.get("Title", ""),
"match": secret.get("Match", "")[:50] + "...",
})
# Parse misconfigurations
for misconfig in result.get("Misconfigurations", []):
misconfigs.append({
"target": target,
"type": misconfig.get("Type", ""),
"id": misconfig.get("ID", ""),
"title": misconfig.get("Title", ""),
"severity": misconfig.get("Severity", ""),
"message": misconfig.get("Message", ""),
"resolution": misconfig.get("Resolution", ""),
})
return summary, vulnerabilities, secrets, misconfigs
def evaluate_policy(summary: VulnSummary, policy: ScanPolicy) -> tuple:
"""Evaluate scan results against policy. Returns (passed, reasons)."""
passed = True
reasons = []
if policy.fail_on_critical and summary.critical > policy.max_critical:
passed = False
reasons.append(
f"CRITICAL vulnerabilities ({summary.critical}) exceed threshold ({policy.max_critical})"
)
if policy.fail_on_high and summary.high > policy.max_high:
passed = False
reasons.append(
f"HIGH vulnerabilities ({summary.high}) exceed threshold ({policy.max_high})"
)
if policy.fail_on_medium and summary.medium > policy.max_medium:
passed = False
reasons.append(
f"MEDIUM vulnerabilities ({summary.medium}) exceed threshold ({policy.max_medium})"
)
return passed, reasons
def generate_report(image: str, summary: VulnSummary, vulnerabilities: list,
secrets: list, misconfigs: list, policy_passed: bool,
policy_reasons: list) -> dict:
"""Generate comprehensive scan report."""
return {
"scan_metadata": {
"tool": "Trivy",
"image": image,
"timestamp": datetime.utcnow().isoformat() + "Z",
"policy_result": "PASS" if policy_passed else "FAIL",
},
"summary": {
"total_vulnerabilities": summary.total,
"critical": summary.critical,
"high": summary.high,
"medium": summary.medium,
"low": summary.low,
"unknown": summary.unknown,
"secrets_found": len(secrets),
"misconfigurations_found": len(misconfigs),
},
"policy_evaluation": {
"passed": policy_passed,
"failure_reasons": policy_reasons,
},
"critical_vulnerabilities": [
v for v in vulnerabilities if v["severity"] == "CRITICAL"
],
"high_vulnerabilities": [
v for v in vulnerabilities if v["severity"] == "HIGH"
],
"secrets": secrets,
"misconfigurations": misconfigs,
"all_vulnerabilities": vulnerabilities,
}
def print_report(report: dict):
"""Print human-readable scan report."""
meta = report["scan_metadata"]
summary = report["summary"]
print("\n" + "=" * 70)
print("TRIVY IMAGE SCAN REPORT")
print("=" * 70)
print(f"Image: {meta['image']}")
print(f"Timestamp: {meta['timestamp']}")
print(f"Policy: {meta['policy_result']}")
print("=" * 70)
print(f"\nVulnerability Summary:")
print(f" CRITICAL: {summary['critical']}")
print(f" HIGH: {summary['high']}")
print(f" MEDIUM: {summary['medium']}")
print(f" LOW: {summary['low']}")
print(f" UNKNOWN: {summary['unknown']}")
print(f" TOTAL: {summary['total_vulnerabilities']}")
if summary["secrets_found"] > 0:
print(f"\n Secrets Found: {summary['secrets_found']}")
if summary["misconfigurations_found"] > 0:
print(f" Misconfigs Found: {summary['misconfigurations_found']}")
# Print critical/high details
for severity in ["critical", "high"]:
vulns = report.get(f"{severity}_vulnerabilities", [])
if vulns:
print(f"\n{severity.upper()} VULNERABILITIES:")
print("-" * 70)
for v in vulns:
fixed = v.get("fixed_version", "not fixed")
print(f" {v['vuln_id']} | {v['pkg_name']} {v['installed_version']} -> {fixed}")
if v.get("title"):
print(f" {v['title']}")
# Print policy result
policy = report["policy_evaluation"]
if not policy["passed"]:
print(f"\nPOLICY FAILURES:")
for reason in policy["failure_reasons"]:
print(f" - {reason}")
print()
def main():
parser = argparse.ArgumentParser(description="Trivy Docker Image Scanner")
parser.add_argument("image", help="Docker image to scan (e.g., nginx:latest)")
parser.add_argument("--output", "-o", default="trivy_report.json", help="Output JSON file")
parser.add_argument("--max-critical", type=int, default=0, help="Max allowed CRITICAL vulns")
parser.add_argument("--max-high", type=int, default=5, help="Max allowed HIGH vulns")
parser.add_argument("--ignore-unfixed", action="store_true", help="Ignore unfixed vulns")
parser.add_argument("--scanners", default="vuln,secret",
help="Comma-separated scanners: vuln,misconfig,secret,license")
parser.add_argument("--update-db", action="store_true", help="Update DB before scan")
args = parser.parse_args()
if not check_trivy_installed():
sys.exit(1)
if args.update_db:
update_db()
policy = ScanPolicy(
max_critical=args.max_critical,
max_high=args.max_high,
ignore_unfixed=args.ignore_unfixed,
scanners=args.scanners.split(","),
)
scan_data = scan_image(args.image, policy)
if not scan_data:
sys.exit(1)
summary, vulnerabilities, secrets, misconfigs = parse_results(scan_data)
policy_passed, policy_reasons = evaluate_policy(summary, policy)
report = generate_report(
args.image, summary, vulnerabilities, secrets, misconfigs,
policy_passed, policy_reasons
)
print_report(report)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"[*] Full report saved to {args.output}")
if not policy_passed:
print("[!] Policy check FAILED - image should not be deployed")
sys.exit(1)
print("[+] Policy check PASSED - image approved for deployment")
if __name__ == "__main__":
main()