cve-gate.py: Job Summary용 축약 리포트(--brief-md) 추가 — 1MB 제한 대응

전체 카탈로그(45+ 차트) 스캔을 실제로 CI에서 돌려보니 GITHUB_STEP_SUMMARY 가
1MB 제한에 걸려 cve-gate.md(2.2MB) 업로드가 중단됐다 — CVE 개별 상세(차단
항목·벤더 하향 등급 등)가 이미지당 CVE 수에 비례해 불어나는 게 원인이다.

render_summary_table()로 차트·이미지별 건수 표를 뽑아 render_md(전체)와
render_brief_md(신규, Job Summary용) 양쪽에서 재사용한다. brief 는 건수 표 +
누락/커버리지 이상 건수만 담고, CVE 개별 상세는 아티팩트(cve-gate.md/json)를
보라고 안내한다 — 이미지 수에는 선형으로 늘지만 이미지당 CVE 수에는 무관해
카탈로그가 커져도 1MB 제한에 걸리지 않는다.

sbom.yml 의 Job Summary 스텝을 --brief-md 출력으로 교체했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-03 16:46:01 +09:00
parent 6bdea97e51
commit aadbf866fb
2 changed files with 78 additions and 11 deletions
+72 -10
View File
@@ -383,17 +383,18 @@ def evaluate(img, exceptions):
}
def render_md(results, expired_exceptions, missing=None):
L = []
A = L.append
missing = missing or []
def gate_verdict(results, missing):
total_block = sum(len(r["blocking"]) for r in results)
nodata = [r for r in results if r["no_data"]]
verdict = "PASS" if (total_block == 0 and not nodata and not missing) else "FAIL"
A(f"## 🎯 CVE 게이트: {verdict}")
A("")
A("목표: 카탈로그 제공 이미지의 CRITICAL/HIGH **0건** (고유 CVE 기준, 벤더·NVD 중 높은 등급 적용)")
A("")
return "PASS" if (total_block == 0 and not nodata and not missing) else "FAIL"
def render_summary_table(results):
"""차트·이미지별 취약점 '건수' 요약 표. 전체 리포트와 Job Summary 양쪽에서 재사용한다 —
이 표만으로는 CVE 개별 상세(ID·패키지·상태)를 담지 않으므로 크기가 카탈로그 규모에
선형으로만 늘어난다(차단 항목 상세 표와 달리 이미지당 CVE 수에 비례해 불어나지 않음)."""
L = []
A = L.append
A("| Chart | Image | OS | 커버리지 | EOSL | 벤더 C/H | NVD C/H | 실효 C/H | 차단 | 예외 |")
A("|---|---|---|---|---|---:|---:|---:|---:|---:|")
PROBE_LABEL = {"ok": "✅ ok", "none": "❌ none", "n/a": " n/a"}
@@ -408,6 +409,52 @@ def render_md(results, expired_exceptions, missing=None):
f"**{c['effective']['CRITICAL']}/{c['effective']['HIGH']}** | "
f"{len(r['blocking'])} | {len(r['excepted'])} |"
)
return L
def render_brief_md(results, missing=None, artifact_name="sbom-and-vuln-report"):
"""GitHub Actions Job Summary(`$GITHUB_STEP_SUMMARY`)용 — 건수 표까지만 담고 CVE
개별 상세는 첨부 파일을 보라고 안내한다.
전체 리포트(`render_md`)를 그대로 Job Summary 에 넣으면 카탈로그 전체 스캔에서
실제로 깨졌다 — 45+ 차트 전체 스캔 시 `cve-gate.md` 가 2.2MB 로 GitHub 의
Job Summary 1MB 제한을 넘어 업로드 자체가 중단됐다(실측). 건수 표는 이미지 수에
선형으로만 늘어나 이 한계에 걸리지 않는다.
"""
missing = missing or []
L = []
A = L.append
verdict = gate_verdict(results, missing)
A(f"## 🎯 CVE 게이트: {verdict}")
A("")
A("목표: 카탈로그 제공 이미지의 CRITICAL/HIGH **0건** (고유 CVE 기준, 벤더·NVD 중 높은 등급 적용)")
A("")
L.extend(render_summary_table(results))
A("")
if missing:
A(f"❌ **스캔되지 않은 이미지 {len(missing)}건** — 인벤토리에 있으나 리포트가 없어 판정하지 못했다.")
nodata_n = sum(1 for r in results if r["no_data"])
if nodata_n:
A(f"❌ **데이터 커버리지 이상 {nodata_n}건** — findings 0건이 실제 0건인지 측정 불가인지 구분 안 됨.")
A("")
A(
f"CVE 개별 상세(차단 항목·벤더 하향 등급·사각지대 후보)는 이 표에 없다 — "
f"아티팩트 **`{artifact_name}`** 의 **`cve-gate.md`**(마크다운 전체 리포트) 또는 "
f"**`cve-gate.json`**(구조화 데이터)을 내려받아 확인한다."
)
return "\n".join(L)
def render_md(results, expired_exceptions, missing=None):
L = []
A = L.append
missing = missing or []
verdict = gate_verdict(results, missing)
A(f"## 🎯 CVE 게이트: {verdict}")
A("")
A("목표: 카탈로그 제공 이미지의 CRITICAL/HIGH **0건** (고유 CVE 기준, 벤더·NVD 중 높은 등급 적용)")
A("")
L.extend(render_summary_table(results))
A("")
A("> **커버리지** 열은 스캐너가 그 배포판을 아는지 직접 물어본 결과다 "
"(SBOM 사본에 취약한 센티널 패키지를 주입해 재스캔하는 양성 대조). "
@@ -520,7 +567,19 @@ def main():
ap.add_argument("--reports", required=True, help="trivy-reports 디렉토리 (전 심각도 스캔 결과)")
ap.add_argument("--index", default="", help="sbom-index.tsv (리포트→이미지 매핑)")
ap.add_argument("--exceptions", default="", help="승인 예외 JSON")
ap.add_argument("--summary-md", default="", help="마크다운 요약 출력 경로")
ap.add_argument("--summary-md", default="", help="마크다운 요약 출력 경로 (CVE 상세 전부 포함)")
ap.add_argument(
"--brief-md",
default="",
help="건수 표만 담은 축약 마크다운 출력 경로. GitHub Actions Job Summary 처럼 "
"크기 제한이 있는 곳에 쓴다 — --summary-md 전체 리포트는 카탈로그 규모가 커지면 "
"GitHub 의 Job Summary 1MB 제한을 넘을 수 있다(실측: 45+ 차트 전체 스캔 시 2.2MB)",
)
ap.add_argument(
"--artifact-name",
default="sbom-and-vuln-report",
help="--brief-md 안내문에 적을 아티팩트 이름 (CVE 상세는 여기서 --summary-md/--json-out 파일을 찾으라고 안내)",
)
ap.add_argument("--json-out", default="", help="판정 결과 JSON 출력 경로")
ap.add_argument("--warn-only", action="store_true", help="실패해도 종료 코드 0")
ap.add_argument(
@@ -597,6 +656,9 @@ def main():
if args.summary_md:
with open(args.summary_md, "w") as f:
f.write(md + "\n")
if args.brief_md:
with open(args.brief_md, "w") as f:
f.write(render_brief_md(results, missing, args.artifact_name) + "\n")
if args.json_out:
with open(args.json_out, "w") as f:
json.dump(results, f, indent=2, default=lambda o: sorted(o) if isinstance(o, set) else str(o))