mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-14 03:15:16 +03:00
327 lines
12 KiB
Python
327 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Trivy Container Scanning Pipeline Script
|
|
|
|
Scans Docker images with Trivy, evaluates quality gates, generates reports,
|
|
and optionally uploads results. Supports both local scanning and CI/CD integration.
|
|
|
|
Usage:
|
|
python process.py --image myapp:latest --severity-threshold high
|
|
python process.py --image myapp:latest --output report.json --fail-on-findings
|
|
python process.py --dockerfile ./Dockerfile --scan-config
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
|
|
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4}
|
|
|
|
|
|
@dataclass
|
|
class Vulnerability:
|
|
vuln_id: str
|
|
pkg_name: str
|
|
installed_version: str
|
|
fixed_version: str
|
|
severity: str
|
|
title: str
|
|
description: str
|
|
cvss_score: float = 0.0
|
|
references: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class Misconfiguration:
|
|
misconfig_id: str
|
|
title: str
|
|
severity: str
|
|
message: str
|
|
resolution: str
|
|
file_path: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ScanResult:
|
|
image: str
|
|
vulnerabilities: list = field(default_factory=list)
|
|
misconfigurations: list = field(default_factory=list)
|
|
scan_duration: float = 0.0
|
|
db_version: str = ""
|
|
trivy_version: str = ""
|
|
error: str = ""
|
|
|
|
|
|
def run_trivy_scan(image: str, severity: str = "CRITICAL,HIGH,MEDIUM,LOW",
|
|
ignore_unfixed: bool = True, scan_type: str = "image",
|
|
cache_dir: Optional[str] = None, skip_db_update: bool = False) -> dict:
|
|
"""Execute Trivy scan and return JSON results."""
|
|
cmd = ["trivy", scan_type]
|
|
|
|
if scan_type == "image":
|
|
cmd.extend([
|
|
"--format", "json",
|
|
"--severity", severity,
|
|
])
|
|
if ignore_unfixed:
|
|
cmd.append("--ignore-unfixed")
|
|
if cache_dir:
|
|
cmd.extend(["--cache-dir", cache_dir])
|
|
if skip_db_update:
|
|
cmd.append("--skip-db-update")
|
|
cmd.append(image)
|
|
elif scan_type == "config":
|
|
cmd.extend([
|
|
"--format", "json",
|
|
"--severity", severity,
|
|
image
|
|
])
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600
|
|
)
|
|
|
|
if proc.stdout:
|
|
return json.loads(proc.stdout)
|
|
else:
|
|
return {"error": proc.stderr[:500]}
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return {"error": "Trivy scan timed out after 600 seconds"}
|
|
except FileNotFoundError:
|
|
return {"error": "trivy binary not found. Install from https://trivy.dev"}
|
|
except json.JSONDecodeError:
|
|
return {"error": "Failed to parse Trivy JSON output"}
|
|
|
|
|
|
def parse_vulnerabilities(trivy_json: dict) -> list:
|
|
"""Extract vulnerability findings from Trivy JSON output."""
|
|
vulns = []
|
|
for result in trivy_json.get("Results", []):
|
|
for vuln in result.get("Vulnerabilities", []):
|
|
vulns.append(Vulnerability(
|
|
vuln_id=vuln.get("VulnerabilityID", ""),
|
|
pkg_name=vuln.get("PkgName", ""),
|
|
installed_version=vuln.get("InstalledVersion", ""),
|
|
fixed_version=vuln.get("FixedVersion", ""),
|
|
severity=vuln.get("Severity", "UNKNOWN"),
|
|
title=vuln.get("Title", ""),
|
|
description=vuln.get("Description", "")[:300],
|
|
cvss_score=vuln.get("CVSS", {}).get("nvd", {}).get("V3Score", 0.0),
|
|
references=vuln.get("References", [])[:3]
|
|
))
|
|
return vulns
|
|
|
|
|
|
def parse_misconfigurations(trivy_json: dict) -> list:
|
|
"""Extract misconfiguration findings from Trivy JSON output."""
|
|
misconfigs = []
|
|
for result in trivy_json.get("Results", []):
|
|
for mc in result.get("Misconfigurations", []):
|
|
if mc.get("Status") == "FAIL":
|
|
misconfigs.append(Misconfiguration(
|
|
misconfig_id=mc.get("ID", ""),
|
|
title=mc.get("Title", ""),
|
|
severity=mc.get("Severity", "UNKNOWN"),
|
|
message=mc.get("Message", ""),
|
|
resolution=mc.get("Resolution", ""),
|
|
file_path=result.get("Target", "")
|
|
))
|
|
return misconfigs
|
|
|
|
|
|
def generate_sarif(scan_result: ScanResult) -> dict:
|
|
"""Generate SARIF format output for GitHub Security tab integration."""
|
|
rules = []
|
|
results = []
|
|
rule_ids_seen = set()
|
|
|
|
for vuln in scan_result.vulnerabilities:
|
|
if vuln.vuln_id not in rule_ids_seen:
|
|
rule_ids_seen.add(vuln.vuln_id)
|
|
severity_map = {"CRITICAL": "9.5", "HIGH": "7.5", "MEDIUM": "5.0", "LOW": "2.5"}
|
|
rules.append({
|
|
"id": vuln.vuln_id,
|
|
"shortDescription": {"text": vuln.title or vuln.vuln_id},
|
|
"fullDescription": {"text": vuln.description[:500]},
|
|
"properties": {
|
|
"security-severity": str(vuln.cvss_score or severity_map.get(vuln.severity, "5.0")),
|
|
"tags": ["security", "vulnerability", vuln.severity.lower()]
|
|
}
|
|
})
|
|
|
|
level_map = {"CRITICAL": "error", "HIGH": "error", "MEDIUM": "warning", "LOW": "note"}
|
|
results.append({
|
|
"ruleId": vuln.vuln_id,
|
|
"level": level_map.get(vuln.severity, "warning"),
|
|
"message": {
|
|
"text": f"{vuln.pkg_name} {vuln.installed_version} -> {vuln.fixed_version}: {vuln.title}"
|
|
},
|
|
"locations": [{
|
|
"physicalLocation": {
|
|
"artifactLocation": {"uri": "Dockerfile"},
|
|
"region": {"startLine": 1}
|
|
}
|
|
}]
|
|
})
|
|
|
|
return {
|
|
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
"version": "2.1.0",
|
|
"runs": [{
|
|
"tool": {
|
|
"driver": {
|
|
"name": "Trivy",
|
|
"informationUri": "https://trivy.dev",
|
|
"rules": rules
|
|
}
|
|
},
|
|
"results": results
|
|
}]
|
|
}
|
|
|
|
|
|
def evaluate_quality_gate(scan_result: ScanResult, threshold: str) -> dict:
|
|
"""Evaluate whether scan results pass the quality gate."""
|
|
threshold_level = SEVERITY_ORDER.get(threshold.upper(), 1)
|
|
|
|
blocking_vulns = [
|
|
v for v in scan_result.vulnerabilities
|
|
if SEVERITY_ORDER.get(v.severity, 4) <= threshold_level
|
|
]
|
|
|
|
blocking_misconfigs = [
|
|
m for m in scan_result.misconfigurations
|
|
if SEVERITY_ORDER.get(m.severity, 4) <= threshold_level
|
|
]
|
|
|
|
severity_counts = {}
|
|
for v in scan_result.vulnerabilities:
|
|
severity_counts[v.severity] = severity_counts.get(v.severity, 0) + 1
|
|
|
|
return {
|
|
"passed": len(blocking_vulns) == 0 and len(blocking_misconfigs) == 0,
|
|
"threshold": threshold.upper(),
|
|
"total_vulnerabilities": len(scan_result.vulnerabilities),
|
|
"total_misconfigurations": len(scan_result.misconfigurations),
|
|
"blocking_vulnerabilities": len(blocking_vulns),
|
|
"blocking_misconfigurations": len(blocking_misconfigs),
|
|
"severity_counts": severity_counts,
|
|
"blocking_details": [
|
|
{"id": v.vuln_id, "pkg": v.pkg_name, "severity": v.severity,
|
|
"installed": v.installed_version, "fixed": v.fixed_version}
|
|
for v in blocking_vulns[:20]
|
|
]
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Trivy Container Scanning Pipeline")
|
|
parser.add_argument("--image", required=True, help="Docker image to scan (e.g., myapp:latest)")
|
|
parser.add_argument("--output", default="trivy-report.json", help="Output report file path")
|
|
parser.add_argument("--sarif-output", default=None, help="SARIF output file path")
|
|
parser.add_argument("--severity-threshold", default="high",
|
|
choices=["critical", "high", "medium", "low"],
|
|
help="Minimum severity to block pipeline")
|
|
parser.add_argument("--fail-on-findings", action="store_true",
|
|
help="Exit non-zero if quality gate fails")
|
|
parser.add_argument("--ignore-unfixed", action="store_true", default=True,
|
|
help="Ignore vulnerabilities without fixes")
|
|
parser.add_argument("--scan-config", action="store_true",
|
|
help="Also scan for misconfigurations")
|
|
parser.add_argument("--dockerfile", default=None,
|
|
help="Path to Dockerfile for misconfiguration scanning")
|
|
parser.add_argument("--cache-dir", default=None, help="Trivy cache directory")
|
|
parser.add_argument("--skip-db-update", action="store_true", help="Skip DB update")
|
|
args = parser.parse_args()
|
|
|
|
scan_result = ScanResult(image=args.image)
|
|
start_time = datetime.now(timezone.utc)
|
|
|
|
print(f"[*] Scanning image: {args.image}")
|
|
trivy_json = run_trivy_scan(
|
|
args.image,
|
|
ignore_unfixed=args.ignore_unfixed,
|
|
cache_dir=args.cache_dir,
|
|
skip_db_update=args.skip_db_update
|
|
)
|
|
|
|
if "error" in trivy_json:
|
|
print(f"[ERROR] {trivy_json['error']}")
|
|
sys.exit(2)
|
|
|
|
scan_result.vulnerabilities = parse_vulnerabilities(trivy_json)
|
|
scan_result.db_version = trivy_json.get("Metadata", {}).get("DB", {}).get("UpdatedAt", "unknown")
|
|
|
|
if args.scan_config and args.dockerfile:
|
|
print(f"[*] Scanning configuration: {args.dockerfile}")
|
|
config_path = os.path.dirname(args.dockerfile) or "."
|
|
config_json = run_trivy_scan(config_path, scan_type="config")
|
|
if "error" not in config_json:
|
|
scan_result.misconfigurations = parse_misconfigurations(config_json)
|
|
|
|
scan_result.scan_duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
|
|
quality_gate = evaluate_quality_gate(scan_result, args.severity_threshold)
|
|
|
|
report = {
|
|
"scan_metadata": {
|
|
"image": args.image,
|
|
"scan_date": datetime.now(timezone.utc).isoformat(),
|
|
"duration_seconds": scan_result.scan_duration,
|
|
"db_version": scan_result.db_version
|
|
},
|
|
"quality_gate": quality_gate,
|
|
"vulnerabilities": [
|
|
{
|
|
"id": v.vuln_id, "package": v.pkg_name, "severity": v.severity,
|
|
"installed": v.installed_version, "fixed": v.fixed_version,
|
|
"title": v.title, "cvss": v.cvss_score
|
|
}
|
|
for v in sorted(scan_result.vulnerabilities,
|
|
key=lambda x: SEVERITY_ORDER.get(x.severity, 4))
|
|
],
|
|
"misconfigurations": [
|
|
{"id": m.misconfig_id, "title": m.title, "severity": m.severity,
|
|
"message": m.message, "resolution": m.resolution}
|
|
for m in scan_result.misconfigurations
|
|
]
|
|
}
|
|
|
|
with open(os.path.abspath(args.output), "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"[*] Report: {os.path.abspath(args.output)}")
|
|
|
|
if args.sarif_output:
|
|
sarif = generate_sarif(scan_result)
|
|
with open(os.path.abspath(args.sarif_output), "w") as f:
|
|
json.dump(sarif, f, indent=2)
|
|
print(f"[*] SARIF: {os.path.abspath(args.sarif_output)}")
|
|
|
|
print(f"\n[*] Vulnerabilities: {len(scan_result.vulnerabilities)} "
|
|
f"| Misconfigs: {len(scan_result.misconfigurations)}")
|
|
|
|
if quality_gate["passed"]:
|
|
print(f"[PASS] Quality gate passed.")
|
|
else:
|
|
print(f"[FAIL] Quality gate failed. {quality_gate['blocking_vulnerabilities']} blocking vulns.")
|
|
for d in quality_gate["blocking_details"][:10]:
|
|
print(f" - [{d['severity']}] {d['id']} in {d['pkg']} ({d['installed']} -> {d['fixed']})")
|
|
|
|
if args.fail_on_findings and not quality_gate["passed"]:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|