scan-sbom.sh: CoverageProbe 커버리지 자가진단 이식 + 관련 파이프라인 버그 수정
security-catalog 프로젝트에서 자체 빌드 이미지 3종을 실제로 로컬 빌드·게이트 검증하는 과정에서 발견한 문제들: - extract-helm-images.sh 가 `image:` 필드만 잡고 `imageName:`(CNPG Cluster CRD 관례) 은 놓쳐, cnpg-cluster 차트의 이미지가 SBOM·스캔·게이트 어디에도 안 나타났다. - patch-catalog-tag.py 의 split 포맷 패처가 registry 필드가 따로 없고 repository 에 registry+repo 를 합쳐 쓰는 차트(cloudnative-pg 오퍼레이터, 업스트림 템플릿이 image.registry 를 아예 참조하지 않음)에서 tag 만 조용히 갱신하고 repository 는 그대로 남겨 깨진 참조를 만들 수 있었다. - CoverageProbe(센티널 패키지 주입 재스캔으로 "0건"과 "측정 안 됨"을 구분)가 이식되지 않아, 전 심각도 0건인 자체 빌드 이미지가 실제로는 깨끗한데도 게이트가 "데이터 커버리지 이상"으로 오탐 처리했다 — security-catalog 의 scan-sbom.sh 를 이식해 해소. cve-gate.py 는 이미 이 키를 읽도록 구현돼 있어 소비 쪽 변경은 없다. rpm(SUSE)·deb(Debian)·apk(Alpine) 세 센티널 경로와 병렬 스캔 회귀를 로컬에서 확인했고, 이식 후 cloudnative-pg·cnpg-postgresql 게이트가 실제로 FAIL→PASS 로 바뀌는 것도 확인했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,12 @@
|
||||
# SBOM 을 입력으로 쓰므로 **이미지 재pull·레지스트리 인증이 불필요**(오프라인).
|
||||
# 취약점 DB 만 있으면 되며, CVE DB 갱신 시 재pull 없이 재스캔할 수 있다.
|
||||
#
|
||||
# 스캔과 함께 **커버리지 자가진단**을 수행한다. OS 패키지 findings 가 0건인 이미지에
|
||||
# 대해 SBOM 사본에 센티널 패키지를 주입해 재스캔하고, 발화하면 스캐너가 그 배포판을
|
||||
# 아는 것으로 본다. 결과를 리포트의 `CoverageProbe`(ok|none|n/a) 에 기록하며 게이트
|
||||
# (`cve-gate.py`)가 이를 읽는다 — "0건" 과 "측정되지 않음" 을 구분하는 유일한 수단이다
|
||||
# (security-catalog 프로젝트에서 포팅, doc/sbom-pipeline.md 참고).
|
||||
#
|
||||
# 사용:
|
||||
# bash scripts/pipeline/generate-sbom.sh images_final.tsv . # 선행: sbom/ 생성
|
||||
# bash scripts/pipeline/scan-sbom.sh . # SBOM 취약점 스캔
|
||||
@@ -20,7 +26,7 @@
|
||||
# TRIVY_CACHE_DIR trivy 캐시 디렉토리 (미설정 시 trivy 기본 ~/.cache/trivy)
|
||||
#
|
||||
# 산출물 ($1 기준):
|
||||
# trivy-reports/<img>.json 이미지별 원본 스캔 결과
|
||||
# trivy-reports/<img>.json 이미지별 원본 스캔 결과 (+ CoverageProbe)
|
||||
# trivy-summary.tsv chart⇥image⇥CRIT⇥HIGH⇥MED⇥LOW⇥status⇥seconds
|
||||
# trivy-summary.md 마크다운 요약
|
||||
# trivy-run.log 실행 로그
|
||||
@@ -38,6 +44,92 @@ PARALLEL="${PARALLEL:-4}"
|
||||
|
||||
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
# --- 내부 모드: 커버리지 자가진단 --------------------------------------------
|
||||
# 인자: --probe <sbom_file> <report_json> <probe_dir>
|
||||
#
|
||||
# 리포트 최상위에 CoverageProbe 를 기록한다. 게이트가 이 값으로 커버리지를 판정한다.
|
||||
# ok 스캐너가 이 배포판을 안다 (0건은 진짜 0건)
|
||||
# none 스캐너에 이 배포판 데이터가 없다 → 게이트 실패
|
||||
# n/a SBOM 에 OS 패키지가 없다 (판정 대상 아님)
|
||||
if [ "${1:-}" = "--probe" ]; then
|
||||
python3 - "$2" "$3" "$4" <<'PY'
|
||||
import json, os, re, subprocess, sys, tempfile
|
||||
|
||||
sbom_path, report_path, probe_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
OS_PURL = re.compile(r"^pkg:(deb|rpm|apk)/([^/]+)/")
|
||||
FLOOR = "0.0.1-1" # 어떤 실제 버전보다도 낮다
|
||||
|
||||
# 센티널 — 그 배포판이라면 권고가 반드시 존재하는 패키지 이름. 여러 개를 넣고
|
||||
# **하나라도 발화하면** 데이터가 있는 것으로 본다.
|
||||
#
|
||||
# 설치된 패키지를 낮추는 방식은 쓰지 않는다. security-catalog 프로젝트에서 실측으로
|
||||
# 깨진 적이 있다 — cloudnative-pg:1.30.0 의 OS 패키지는 base-files·media-types·netbase·
|
||||
# tzdata 4개뿐이고 넷 다 Debian 권고가 존재한 적이 없어, 버전을 낮춰도 0건이라 프로브가
|
||||
# "데이터 없음" 이라 오판했다. 설치 목록에 의존하지 않는 센티널이라야 한다.
|
||||
SENTINELS = {
|
||||
"deb": ["openssl", "curl", "libxml2", "perl", "zlib1g", "libc6"],
|
||||
"rpm": ["openssl", "openssl-3", "curl", "libxml2-2", "glibc", "zlib"],
|
||||
"apk": ["openssl", "curl", "busybox", "zlib", "musl"],
|
||||
}
|
||||
|
||||
|
||||
def write(verdict):
|
||||
d = json.load(open(report_path))
|
||||
d["CoverageProbe"] = verdict
|
||||
json.dump(d, open(report_path, "w"))
|
||||
print(f"probe: {verdict}", file=sys.stderr)
|
||||
|
||||
|
||||
sbom = json.load(open(sbom_path))
|
||||
sample = next((c for c in (sbom.get("components") or [])
|
||||
if OS_PURL.match(c.get("purl") or "")), None)
|
||||
if sample is None:
|
||||
write("n/a"); sys.exit(0)
|
||||
|
||||
# 이미 findings 가 있으면 스캐너가 이 배포판을 아는 것이 자명하다 — 추가 스캔 불필요.
|
||||
rep = json.load(open(report_path))
|
||||
if any((r.get("Vulnerabilities") or []) for r in (rep.get("Results") or [])
|
||||
if r.get("Class") == "os-pkgs"):
|
||||
write("ok"); sys.exit(0)
|
||||
|
||||
# 사본에 센티널을 주입한다. **원본은 건드리지 않는다.**
|
||||
# 배포판·아키텍처 한정자(distro=, arch=)는 설치 패키지에서 그대로 베껴 와야 한다 —
|
||||
# trivy 는 purl 의 distro 로 어느 배포판 DB 를 볼지 정한다.
|
||||
ptype, namespace = OS_PURL.match(sample["purl"]).groups()
|
||||
qs = sample["purl"].partition("?")[2]
|
||||
comps = sbom.setdefault("components", [])
|
||||
for i, name in enumerate(SENTINELS[ptype]):
|
||||
comps.append({
|
||||
"bom-ref": f"coverage-probe-{i}", "type": "library",
|
||||
"name": name, "version": FLOOR,
|
||||
"purl": f"pkg:{ptype}/{namespace}/{name}@{FLOOR}" + (f"?{qs}" if qs else ""),
|
||||
})
|
||||
|
||||
os.makedirs(probe_dir, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=probe_dir, suffix=".cdx.json")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(sbom, f)
|
||||
p = subprocess.run(
|
||||
["trivy", "sbom", "--quiet", "--scanners", "vuln",
|
||||
"--severity", "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
|
||||
"--cache-backend", "memory", "--skip-db-update", "--format", "json", tmp],
|
||||
capture_output=True, text=True)
|
||||
if p.returncode != 0:
|
||||
# 프로브가 실패하면 판정을 지어내지 않는다. 키를 남기지 않으면 게이트가
|
||||
# 구버전 리포트와 동일하게(총계 0건 → 실패) 보수적으로 처리한다.
|
||||
print("probe: 실패 — " + (p.stderr or "").strip()[:200], file=sys.stderr)
|
||||
sys.exit(0)
|
||||
got = sum(len(r.get("Vulnerabilities") or [])
|
||||
for r in (json.loads(p.stdout).get("Results") or [])
|
||||
if r.get("Class") == "os-pkgs")
|
||||
write("ok" if got > 0 else "none")
|
||||
finally:
|
||||
os.unlink(tmp)
|
||||
PY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 내부 모드: SBOM 1개 스캔 (xargs 병렬 워커) ------------------------------
|
||||
# 인자: --scan-one <image> <sbom_file> <reports_dir> <log>
|
||||
if [ "${1:-}" = "--scan-one" ]; then
|
||||
@@ -48,6 +140,13 @@ if [ "${1:-}" = "--scan-one" ]; then
|
||||
if trivy sbom --quiet --scanners vuln --severity "$SEVERITY" \
|
||||
--cache-backend memory --skip-db-update \
|
||||
--format json "$sbomfile" > "$json" 2>"$json.err"; then
|
||||
# --- 커버리지 자가진단 --------------------------------------------------
|
||||
# os-pkgs findings 가 0건이면 두 가지 가능성이 있고 숫자로는 구분되지 않는다.
|
||||
# (a) 이미지가 실제로 깨끗하다 (b) 스캐너에 그 배포판 데이터가 없다
|
||||
# 스캐너에게 직접 물어본다 — SBOM 사본에 취약한 센티널 패키지를 주입해 재스캔하고,
|
||||
# 발화하면 (a), 여전히 0건이면 (b) 다.
|
||||
probe_dir="$(dirname "$json")/../.probe"
|
||||
bash "${BASH_SOURCE[0]}" --probe "$sbomfile" "$json" "$probe_dir" 2>>"$json.err" || true
|
||||
dur=$(( $(date +%s) - t0 ))
|
||||
line="$(python3 - "$img" "$json" "$dur" <<'PY'
|
||||
import sys,json,collections
|
||||
@@ -56,9 +155,10 @@ try:
|
||||
d=json.load(open(path)); c=collections.Counter()
|
||||
for r in d.get("Results",[]):
|
||||
for v in (r.get("Vulnerabilities") or []): c[v["Severity"]]+=1
|
||||
print(f"{img}\t{c['CRITICAL']}\t{c['HIGH']}\t{c['MEDIUM']}\t{c['LOW']}\tOK\t{dur}")
|
||||
# 8번째 열 = 커버리지 자가진단 결과. 요약 생성기는 7열까지만 읽으므로 무해하다.
|
||||
print(f"{img}\t{c['CRITICAL']}\t{c['HIGH']}\t{c['MEDIUM']}\t{c['LOW']}\tOK\t{dur}\t{d.get('CoverageProbe','?')}")
|
||||
except Exception as e:
|
||||
print(f"{img}\t-\t-\t-\t-\tPARSE_ERR:{str(e)[:40]}\t{dur}")
|
||||
print(f"{img}\t-\t-\t-\t-\tPARSE_ERR:{str(e)[:40]}\t{dur}\t?")
|
||||
PY
|
||||
)"
|
||||
else
|
||||
@@ -66,11 +166,12 @@ PY
|
||||
reason="$(tr '\n' ' ' < "$json.err" | grep -oiE '(unable|invalid|unsupported|not found|failed to)[^.]*' | head -1)"
|
||||
[ -z "$reason" ] && reason="scan failed"
|
||||
rm -f "$json"
|
||||
line="$(printf '%s\t-\t-\t-\t-\tERROR:%s\t%s' "$img" "${reason:0:40}" "$dur")"
|
||||
line="$(printf '%s\t-\t-\t-\t-\tERROR:%s\t%s\t-' "$img" "${reason:0:40}" "$dur")"
|
||||
fi
|
||||
printf '%s\n' "$line"
|
||||
st="$(printf '%s' "$line" | cut -f6)"
|
||||
printf '%s %-6s %5ss %s\n' "$(ts)" "${st%%:*}" "$dur" "$img" >> "$log"
|
||||
cov="$(printf '%s' "$line" | cut -f8)"
|
||||
printf '%s %-6s %5ss cov=%-4s %s\n' "$(ts)" "${st%%:*}" "$dur" "$cov" "$img" >> "$log"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -215,3 +316,4 @@ PY
|
||||
echo "==================================================================="
|
||||
} | tee -a "$LOG" >&2
|
||||
rm -f "$RESULTS" "$CHARTMAP" "$TARGETS"
|
||||
rm -rf "$OUT_DIR/.probe" # 자가진단용 합성 SBOM 사본 — 산출물이 아니다
|
||||
|
||||
Reference in New Issue
Block a user