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
+6 -1
View File
@@ -127,17 +127,22 @@ jobs:
--exceptions doc/cve-exceptions.json \ --exceptions doc/cve-exceptions.json \
--sbom-dir "$OUT_DIR/sbom" \ --sbom-dir "$OUT_DIR/sbom" \
--summary-md "$OUT_DIR/cve-gate.md" \ --summary-md "$OUT_DIR/cve-gate.md" \
--brief-md "$OUT_DIR/cve-gate-brief.md" \
--artifact-name sbom-and-vuln-report \
--json-out "$OUT_DIR/cve-gate.json" \ --json-out "$OUT_DIR/cve-gate.json" \
--warn-only --warn-only
else else
echo "스캔 리포트 없음 — 게이트 판정 생략" echo "스캔 리포트 없음 — 게이트 판정 생략"
fi fi
# Job Summary($GITHUB_STEP_SUMMARY)는 1MB 제한이 있다 — 전체 리포트(cve-gate.md)를
# 그대로 넣으면 카탈로그 전체 스캔에서 실제로 잘렸다(45+ 차트, 2.2MB 실측).
# 건수 표만 담은 cve-gate-brief.md 를 쓰고, CVE 상세는 아티팩트를 보라고 안내한다.
- name: Job Summary - name: Job Summary
if: always() if: always()
run: | run: |
if [ -f "$OUT_DIR/trivy-summary.md" ]; then cat "$OUT_DIR/trivy-summary.md" >> "$GITHUB_STEP_SUMMARY"; fi if [ -f "$OUT_DIR/trivy-summary.md" ]; then cat "$OUT_DIR/trivy-summary.md" >> "$GITHUB_STEP_SUMMARY"; fi
if [ -f "$OUT_DIR/cve-gate.md" ]; then cat "$OUT_DIR/cve-gate.md" >> "$GITHUB_STEP_SUMMARY"; fi if [ -f "$OUT_DIR/cve-gate-brief.md" ]; then cat "$OUT_DIR/cve-gate-brief.md" >> "$GITHUB_STEP_SUMMARY"; fi
# 아티팩트 레이아웃 정리: 요약·인덱스는 최상위, 이미지별 상세 SBOM 은 sbom/ 하위. # 아티팩트 레이아웃 정리: 요약·인덱스는 최상위, 이미지별 상세 SBOM 은 sbom/ 하위.
# (절대경로로 업로드하면 v4 가 전체 경로트리를 보존하므로, 스테이징 후 상대경로로 업로드) # (절대경로로 업로드하면 v4 가 전체 경로트리를 보존하므로, 스테이징 후 상대경로로 업로드)
+72 -10
View File
@@ -383,17 +383,18 @@ def evaluate(img, exceptions):
} }
def render_md(results, expired_exceptions, missing=None): def gate_verdict(results, missing):
L = []
A = L.append
missing = missing or []
total_block = sum(len(r["blocking"]) for r in results) total_block = sum(len(r["blocking"]) for r in results)
nodata = [r for r in results if r["no_data"]] nodata = [r for r in results if r["no_data"]]
verdict = "PASS" if (total_block == 0 and not nodata and not missing) else "FAIL" return "PASS" if (total_block == 0 and not nodata and not missing) else "FAIL"
A(f"## 🎯 CVE 게이트: {verdict}")
A("")
A("목표: 카탈로그 제공 이미지의 CRITICAL/HIGH **0건** (고유 CVE 기준, 벤더·NVD 중 높은 등급 적용)") def render_summary_table(results):
A("") """차트·이미지별 취약점 '건수' 요약 표. 전체 리포트와 Job Summary 양쪽에서 재사용한다 —
이 표만으로는 CVE 개별 상세(ID·패키지·상태)를 담지 않으므로 크기가 카탈로그 규모에
선형으로만 늘어난다(차단 항목 상세 표와 달리 이미지당 CVE 수에 비례해 불어나지 않음)."""
L = []
A = L.append
A("| Chart | Image | OS | 커버리지 | EOSL | 벤더 C/H | NVD C/H | 실효 C/H | 차단 | 예외 |") A("| Chart | Image | OS | 커버리지 | EOSL | 벤더 C/H | NVD C/H | 실효 C/H | 차단 | 예외 |")
A("|---|---|---|---|---|---:|---:|---:|---:|---:|") A("|---|---|---|---|---|---:|---:|---:|---:|---:|")
PROBE_LABEL = {"ok": "✅ ok", "none": "❌ none", "n/a": " n/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"**{c['effective']['CRITICAL']}/{c['effective']['HIGH']}** | "
f"{len(r['blocking'])} | {len(r['excepted'])} |" 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("")
A("> **커버리지** 열은 스캐너가 그 배포판을 아는지 직접 물어본 결과다 " A("> **커버리지** 열은 스캐너가 그 배포판을 아는지 직접 물어본 결과다 "
"(SBOM 사본에 취약한 센티널 패키지를 주입해 재스캔하는 양성 대조). " "(SBOM 사본에 취약한 센티널 패키지를 주입해 재스캔하는 양성 대조). "
@@ -520,7 +567,19 @@ def main():
ap.add_argument("--reports", required=True, help="trivy-reports 디렉토리 (전 심각도 스캔 결과)") ap.add_argument("--reports", required=True, help="trivy-reports 디렉토리 (전 심각도 스캔 결과)")
ap.add_argument("--index", default="", help="sbom-index.tsv (리포트→이미지 매핑)") ap.add_argument("--index", default="", help="sbom-index.tsv (리포트→이미지 매핑)")
ap.add_argument("--exceptions", default="", help="승인 예외 JSON") 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("--json-out", default="", help="판정 결과 JSON 출력 경로")
ap.add_argument("--warn-only", action="store_true", help="실패해도 종료 코드 0") ap.add_argument("--warn-only", action="store_true", help="실패해도 종료 코드 0")
ap.add_argument( ap.add_argument(
@@ -597,6 +656,9 @@ def main():
if args.summary_md: if args.summary_md:
with open(args.summary_md, "w") as f: with open(args.summary_md, "w") as f:
f.write(md + "\n") 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: if args.json_out:
with open(args.json_out, "w") as f: 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)) json.dump(results, f, indent=2, default=lambda o: sorted(o) if isinstance(o, set) else str(o))