mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-15 15:34:56 +03:00
335 lines
11 KiB
Python
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()
|