Files
a13labs.infra/terraform/modules/utils/trivy/resources/send_report.py
T
alexandre.pires 255ba6aec9 feat(trivy): Enhance Trivy report generation and metrics handling
- Updated `generate_report.sh` to include additional scanners and output paths.
- Enhanced `send_report.py` with new data classes for misconfigurations and vulnerabilities.
- Improved parsing logic for Trivy output, supporting both Kubernetes and image scans.
- Added Prometheus metrics generation for various scan results, including misconfigurations and vulnerabilities.
- Introduced unit tests for parsing Trivy output and generating Prometheus metrics.
- Modified Terraform variables for SMTP port type and added Pushgateway URL variable.
2026-05-22 23:41:26 +02:00

727 lines
30 KiB
Python

#!/usr/bin/env python3
import os
import time
import smtplib
from email.message import EmailMessage
import json
import re
import urllib.request
import urllib.error
from datetime import datetime
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
# Imports and Configuration
@dataclass
class Misconfiguration:
"""Represents a Trivy misconfiguration finding."""
status: str
message: str
severity: str
check_id: str = ""
title: str = ""
check_namespace: str = ""
primary_url: str = ""
@dataclass
class Vulnerability:
"""Represents a Trivy vulnerability finding."""
vulnerability_id: str
package_name: str
severity: str
target: str = ""
vuln_type: str = ""
class_name: str = ""
installed_version: str = ""
fixed_version: str = ""
title: str = ""
@dataclass
class Resource:
"""Represents a Trivy scan resource."""
type: str
name: str
kind: Optional[str] = None
namespace: Optional[str] = None
image_digests: List[str] = field(default_factory=list)
metadata: List[dict] = field(default_factory=list)
misconf_summary: dict = field(default_factory=dict)
misconfigurations: List[Misconfiguration] = field(default_factory=list)
vulnerabilities: List[Vulnerability] = field(default_factory=list)
vulnerability_summary: dict = field(default_factory=dict)
successes_count: int = 0
failures_count: int = 0
@dataclass
class ParseResult:
"""Parsed Trivy scan result."""
cluster_name: Optional[str] = None
resources: List[Resource] = field(default_factory=list)
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
duration: Optional[float] = None
image: Optional[str] = None
# Configuration
PUSHGATEWAY_URL = os.environ.get("PUSHGATEWAY_URL", "http://pushgateway:9091/metrics")
PUSHGATEWAY_NAMESPACE = os.environ.get("PUSHGATEWAY_NAMESPACE", "trivy")
PUSHGATEWAY_JOB = os.environ.get("PUSHGATEWAY_JOB", "trivy-metrics")
METRICS_PREFIX = "trivy"
REQUEST_TIMEOUT = 30
REPORT_PATH = os.environ.get("REPORT_PATH", "/tmp/report.html")
TRIVY_OUTPUT_FILE = os.environ.get("TRIVY_OUTPUT_FILE", "/tmp/trivy.json")
WAIT_TIMEOUT = int(os.environ.get("WAIT_TIMEOUT", "200"))
SLEEP_INTERVAL = int(os.environ.get("SLEEP_INTERVAL", "5") )
# Metric definitions
METRICS = {
"vulnerabilities_found": {
"type": "gauge",
"description": "Total number of vulnerabilities found",
"labels": ["severity", "type", "package", "vulnerability_id"]
},
"vulnerabilities_by_severity": {
"type": "gauge",
"description": "Count of vulnerabilities grouped by severity",
"labels": ["severity"]
},
"vulnerabilities_by_type": {
"type": "gauge",
"description": "Count of vulnerabilities grouped by type",
"labels": ["type"]
},
"scans_completed": {
"type": "counter",
"description": "Total number of scans completed",
"labels": ["status"]
},
"scan_duration_seconds": {
"type": "gauge",
"description": "Duration of the last scan in seconds",
"labels": ["status"]
},
"image_scanned": {
"type": "gauge",
"description": "Image that was scanned",
"labels": ["image"]
},
"scan_timestamp": {
"type": "gauge",
"description": "Timestamp of the scan",
"labels": []
},
"severities_found": {
"type": "gauge",
"description": "Severities found in the scan",
"labels": ["severity"]
}
}
def parse_trivy_output(output: str) -> ParseResult:
"""Parse Trivy scan output and extract metrics."""
# Try to parse JSON output if available
if output.strip().startswith("{"):
try:
data = json.loads(output)
# Check if it's Kubernetes/container scan JSON
if "Resources" in data or "ClusterName" in data:
result = ParseResult(cluster_name=data.get("ClusterName"))
for resource_data in data.get("Resources", []):
misconfigurations = []
vulnerabilities = []
misconf_summary: dict = {}
vulnerability_summary: dict = {}
resource_type = "kubernetes"
for res in resource_data.get("Results", []):
resource_type = res.get("Type") or resource_type
for k, v in res.get("MisconfSummary", {}).items():
misconf_summary[k] = misconf_summary.get(k, 0) + v
for mc in res.get("Misconfigurations", []):
misconfigurations.append(Misconfiguration(
status=mc.get("Status", ""),
message=mc.get("Message", ""),
severity=mc.get("Severity", ""),
check_id=mc.get("ID", ""),
title=mc.get("Title", ""),
check_namespace=mc.get("Namespace", ""),
primary_url=mc.get("PrimaryURL", ""),
))
for v in res.get("Vulnerabilities", []):
severity = v.get("Severity", "UNKNOWN")
vulnerability_summary[severity] = vulnerability_summary.get(severity, 0) + 1
vulnerabilities.append(Vulnerability(
vulnerability_id=v.get("VulnerabilityID", ""),
package_name=v.get("PkgName", ""),
severity=severity,
target=res.get("Target", ""),
vuln_type=res.get("Type", ""),
class_name=res.get("Class", ""),
installed_version=v.get("InstalledVersion", ""),
fixed_version=v.get("FixedVersion", ""),
title=v.get("Title", ""),
))
resource = Resource(
type=resource_type,
name=resource_data.get("Name", ""),
kind=resource_data.get("Kind"),
namespace=resource_data.get("Namespace"),
metadata=resource_data.get("Metadata", []),
misconf_summary=misconf_summary,
misconfigurations=misconfigurations,
vulnerabilities=vulnerabilities,
vulnerability_summary=vulnerability_summary,
successes_count=misconf_summary.get("Successes", 0),
failures_count=misconf_summary.get("Failures", 0),
)
result.resources.append(resource)
return result
except json.JSONDecodeError:
pass
# Parse text output for image scanning
metrics_data = {
"vulnerabilities": [],
"image": None,
"start_time": None,
"end_time": None,
"summary": {}
}
# Extract image name
image_match = re.search(r'Scanning image: (.+)', output)
if image_match:
metrics_data["image"] = image_match.group(1).strip()
# Extract scan duration
duration_match = re.search(r'Spent.*? (\d+(?:\.\d+)?)', output)
if duration_match:
metrics_data["scan_duration"] = float(duration_match.group(1))
# Extract severity counts
severity_patterns = {
"CRITICAL": r'Critical:.*?(\d+)',
"HIGH": r'High:.*?(\d+)',
"MEDIUM": r'Medium:.*?(\d+)',
"LOW": r'Low:.*?(\d+)',
"UNKNOWN": r'Unknown:.*?(\d+)',
"STYLE": r'Style:.*?(\d+)',
"NOT_SET": r'Not Set:.*?(\d+)'
}
severity_counts = {}
for severity, pattern in severity_patterns.items():
match = re.search(pattern, output)
if match:
severity_counts[severity] = int(match.group(1))
metrics_data["summary"] = severity_counts
for line in output.split('\n'):
# Match vulnerability lines
vuln_match = re.search(r'\[.*?\].*?CVE-?\d+\.\d+-\d+', line)
if vuln_match:
# Parse vulnerability details
vuln_data = {
"cve": None,
"package": None,
"type": None,
"severity": None,
"fix_version": None
}
# Extract CVE
cve_match = re.search(r'CVE-?\d+\.\d+-\d+', line)
if cve_match:
vuln_data["cve"] = cve_match.group()
# Extract package name
pkg_match = re.search(r'package: (\S+)', line)
if pkg_match:
vuln_data["package"] = pkg_match.group(1)
# Extract vulnerability ID
vuln_id_match = re.search(r'ID: ([\w\-]+)', line)
if vuln_id_match:
vuln_data["vulnerability_id"] = vuln_id_match.group(1)
# Extract severity
severity_match = re.search(r'severity: (\S+)', line)
if severity_match:
vuln_data["severity"] = severity_match.group(1)
if any(vuln_data.values()):
metrics_data["vulnerabilities"].append(vuln_data)
# Convert to ParseResult for backward compatibility
result = ParseResult()
result.image = metrics_data.get("image")
result.start_time = metrics_data.get("start_time")
result.end_time = metrics_data.get("end_time")
result.duration = metrics_data.get("scan_duration")
return result
def generate_prometheus_metrics(scan_data: Dict[str, Any], scan_duration: Optional[float] = None) -> str:
"""Generate Prometheus metrics format from scan data."""
lines = []
# Scan metadata
timestamp = datetime.utcnow().isoformat() + "Z"
lines.append(f'# HELP {METRICS_PREFIX}_scan_timestamp Timestamp of the scan')
lines.append(f'# TYPE {METRICS_PREFIX}_scan_timestamp gauge')
lines.append(f'{METRICS_PREFIX}_scan_timestamp 1')
# Image scanned
if scan_data.get("image"):
image = scan_data["image"].replace(":", "_").replace("/", "_")
lines.append(f'# HELP {METRICS_PREFIX}_image_scanned Image that was scanned')
lines.append(f'# TYPE {METRICS_PREFIX}_image_scanned gauge')
lines.append(f'{METRICS_PREFIX}_image_scanned{{image="{image}"}} 1')
# Duration
if scan_duration is not None:
lines.append(f'# HELP {METRICS_PREFIX}_scan_duration_seconds Duration of the last scan in seconds')
lines.append(f'# TYPE {METRICS_PREFIX}_scan_duration_seconds gauge')
lines.append(f'{METRICS_PREFIX}_scan_duration_seconds{{status="success"}} {scan_duration}')
# Vulnerabilities by severity
summary = scan_data.get("summary", {})
for severity, count in summary.items():
if count > 0:
lines.append(f'# HELP {METRICS_PREFIX}_vulnerabilities_by_severity Count of vulnerabilities grouped by severity')
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerabilities_by_severity gauge')
lines.append(f'{METRICS_PREFIX}_vulnerabilities_by_severity{{severity="{severity}"}} {count}')
# Individual vulnerabilities
vulnerabilities = scan_data.get("vulnerabilities", [])
for vuln in vulnerabilities:
cve = vuln.get("cve", "unknown")
package = vuln.get("package", "unknown")
vuln_id = vuln.get("vulnerability_id", cve)
severity = vuln.get("severity", "unknown")
# Truncate CVE to fit in label
cve_short = cve[-12:] if len(cve) > 12 else cve
lines.append(f'# HELP {METRICS_PREFIX}_vulnerabilities_found Total number of vulnerabilities found')
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerabilities_found gauge')
labels = {
"severity": severity,
"type": vuln.get("type", "unknown"),
"package": package,
"vulnerability_id": cve_short
}
# Sanitize labels for Prometheus
safe_labels = {k: str(v).replace('"', "").replace("'", "")[:20] for k, v in labels.items()}
label_string = ",".join(f'{k}="{v}"' for k, v in safe_labels.items())
lines.append(f'{METRICS_PREFIX}_vulnerabilities_found{{{label_string}}} 1')
# Clean up duplicate HELP lines
cleaned_lines = []
last_help = None
for line in lines:
if line.startswith('# HELP'):
if last_help and line.split()[2] == last_help.split()[2]:
continue # Skip duplicate HELP
last_help = line.split()[2]
cleaned_lines.append(line)
return '\n'.join(cleaned_lines)
def push_metrics_to_pushgateway(metrics: str):
"""Push metrics to PushGateway."""
try:
validated_metrics = sanitize_prometheus_metrics(metrics)
if not validated_metrics:
print("✗ No valid Prometheus metrics remained after sanitization")
return False
req = urllib.request.Request(
PUSHGATEWAY_URL,
data=(validated_metrics + "\n").encode("utf-8"),
headers={"Content-Type": "text/plain; charset=utf-8"},
method="POST",
)
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as response:
status_code = response.getcode()
response_body = response.read().decode("utf-8", errors="replace")
if status_code == 200:
print(f"✓ Successfully pushed {len(validated_metrics.split(chr(10)))} lines of metrics to PushGateway")
return True
else:
print(f"✗ Failed to push metrics to PushGateway. Status code: {status_code}")
print(f"Response: {response_body}")
return False
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f"✗ Error pushing metrics to PushGateway: HTTP {e.code} {e.reason}")
print(f"Response: {body}")
return False
except urllib.error.URLError as e:
print(f"✗ Error pushing metrics to PushGateway: {e}")
return False
def _has_balanced_label_quotes(label_block: str) -> bool:
"""Return True when all double quotes in a label block are balanced."""
in_quotes = False
escaped = False
for ch in label_block:
if escaped:
escaped = False
continue
if ch == "\\":
escaped = True
continue
if ch == '"':
in_quotes = not in_quotes
return not in_quotes
def sanitize_prometheus_metrics(metrics: str) -> str:
"""Drop malformed metric lines so one bad sample does not break the whole push."""
sanitized_lines: List[str] = []
dropped_lines = 0
merged_duplicates = 0
series_totals: Dict[str, float] = {}
series_order: List[str] = []
def _format_value(value: float) -> str:
return str(int(value)) if value.is_integer() else format(value, "g")
for idx, raw_line in enumerate(metrics.splitlines(), start=1):
line = raw_line.rstrip("\r")
if not line:
continue
if line.startswith("#"):
sanitized_lines.append(line)
continue
if "{" in line:
open_brace = line.find("{")
close_brace = line.find("}", open_brace + 1)
if close_brace == -1:
print(f"⚠ Dropping malformed metric line {idx}: missing closing brace")
dropped_lines += 1
continue
label_block = line[open_brace + 1:close_brace]
if not _has_balanced_label_quotes(label_block):
print(f"⚠ Dropping malformed metric line {idx}: unbalanced label quotes")
dropped_lines += 1
continue
parts = line.rsplit(" ", 1)
if len(parts) != 2:
print(f"⚠ Dropping malformed metric line {idx}: missing value")
dropped_lines += 1
continue
series_id, value_raw = parts[0], parts[1].strip()
try:
value = float(value_raw)
except ValueError:
print(f"⚠ Dropping malformed metric line {idx}: invalid numeric value '{value_raw}'")
dropped_lines += 1
continue
if series_id in series_totals:
merged_duplicates += 1
series_totals[series_id] += value
else:
series_order.append(series_id)
series_totals[series_id] = value
if dropped_lines:
print(f"⚠ Dropped {dropped_lines} malformed metric line(s) before push")
for series_id in series_order:
sanitized_lines.append(f"{series_id} {_format_value(series_totals[series_id])}")
if merged_duplicates:
print(f"⚠ Merged {merged_duplicates} duplicate metric line(s) before push")
return "\n".join(sanitized_lines)
def dispatch_email(report_path, subject):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = os.environ.get("EMAIL_FROM", "noreply@example.com")
msg["To"] = os.environ.get("EMAIL_TO", "admin@example.com")
if report_path:
msg.set_content("Trivy scan report attached.")
# Attach the report file
with open(report_path, "rb") as f:
file_data = f.read()
file_name = os.path.basename(report_path)
msg.add_attachment(file_data, maintype="text", subtype="plain", filename=file_name)
else:
msg.set_content("Trivy scan report not found. Check logs for details.")
smtp_host = os.environ.get("SMTP_HOST", "smtp.example.com")
smtp_port = int(os.environ.get("SMTP_PORT", "587"))
smtp_user = os.environ.get("SMTP_USER", "user")
smtp_pass = os.environ.get("SMTP_PASS", "pass")
if smtp_port == 465:
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
else:
server = smtplib.SMTP(smtp_host, smtp_port)
server.starttls()
try:
server.login(smtp_user, smtp_pass)
except smtplib.SMTPAuthenticationError:
print("SMTP Authentication Error: Check your SMTP credentials.")
server.quit()
return
server.send_message(msg)
server.quit()
print("Report sent.")
def generate_prometheus_metrics_from_parse_result(scan_result: ParseResult) -> str:
"""Generate Prometheus metrics from ParseResult object."""
lines = []
lines.append(f'# HELP {METRICS_PREFIX}_cluster_scan_timestamp Timestamp of the cluster scan')
lines.append(f'# TYPE {METRICS_PREFIX}_cluster_scan_timestamp gauge')
lines.append(f'{METRICS_PREFIX}_cluster_scan_timestamp 1')
lines.append(f'# HELP {METRICS_PREFIX}_cluster_name Name of the cluster')
lines.append(f'# TYPE {METRICS_PREFIX}_cluster_name gauge')
def _safe_label(value: str) -> str:
# Prometheus label values cannot contain unescaped control chars/newlines.
cleaned = re.sub(r"[\x00-\x1F\x7F]+", " ", value)
return cleaned.replace('\\', '\\\\').replace('"', '\\"')
def _safe_truncated_label(value: str, max_len: int) -> str:
# Truncate before escaping so we never cut through an escape sequence.
raw_truncated = re.sub(r"[\x00-\x1F\x7F]+", " ", value)[:max_len]
return _safe_label(raw_truncated)
safe_cluster_name = _safe_label(scan_result.cluster_name or "")
lines.append(f'{METRICS_PREFIX}_cluster_name{{cluster_name="{safe_cluster_name}"}} 1')
resource_type_header_written = False
image_digest_header_written = False
successes_header_written = False
failures_header_written = False
misconfig_header_written = False
vulnerability_total_header_written = False
vulnerability_severity_header_written = False
vulnerability_header_written = False
for resource in scan_result.resources:
rtype = _safe_label(resource.type or "")
rname = _safe_label(resource.name or "")
rkind = _safe_label(resource.kind or "")
rns = _safe_label(resource.namespace or "")
resource_labels = f'type="{rtype}",kind="{rkind}",name="{rname}",namespace="{rns}"'
if not resource_type_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_resource_type Type of resource')
lines.append(f'# TYPE {METRICS_PREFIX}_resource_type gauge')
resource_type_header_written = True
lines.append(f'{METRICS_PREFIX}_resource_type{{{resource_labels}}} 1')
if resource.image_digests:
if not image_digest_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_image_digest Image digest')
lines.append(f'# TYPE {METRICS_PREFIX}_image_digest gauge')
image_digest_header_written = True
for digest in resource.image_digests:
safe_digest = _safe_truncated_label(digest, 64)
lines.append(f'{METRICS_PREFIX}_image_digest{{{resource_labels},digest="{safe_digest}"}} 1')
if resource.misconf_summary:
summary = resource.misconf_summary
successes = summary.get('Successes', 0)
failures = summary.get('Failures', 0)
if not successes_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration_successes Successful checks')
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration_successes gauge')
successes_header_written = True
lines.append(f'{METRICS_PREFIX}_misconfiguration_successes{{{resource_labels}}} {successes}')
if not failures_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration_failures Failed checks')
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration_failures gauge')
failures_header_written = True
lines.append(f'{METRICS_PREFIX}_misconfiguration_failures{{{resource_labels}}} {failures}')
misconfig_counts = {}
for m in resource.misconfigurations:
key = (
_safe_label(m.severity or ""),
_safe_label(m.status or ""),
_safe_truncated_label(m.check_id or "", 32),
_safe_truncated_label(m.check_namespace or "", 64),
_safe_truncated_label(m.title or "", 100),
_safe_truncated_label(m.primary_url or "", 200),
)
misconfig_counts[key] = misconfig_counts.get(key, 0) + 1
for (safe_severity, safe_status, safe_check_id, safe_check_namespace, safe_title, safe_primary_url), count in misconfig_counts.items():
if not misconfig_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_misconfiguration Misconfiguration finding')
lines.append(f'# TYPE {METRICS_PREFIX}_misconfiguration gauge')
misconfig_header_written = True
lines.append(
f'{METRICS_PREFIX}_misconfiguration{{{resource_labels},'
f'severity="{safe_severity}",status="{safe_status}",'
f'check_id="{safe_check_id}",check_namespace="{safe_check_namespace}",title="{safe_title}",primary_url="{safe_primary_url}"}} {count}'
)
if not vulnerability_total_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability_total Total vulnerabilities for resource')
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability_total gauge')
vulnerability_total_header_written = True
lines.append(f'{METRICS_PREFIX}_vulnerability_total{{{resource_labels}}} {len(resource.vulnerabilities)}')
for severity, count in resource.vulnerability_summary.items():
safe_severity = _safe_label(severity)
if not vulnerability_severity_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability_severity_count Vulnerabilities grouped by severity for resource')
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability_severity_count gauge')
vulnerability_severity_header_written = True
lines.append(
f'{METRICS_PREFIX}_vulnerability_severity_count{{{resource_labels},severity="{safe_severity}"}} {count}'
)
vulnerability_counts = {}
for v in resource.vulnerabilities:
key = (
_safe_label(v.severity or ""),
_safe_truncated_label(v.vulnerability_id or "", 32),
_safe_truncated_label(v.package_name or "", 64),
_safe_truncated_label(v.target or "", 96),
_safe_truncated_label(v.vuln_type or "", 24),
_safe_truncated_label(v.class_name or "", 24),
)
vulnerability_counts[key] = vulnerability_counts.get(key, 0) + 1
for (safe_severity, safe_vuln_id, safe_package, safe_target, safe_vuln_type, safe_class_name), count in vulnerability_counts.items():
if not vulnerability_header_written:
lines.append(f'# HELP {METRICS_PREFIX}_vulnerability Vulnerability finding')
lines.append(f'# TYPE {METRICS_PREFIX}_vulnerability gauge')
vulnerability_header_written = True
lines.append(
f'{METRICS_PREFIX}_vulnerability{{{resource_labels},'
f'severity="{safe_severity}",vulnerability_id="{safe_vuln_id}",'
f'package="{safe_package}",target="{safe_target}",'
f'vuln_type="{safe_vuln_type}",class="{safe_class_name}"}} {count}'
)
if scan_result.duration:
lines.append(f'# HELP {METRICS_PREFIX}_scan_duration Duration of the last scan in seconds')
lines.append(f'# TYPE {METRICS_PREFIX}_scan_duration gauge')
lines.append(f'{METRICS_PREFIX}_scan_duration {scan_result.duration}')
return '\n'.join(lines)
def main():
"""Main function to process Trivy scan output and push metrics."""
print("🔍 Trivy Metrics Exporter")
print(f"📍 PushGateway URL: {PUSHGATEWAY_URL}")
print(f"🏷️ Namespace: {PUSHGATEWAY_NAMESPACE}")
print(f"💼 Job: {PUSHGATEWAY_JOB}")
print(f"⏳ Request timeout: {REQUEST_TIMEOUT} seconds")
print()
print("Trivy report path:", REPORT_PATH)
print("Starting to wait for the report to be generated...")
start = time.time()
while not os.path.exists(REPORT_PATH):
if time.time() - start > WAIT_TIMEOUT:
print("Timeout waiting for report to be generated.")
dispatch_email(None, "Trivy Scan Report - Timeout")
exit(1)
print("Still waiting for report to be generated...")
time.sleep(SLEEP_INTERVAL)
print("Report generated, proceeding to send email...")
time.sleep(5) # Wait for a bit to ensure the file is fully written
dispatch_email(REPORT_PATH, "Trivy Scan Report - " + time.strftime("%Y-%m-%d %H:%M:%S"))
if not os.path.exists(TRIVY_OUTPUT_FILE):
print(f"✗ Output file not found: {TRIVY_OUTPUT_FILE}")
print(" Make sure Trivy has written its output before running this script.")
exit(1)
print(f"📄 Reading scan output from: {TRIVY_OUTPUT_FILE}")
with open(TRIVY_OUTPUT_FILE, 'r') as f:
output = f.read()
print(f"📊 Parsing scan output...")
# Parse the output
scan_data = parse_trivy_output(output)
# Handle both ParseResult (for Kubernetes scans) and old dict format (for image scans)
if hasattr(scan_data, 'resources'):
# ParseResult object (Kubernetes/container scan output)
resource_count = len(scan_data.resources) if scan_data.resources else 0
if resource_count == 0:
print("⚠️ No vulnerabilities found in scan output")
print(" Pushing basic scan metadata...")
scan_data = None
else:
print(f"Found {resource_count} resource(s) to scan")
# Generate Prometheus metrics
print("🔧 Generating Prometheus metrics...")
metrics = generate_prometheus_metrics_from_parse_result(scan_data)
# Push to PushGateway
print("🚀 Pushing metrics to PushGateway...")
success = push_metrics_to_pushgateway(metrics)
if success:
print("✓ Metrics successfully pushed to PushGateway")
# Also print a summary to stdout for dashboards
print("\n📈 Scan Summary:")
print(f" Resources scanned: {resource_count}")
total_vulns = sum(resource.misconf_summary.get('Failures', 0) for resource in scan_data.resources)
print(f" Misconfigurations found: {total_vulns}")
for resource in scan_data.resources:
if resource.misconf_summary:
print(f" Resource: {resource.name} ({resource.type})")
for severity, count in resource.misconf_summary.items():
if count > 0:
print(f" - {severity}: {count}")
else:
print("✗ Failed to push metrics")
exit(1)
else:
# Old dict format (image scan output) - keep existing behavior for backward compatibility
print("⚠️ No vulnerabilities found in scan output")
print(" Pushing basic scan metadata...")
# For backward compatibility with image scans
metrics = generate_prometheus_metrics(scan_data)
print("🚀 Pushing metrics to PushGateway...")
success = push_metrics_to_pushgateway(metrics)
if success:
print("✓ Metrics successfully pushed to PushGateway")
else:
print("✗ Failed to push metrics")
exit(1)
if __name__ == "__main__":
main()