"""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 subprocess from soc import finding from . import _docker def _running_images(): conts = _docker.get("/containers/json") or [] seen = [] for c in conts: img = c.get("Image", "") if img and img not in seen: seen.append(img) 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(): if not _docker.available(): return None # socket not mounted -> skip images = _running_images() if not images: return None findings, clean = [], 0 for img in images: vulns = _scan(img) if not vulns: clean += 1 continue worst = "crit" if any(v.get("Severity") == "CRITICAL" for v in vulns) else "high" # 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( f"cve-{img}", worst, f"{len(vulns)} fixable HIGH/CRITICAL CVE(s) in {img}", host=img, evidence=evidence, 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 { "scope": f"{len(images)} running image(s) — fixable HIGH/CRITICAL only", "findings": findings, "good": good, }