diff --git a/scripts/pipeline/cve-gate.py b/scripts/pipeline/cve-gate.py index 4a50fd6..47daa0f 100755 --- a/scripts/pipeline/cve-gate.py +++ b/scripts/pipeline/cve-gate.py @@ -437,6 +437,17 @@ def version_sort_key(version): return (core_parts, 0 if pre_parts else 1, pre_parts) +SUMMARY_LEGEND = ( + "> **실효 C/H** 는 `max(벤더 등급, NVD 등급)` 으로 집계한 고유 CVE 수이며 " + "**게이트 판정의 유일한 기준**이다. 벤더 등급과 NVD 등급을 따로 세던 열은 뺐다 — " + "비슷하게 생긴 숫자 셋이 나란히 놓여 어느 것이 판정값인지 헷갈렸다. " + "대신 **하향** 열이 벤더가 NVD 보다 낮게 매긴 CVE 수를 알려준다. 이 값이 있으면 " + "\"벤더 기준으로는 안전해 보이지만 실제 위험은 NVD 등급에 가까운\" 항목이 있다는 뜻이고, " + "베이스 OS 교체로 수치만 낮아진 것이 아닌지 확인해야 한다 " + "(해당 CVE 목록은 `cve-gate.md` 의 `⚠️ 벤더 하향 등급` 절)." +) + + def render_summary_table(results, charts_of=None): """차트·이미지별 취약점 '건수' 요약 표. 전체 리포트와 Job Summary 양쪽에서 재사용한다 — 이 표만으로는 CVE 개별 상세(ID·패키지·상태)를 담지 않으므로 크기가 카탈로그 규모에 @@ -469,19 +480,19 @@ def render_summary_table(results, charts_of=None): L = [] A = L.append - A("| Chart | Image | OS | 커버리지 | EOSL | 벤더 C/H | NVD C/H | 실효 C/H | 차단 | 예외 |") - A("|---|---|---|---|---|---:|---:|---:|---:|---:|") + A("| Chart | Image | OS | 커버리지 | EOSL | 실효 C/H | 차단 | 예외 | 하향 |") + A("|---|---|---|---|---|---:|---:|---:|---:|") PROBE_LABEL = {"ok": "✅ ok", "none": "❌ none", "n/a": "– n/a"} for chart, version, r in expanded: c = r["counts"] src = PROBE_LABEL.get(r.get("coverage_probe"), "? 미측정") + under = len(r["underrated"]) A( f"| {chart}@{version} | `{r['image']}` | {r['os']} | {src} | " f"{'⚠️ EOL' if r['eosl'] else '-'} | " - f"{c['vendor']['CRITICAL']}/{c['vendor']['HIGH']} | " - f"{c['nvd']['CRITICAL']}/{c['nvd']['HIGH']} | " f"**{c['effective']['CRITICAL']}/{c['effective']['HIGH']}** | " - f"{len(r['blocking'])} | {len(r['excepted'])} |" + f"{len(r['blocking'])} | {len(r['excepted'])} | " + f"{('⚠️ ' + str(under)) if under else '-'} |" ) return L @@ -505,6 +516,8 @@ def render_brief_md(results, missing=None, artifact_name="sbom-and-vuln-report", A("") L.extend(render_summary_table(results, charts_of)) A("") + A(SUMMARY_LEGEND) + A("") if missing: A(f"❌ **스캔되지 않은 이미지 {len(missing)}건** — 인벤토리에 있으나 리포트가 없어 판정하지 못했다.") nodata_n = sum(1 for r in results if r["no_data"]) @@ -530,6 +543,8 @@ def render_md(results, expired_exceptions, missing=None, charts_of=None): A("") L.extend(render_summary_table(results, charts_of)) A("") + A(SUMMARY_LEGEND) + A("") A("> **커버리지** 열은 스캐너가 그 배포판을 아는지 직접 물어본 결과다 " "(SBOM 사본에 취약한 센티널 패키지를 주입해 재스캔하는 양성 대조). " "`ok` = 데이터 있음이므로 0건은 진짜 0건 / `none` = 데이터 없음이므로 **차단** / "