cve: report only fixable CVEs, grouped by package with the version to bump
Build and push soc-collector / build (push) Successful in 6s
Build and push soc-collector / build (push) Successful in 6s
--ignore-unfixed so unpatchable base-OS CVEs don't drown the actionable ones. Each finding now lists packages (installed -> fixed) and how many CVEs each bump clears, criticals first, plus a 'N of M images clean' line — a to-do list, not a wall of CVE IDs.
This commit is contained in:
+47
-20
@@ -1,4 +1,9 @@
|
|||||||
"""Container-CVE source: Trivy-scan the images of running containers (HIGH/CRITICAL)."""
|
"""Container-CVE source: Trivy-scan running images, reporting only FIXABLE HIGH/CRITICAL CVEs.
|
||||||
|
|
||||||
|
`--ignore-unfixed` is the whole point: a CVE with no available patch isn't a to-do item,
|
||||||
|
it's noise. We report only vulns that have a fix, grouped by package with the version to
|
||||||
|
bump to — so each finding is a short, actionable list instead of a wall of CVE IDs.
|
||||||
|
"""
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -6,8 +11,6 @@ from soc import finding
|
|||||||
|
|
||||||
from . import _docker
|
from . import _docker
|
||||||
|
|
||||||
SEV = {"CRITICAL": "crit", "HIGH": "high", "MEDIUM": "med", "LOW": "low"}
|
|
||||||
|
|
||||||
|
|
||||||
def _running_images():
|
def _running_images():
|
||||||
conts = _docker.get("/containers/json") or []
|
conts = _docker.get("/containers/json") or []
|
||||||
@@ -19,6 +22,19 @@ def _running_images():
|
|||||||
return seen
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def _scan(img):
|
||||||
|
"""Fixable HIGH/CRITICAL vulns for one image (empty list on error)."""
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["trivy", "image", "--quiet", "--scanners", "vuln", "--ignore-unfixed",
|
||||||
|
"--severity", "HIGH,CRITICAL", "--format", "json", img],
|
||||||
|
capture_output=True, text=True, timeout=900)
|
||||||
|
rep = json.loads(proc.stdout or "{}")
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
return [v for res in (rep.get("Results") or []) for v in (res.get("Vulnerabilities") or [])]
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
if not _docker.available():
|
if not _docker.available():
|
||||||
return None # socket not mounted -> skip
|
return None # socket not mounted -> skip
|
||||||
@@ -26,29 +42,40 @@ def run():
|
|||||||
if not images:
|
if not images:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
findings = []
|
findings, clean = [], 0
|
||||||
for img in images:
|
for img in images:
|
||||||
try:
|
vulns = _scan(img)
|
||||||
proc = subprocess.run(
|
|
||||||
["trivy", "image", "--quiet", "--scanners", "vuln",
|
|
||||||
"--severity", "HIGH,CRITICAL", "--format", "json", img],
|
|
||||||
capture_output=True, text=True, timeout=900)
|
|
||||||
rep = json.loads(proc.stdout or "{}")
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
vulns = [v for res in (rep.get("Results") or []) for v in (res.get("Vulnerabilities") or [])]
|
|
||||||
if not vulns:
|
if not vulns:
|
||||||
|
clean += 1
|
||||||
continue
|
continue
|
||||||
worst = "crit" if any(v.get("Severity") == "CRITICAL" for v in vulns) else "high"
|
worst = "crit" if any(v.get("Severity") == "CRITICAL" for v in vulns) else "high"
|
||||||
ids = sorted({v.get("VulnerabilityID", "") for v in vulns if v.get("VulnerabilityID")})
|
# Group by package: which to bump, installed -> fixed, and how many CVEs it clears.
|
||||||
|
pkgs = {}
|
||||||
|
for v in vulns:
|
||||||
|
p = v.get("PkgName", "?")
|
||||||
|
d = pkgs.setdefault(p, {"inst": v.get("InstalledVersion", "?"),
|
||||||
|
"fix": v.get("FixedVersion", "?"), "n": 0})
|
||||||
|
d["n"] += 1
|
||||||
|
top = sorted(pkgs.items(), key=lambda kv: -kv[1]["n"])[:6]
|
||||||
|
evidence = "; ".join(f"{p} {d['inst']}→{d['fix']} ({d['n']})" for p, d in top)
|
||||||
|
if len(pkgs) > 6:
|
||||||
|
evidence += f"; +{len(pkgs) - 6} more package(s)"
|
||||||
findings.append(finding(
|
findings.append(finding(
|
||||||
f"cve-{img}", worst, f"{len(vulns)} HIGH/CRITICAL CVEs in {img}", host=img,
|
f"cve-{img}", worst,
|
||||||
evidence=", ".join(ids[:10]) + (" …" if len(ids) > 10 else ""),
|
f"{len(vulns)} fixable HIGH/CRITICAL CVE(s) in {img}", host=img,
|
||||||
impact=["known-exploitable vulnerabilities in a running image"],
|
evidence=evidence,
|
||||||
fix=[f"pull/rebuild a patched {img.split(':')[0]} image"]))
|
impact=[f"{len(vulns)} CVEs with an available patch across {len(pkgs)} package(s)"],
|
||||||
|
fix=[f"bump: {', '.join(p for p, _ in top)}" + (" …" if len(pkgs) > 6 else ""),
|
||||||
|
f"or pull/rebuild a newer {img.split(':')[0]} base image"]))
|
||||||
|
|
||||||
|
findings.sort(key=lambda f: (f["sev"] != "crit", f["title"])) # criticals first
|
||||||
|
good = []
|
||||||
|
if clean:
|
||||||
|
good.append(f"{clean} of {len(images)} image(s) have no fixable HIGH/CRITICAL CVEs")
|
||||||
|
if not findings and not good:
|
||||||
|
good.append("No fixable HIGH/CRITICAL CVEs in any running image")
|
||||||
return {
|
return {
|
||||||
"scope": f"{len(images)} running image(s)",
|
"scope": f"{len(images)} running image(s) — fixable HIGH/CRITICAL only",
|
||||||
"findings": findings,
|
"findings": findings,
|
||||||
"good": [] if findings else ["No HIGH/CRITICAL CVEs in running images"],
|
"good": good,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user