"""The CVE page: server-rendered HTML with no script at all. Every string that reaches the document goes through e(), the single escape path; links additionally go through safe_url(). The style block is the only inline resource and is allowed by hash. """ import base64 import hashlib import html import math from datetime import timedelta from urllib.parse import parse_qs, urlencode import report as report_mod import trivy from state import parse_ts REPORT_STALE = timedelta(hours=26) DB_STALE = timedelta(hours=48) SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN") SEV_RANK = {name: i for i, name in enumerate(SEVERITIES)} STATUS_RANK = {"fix-available": 0, "open": 1, "accepted": 2} STATE_RANK = {name: i for i, name in enumerate(report_mod.STATES)} PAGE_SIZES = (25, 50, 100, 200, 500) DEFAULT_SIZE = 100 _MAX_PARAM = 200 _EXPANSION_LIMIT = 200 # per-target CVE list in the inventory view; the remainder is linked STYLE = """ body{font:14px/1.45 system-ui,sans-serif;margin:0;background:#fff;color:#1a1a1a} main{padding:0 16px 32px} header{padding:12px 16px;background:#f4f4f6;border-bottom:1px solid #ccc} h1{font-size:18px;margin:0 0 8px} nav a{margin-right:12px} .warn{background:#fff0d6;border:2px solid #b35900;padding:8px 12px;margin:8px 0;font-weight:600} .status{font-weight:700} .tiles span{display:inline-block;margin:2px 10px 2px 0} table{border-collapse:collapse;width:100%;margin-top:8px} th,td{border-bottom:1px solid #ddd;padding:4px 6px;text-align:left;vertical-align:top} th a{text-decoration:none} .sev-CRITICAL{color:#a00;font-weight:700} .sev-HIGH{color:#b35900;font-weight:700} form{margin:8px 0} form label{margin-right:8px} code{word-break:break-all} @media (prefers-color-scheme:dark){body{background:#16181c;color:#e6e6e6} header{background:#22252b;border-color:#3a3d44} th,td{border-color:#33363c} a{color:#7fb4ff} .warn{background:#3d2a12;border-color:#c98422}} """ STYLE_HASH = base64.b64encode(hashlib.sha256(STYLE.encode("utf-8")).digest()).decode("ascii") CSP = ("default-src 'none'; style-src 'sha256-" + STYLE_HASH + "'; base-uri 'none'; " "form-action 'self'; frame-ancestors 'none'") SECURITY_HEADERS = { "Content-Security-Policy": CSP, "X-Content-Type-Options": "nosniff", "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", } def e(value): """The one escape path for anything placed in the document.""" return "" if value is None else html.escape(str(value), quote=True) def safe_url(value): return trivy.safe_url(value) if isinstance(value, str) else "" def _link(url, label): url = safe_url(url) return '' + e(label) + "" if url else e(label) def _parse(query): raw = parse_qs(query or "", keep_blank_values=False) params = {k: v[0][:_MAX_PARAM] for k, v in raw.items() if v} view = params.get("view", "cves") params["view"] = view if view in ("cves", "workloads", "inventory") else "cves" try: params["page"] = max(1, int(params.get("page", "1"))) except ValueError: params["page"] = 1 try: size = int(params.get("size", str(DEFAULT_SIZE))) except ValueError: size = DEFAULT_SIZE params["size"] = size if size in PAGE_SIZES else DEFAULT_SIZE return params def _qs(params, **overrides): merged = dict(params) merged.update(overrides) pairs = sorted((k, str(v)) for k, v in merged.items() if v not in (None, "")) return "?" + urlencode(pairs) def _age(moment, now): if moment is None: return "unknown" seconds = int((now - moment).total_seconds()) if seconds < 0: return "in the future" hours, remainder = divmod(seconds, 3600) minutes = remainder // 60 return str(hours) + "h " + str(minutes) + "m ago" def _finding_text(finding, target): parts = [finding["id"], finding["package"], finding["title"], target["id"], target["image"]] for workload in target["workloads"]: parts.extend((workload["namespace"], workload["name"], *workload["containers"])) return " ".join(parts).lower() def _match_workload(target, params): namespace, workload = params.get("namespace"), params.get("workload") if not namespace and not workload: return True for item in target["workloads"]: if namespace and item["namespace"] != namespace: continue if workload and workload not in (item["name"], item["kind"] + "/" + item["name"]): continue return True return False def _match_finding(finding, target, params): if params.get("severity") and finding["severity"] != params["severity"].upper(): return False if params.get("status") and finding["status"] != params["status"]: return False fix = params.get("fix") if fix == "yes" and not finding["fixedVersion"]: return False if fix == "no" and finding["fixedVersion"]: return False if not _match_workload(target, params): return False text = params.get("q", "").lower() return not text or text in _finding_text(finding, target) def _paginate(rows, params): total = len(rows) size = params["size"] pages = max(1, math.ceil(total / size)) page = min(params["page"], pages) start = (page - 1) * size return rows[start:start + size], total, page, pages, start def _pager(params, total, page, pages, start, shown, noun): first = start + 1 if total else 0 summary = "Showing " + str(first) + "–" + str(start + shown) + " of " + str(total) + " " + noun \ + " (page " + str(page) + " of " + str(pages) + ")" links = [] if page > 1: links.append('previous') if page < pages: links.append('next') return "

" + e(summary) + " " + " ".join(links) + "

" def _th(params, label, key): direction = "desc" if params.get("sort") == key and params.get("dir", "asc") == "asc" else "asc" return '' + e(label) + "" def _table(head, rows, empty): if not rows: return "

" + e(empty) + "

" return "" + "".join(head) + "" + "".join(rows) + "
" def _sev(severity): return '' + e(severity) + "" def _state_label(target): return "target " + e(target["state"]) + (" (" + e(target["reason"]) + ")" if target.get("reason") else "") def _status_label(finding): """One finding's own status, with its own acceptance: never another image's.""" label = "status " + e(finding["status"]) acceptance = finding.get("acceptance") if acceptance: label += " (" + e(acceptance["reason"]) + ", until " + e(acceptance["expires"]) + ")" return label def _workload_lines(target, finding=None): """One line per workload, each carrying the target state and, in a CVE expansion, the status.""" suffix = " — " + _state_label(target) + (" — " + _status_label(finding) if finding else "") items = [] for workload in target["workloads"]: items.append("
  • " + e(workload["namespace"]) + " / " + e(workload["kind"]) + " / " + e(workload["name"]) + " — containers: " + e(", ".join(workload["containers"]) or "none") + suffix + "
  • ") return "" def _status_summary(statuses): """The row-level status. A CVE accepted on one image and open on another reads as mixed.""" counts = {} for status in statuses: counts[status] = counts.get(status, 0) + 1 if len(counts) == 1: return e(next(iter(counts))) return "mixed: " + ", ".join(e(counts[s]) + " " + e(s) for s in sorted(counts, key=STATUS_RANK.get)) def _remediation_cell(images, remediation): if not images: return "" out = [] for image in sorted(images): entry = remediation.get(image) if entry is None: out.append(e(image) + ": no data") elif entry["state"] == "pr-open" and safe_url(entry.get("url")): out.append(e(image) + ": " + _link(entry["url"], "pr-open")) else: out.append(e(image) + ": " + e(entry["state"])) return "
    ".join(out) # --- views ------------------------------------------------------------------------------------------ def _cves(report, targets, params, remediation): groups = {} for finding in report["findings"]: target = targets[finding["targetId"]] if not _match_finding(finding, target, params): continue group = groups.setdefault(finding["id"], { "id": finding["id"], "severity": finding["severity"], "cvss": None, "packages": set(), "fixed": set(), "firstSeen": finding["firstSeenAt"], "statuses": set(), "url": "", "title": "", "items": [], }) if SEV_RANK[finding["severity"]] < SEV_RANK[group["severity"]]: group["severity"] = finding["severity"] if finding["cvss"] is not None and (group["cvss"] is None or finding["cvss"] > group["cvss"]): group["cvss"] = finding["cvss"] group["packages"].add(finding["package"]) if finding["fixedVersion"]: group["fixed"].add(finding["fixedVersion"]) group["firstSeen"] = min(group["firstSeen"], finding["firstSeenAt"]) group["statuses"].add(finding["status"]) group["url"] = group["url"] or finding["url"] group["title"] = group["title"] or finding["title"] group["items"].append((finding, target)) rows = [] for group in groups.values(): group["status"] = min(group["statuses"], key=STATUS_RANK.get) group["imageCount"] = len({t["id"] for _, t in group["items"]}) group["workloadCount"] = len({(w["namespace"], w["kind"], w["name"]) for _, t in group["items"] for w in t["workloads"]}) group["package"] = ", ".join(sorted(group["packages"])) group["fix"] = ", ".join(sorted(group["fixed"])) rows.append(group) sorts = { "id": lambda r: r["id"], "severity": lambda r: SEV_RANK[r["severity"]], "cvss": lambda r: r["cvss"] if r["cvss"] is not None else -1, "package": lambda r: r["package"], "fixed": lambda r: r["fix"], "firstSeen": lambda r: r["firstSeen"], "status": lambda r: STATUS_RANK[r["status"]], "images": lambda r: r["imageCount"], "workloads": lambda r: r["workloadCount"], } rows.sort(key=lambda r: r["id"]) rows.sort(key=sorts.get(params.get("sort"), sorts["severity"]), reverse=params.get("dir") == "desc") page_rows, total, page, pages, start = _paginate(rows, params) html_rows = [] for row in page_rows: lines = [] fix_images = set() for finding, target in row["items"]: if finding["status"] == "fix-available": fix_images.add(target["image"]) lines.append("
  • " + e(target["id"]) + " — " + _state_label(target) + " — " + _status_label(finding) + " — " + e(finding["package"]) + " " + e(finding["installedVersion"]) + " → " + e(finding["fixedVersion"] or "no fix") + _workload_lines(target, finding) + "
  • ") status_text = _status_summary(f["status"] for f, _ in row["items"]) html_rows.append( "" + _link(row["url"], row["id"]) + "" + _sev(row["severity"]) + "" + e("" if row["cvss"] is None else row["cvss"]) + "" + e(row["package"]) + "" + e(row["fix"]) + "" + e(row["firstSeen"]) + "" + status_text + "" + _remediation_cell(fix_images, remediation) + "
    " + e(row["imageCount"]) + " images, " + e(row["workloadCount"]) + " workloads

    " + e(row["title"]) + "

    ") head = [_th(params, "CVE", "id"), _th(params, "Severity", "severity"), _th(params, "CVSS", "cvss"), _th(params, "Package", "package"), _th(params, "Fixed in", "fixed"), _th(params, "First seen", "firstSeen"), _th(params, "Status", "status"), "Remediation", _th(params, "Affected", "images")] return _pager(params, total, page, pages, start, len(page_rows), "CVEs") \ + _table(head, html_rows, "No CVE rows match these filters.") def _workload_key(item): return item["namespace"], item["kind"], item["name"] def _workloads(report, targets, params): by_target = {} for finding in report["findings"]: target = targets[finding["targetId"]] if _match_finding(finding, target, params): by_target.setdefault(target["id"], []).append(finding) rows = {} finding_filter = params.get("severity") or params.get("status") or params.get("fix") needle = params.get("q", "").lower() for target in report["targets"]: matching = by_target.get(target["id"], []) for workload in target["workloads"]: key = _workload_key(workload) scope = {"namespace": params.get("namespace"), "workload": params.get("workload")} if not _match_workload({"workloads": [workload]}, scope): continue text = " ".join((workload["namespace"], workload["name"], *workload["containers"], target["image"])).lower() if not matching and (finding_filter or (needle and needle not in text)): continue row = rows.setdefault(key, {"key": key, "containers": set(), "images": set(), "findings": {}, "counts": dict.fromkeys(SEVERITIES, 0)}) row["containers"].update(workload["containers"]) row["images"].add(target["id"]) for finding in matching: row["findings"].setdefault((finding["id"], target["id"], finding["package"]), finding) for row in rows.values(): for finding in row["findings"].values(): row["counts"][finding["severity"]] += 1 items = list(rows.values()) sorts = { "namespace": lambda r: r["key"][0], "kind": lambda r: r["key"][1], "name": lambda r: r["key"][2], "images": lambda r: len(r["images"]), "total": lambda r: -len(r["findings"]), } for severity in SEVERITIES: sorts[severity.lower()] = (lambda s: lambda r: -r["counts"][s])(severity) items.sort(key=lambda r: r["key"]) if params.get("sort") in sorts: items.sort(key=sorts[params["sort"]], reverse=params.get("dir") == "desc") else: items.sort(key=lambda r: tuple(-r["counts"][s] for s in SEVERITIES)) page_rows, total, page, pages, start = _paginate(items, params) html_rows = [] for row in page_rows: lines = "".join( "
  • " + _link(f["url"], f["id"]) + " " + _sev(f["severity"]) + " " + e(f["package"]) + " — " + _status_label(f) + " — " + e(f["targetId"]) + " — " + _state_label(targets[f["targetId"]]) + "
  • " for f in sorted(row["findings"].values(), key=lambda f: (SEV_RANK[f["severity"]], f["id"], f["targetId"]))) counts = "".join("" + e(row["counts"][s]) + "" for s in SEVERITIES) html_rows.append( "" + e(row["key"][0]) + "" + e(row["key"][1]) + "" + e(row["key"][2]) + "" + e(", ".join(sorted(row["containers"])) or "none") + "" + "
    ".join(e(i) + " — " + _state_label(targets[i]) for i in sorted(row["images"])) + "" + counts + "
    " + e(len(row["findings"])) + " findings
    ") head = [_th(params, "Namespace", "namespace"), _th(params, "Kind", "kind"), _th(params, "Workload", "name"), "Containers", _th(params, "Images", "images")] + [_th(params, s, s.lower()) for s in SEVERITIES] + [_th(params, "Findings", "total")] return _pager(params, total, page, pages, start, len(page_rows), "workloads") \ + _table(head, html_rows, "No workload rows match these filters.") def _inventory(report, params): counts = {} by_target = {} for finding in report["findings"]: counts[finding["targetId"]] = counts.get(finding["targetId"], 0) + 1 by_target.setdefault(finding["targetId"], []).append(finding) needle = params.get("q", "").lower() wanted_state = params.get("status") rows = [] for target in report["targets"]: if wanted_state and target["state"] != wanted_state: continue if not _match_workload(target, params): continue if needle: text = " ".join((target["id"], target["image"], target["state"], *[w["namespace"] + " " + w["name"] for w in target["workloads"]])).lower() if needle not in text: continue rows.append(target) sorts = { "id": lambda t: t["id"], "type": lambda t: t["type"], "state": lambda t: STATE_RANK[t["state"]], "lastScanned": lambda t: t["lastScannedAt"] or "", "findings": lambda t: counts.get(t["id"], 0), } rows.sort(key=lambda t: t["id"]) rows.sort(key=sorts.get(params.get("sort"), sorts["state"]), reverse=params.get("dir") == "desc") page_rows, total, page, pages, start = _paginate(rows, params) html_rows = [] for target in page_rows: found = sorted(by_target.get(target["id"], []), key=lambda f: (SEV_RANK[f["severity"]], f["id"])) listed = "".join("
  • " + _link(f["url"], f["id"]) + " " + _sev(f["severity"]) + " " + e(f["package"]) + " — " + e(f["status"]) + "
  • " for f in found[:_EXPANSION_LIMIT]) if len(found) > _EXPANSION_LIMIT: listed += '
  • ' + e(len(found) - _EXPANSION_LIMIT) + " more in the CVE view
  • " html_rows.append( "" + e(target["id"]) + "" + e(target["type"]) + "" + e(target["state"]) + "" + e(target["reason"]) + "" + e(target["lastScannedAt"] or "never") + "" + e(counts.get(target["id"], 0)) + "
    " + e(len(target["workloads"])) + " workloads, " + e(len(found)) + " findings" + _workload_lines(target) + ("" if listed else "") + "
    ") head = [_th(params, "Target", "id"), _th(params, "Type", "type"), _th(params, "State", "state"), "Reason", _th(params, "Last scanned", "lastScanned"), _th(params, "Findings", "findings"), "Workloads"] return _pager(params, total, page, pages, start, len(page_rows), "targets") \ + _table(head, html_rows, "No targets match these filters.") # --- header ----------------------------------------------------------------------------------------- def _header(report, now): generated = parse_ts(report["generatedAt"]) database = parse_ts(report["database"]["updatedAt"]) states = dict.fromkeys(report_mod.STATES, 0) for target in report["targets"]: states[target["state"]] += 1 open_counts = dict.fromkeys(SEVERITIES, 0) accepted = 0 for finding in report["findings"]: if finding["status"] == "accepted": accepted += 1 else: open_counts[finding["severity"]] += 1 warnings = [] report_stale = generated is None or now - generated > REPORT_STALE if report_stale: warnings.append("STALE REPORT: generated " + _age(generated, now) + "; the expected refresh is within 26 hours.") if report["run"]["status"] == "degraded": warnings.append("DEGRADED RUN: the last run did not complete normally (database, acceptances file or inventory problem). Do not treat these results as current.") db_epoch = report["database"]["version"] == "unavailable" if db_epoch: warnings.append("DATABASE UNAVAILABLE: no vulnerability database has been downloaded.") elif database is None or now - database > DB_STALE: warnings.append("DATABASE STALE: the vulnerability database is older than 48 hours.") total_targets = len(report["targets"]) not_scanned = total_targets - states["scanned"] if total_targets == 0: warnings.append("NO TARGETS: the inventory is empty; nothing has been evaluated.") if not_scanned: warnings.append("INCOMPLETE COVERAGE: " + str(not_scanned) + " of " + str(total_targets) + " targets are not in the scanned state; findings for them are missing or may be out of date.") open_total = sum(open_counts.values()) if warnings: status = "Status: ATTENTION NEEDED" elif open_total or accepted: status = "Status: CURRENT, findings present" else: status = "Status: CURRENT" tiles = "".join("" + e(name) + ": " + e(states[name]) + "" for name in report_mod.STATES) sev_tiles = "".join("" + _sev(s) + ": " + e(open_counts[s]) + "" for s in SEVERITIES) claim = "" if not warnings and not report["findings"] and total_targets > 0 and not_scanned == 0: claim = "

    Clean: all " + e(total_targets) + " targets were evaluated within 24 hours with zero findings.

    " return ("

    CVE visibility

    " "

    " + e(status) + "

    " + "".join('

    ' + e(w) + "

    " for w in warnings) + claim + "

    Report age: " + e(_age(generated, now)) + " · Database age: " + e("unavailable" if db_epoch else _age(database, now)) + " · Run: " + e(report["run"]["status"]) + "

    " + '

    Targets (' + e(total_targets) + "): " + tiles + "

    " + '

    Open findings by severity: ' + sev_tiles + "accepted: " + e(accepted) + "

    " + "
    ") def _nav(params): links = [] for view, label in (("cves", "CVEs"), ("workloads", "Workloads"), ("inventory", "Inventory")): marker = ' aria-current="page"' if params["view"] == view else "" target = {k: v for k, v in params.items() if k not in ("page", "sort", "dir")} target["view"] = view links.append('" + e(label) + "") return "" def _filters(params): def field(name, label): return "' selected = params.get("fix", "") fix_options = "".join( '" for v, t in (("", "any"), ("yes", "yes"), ("no", "no"))) return ('
    ' + field("q", "Search") + field("severity", "Severity") + field("namespace", "Namespace") + field("workload", "Workload") + field("status", "Status") + '" + '
    ') def _document(body): return ("CVE visibility" '" "" + body + "").encode("utf-8") def render(report, query, now): """(status code, header dict, body bytes) for one page request.""" if report is None: body = ("

    CVE visibility

    NO REPORT YET: nothing has been evaluated. " "Nothing can be said about this cluster until the first report is published.

    ") return 503, dict(SECURITY_HEADERS), _document(body) params = _parse(query) targets = {t["id"]: t for t in report["targets"]} remediation = {r["image"]: r for r in report["remediation"]} view = params["view"] if view == "workloads": content = _workloads(report, targets, params) elif view == "inventory": content = _inventory(report, params) else: content = _cves(report, targets, params, remediation) return 200, dict(SECURITY_HEADERS), _document(_header(report, now) + "
    " + _nav(params) + _filters(params) + content + "
    ") def not_found(): return 404, dict(SECURITY_HEADERS), _document("

    Not found.

    ")