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:
@@ -127,17 +127,22 @@ jobs:
|
||||
--exceptions doc/cve-exceptions.json \
|
||||
--sbom-dir "$OUT_DIR/sbom" \
|
||||
--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" \
|
||||
--warn-only
|
||||
else
|
||||
echo "스캔 리포트 없음 — 게이트 판정 생략"
|
||||
fi
|
||||
|
||||
# Job Summary($GITHUB_STEP_SUMMARY)는 1MB 제한이 있다 — 전체 리포트(cve-gate.md)를
|
||||
# 그대로 넣으면 카탈로그 전체 스캔에서 실제로 잘렸다(45+ 차트, 2.2MB 실측).
|
||||
# 건수 표만 담은 cve-gate-brief.md 를 쓰고, CVE 상세는 아티팩트를 보라고 안내한다.
|
||||
- name: Job Summary
|
||||
if: always()
|
||||
run: |
|
||||
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/ 하위.
|
||||
# (절대경로로 업로드하면 v4 가 전체 경로트리를 보존하므로, 스테이징 후 상대경로로 업로드)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user