scripts/pipeline/ 신설: 기존 SBOM 스캔 스크립트 경로 이동
doc/scripts/ -> scripts/pipeline/ (security-catalog 의 최상위 scripts/ 관례 채택). 스크립트 로직은 변경 없음 (상대경로 깊이 동일).
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# =============================================================================
|
||||
# SBOM 파이프라인 실행 이미지
|
||||
#
|
||||
# .github/workflows/sbom.yml 의 `container:` (Repo Variable SBOM_PIPELINE_IMAGE)
|
||||
# 로 사용되는 이미지. doc/scripts/*.sh 를 컨테이너 내부에서 직접 실행한다.
|
||||
# 상세: doc/sbom-pipeline.md
|
||||
#
|
||||
# 도구: helm(v3) + trivy + python3 + bash + git
|
||||
# 베이스: debian(glibc) — GitHub Actions container 안에서 node 기반 액션
|
||||
# (actions/checkout, upload-artifact)이 동작하려면 glibc 필요.
|
||||
# (alpine/musl 은 node 실행 실패 가능 → debian 사용)
|
||||
#
|
||||
# 빌드 & 푸시 (amd64 필수 — GitHub 러너가 amd64):
|
||||
# docker buildx build --platform linux/amd64 \
|
||||
# -t docker.io/<org>/sbom-pipeline:latest -f doc/scripts/Dockerfile --push doc/scripts
|
||||
# # 이후: gh variable set SBOM_PIPELINE_IMAGE --body docker.io/<org>/sbom-pipeline:latest
|
||||
# =============================================================================
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates git python3 bash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# helm v3
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
# trivy (최신)
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||
| sh -s -- -b /usr/local/bin
|
||||
|
||||
# 설치 확인 (빌드 시 도구 누락 조기 감지)
|
||||
RUN helm version --short && trivy --version | head -1 && python3 --version && git --version
|
||||
|
||||
ENTRYPOINT []
|
||||
CMD ["bash"]
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# extract-helm-images.sh
|
||||
# dip-catalog 정적 Helm 카탈로그에서 컨테이너 이미지 인벤토리를 추출한다.
|
||||
#
|
||||
# 방법 (100% deterministic, LLM 미사용):
|
||||
# 각 차트 버전 디렉토리를 `helm template` 로 렌더 → 렌더된 매니페스트에서
|
||||
# `image:` 필드를 추출 → 정규화/중복 제거.
|
||||
#
|
||||
# custom-values.yaml 이 있으면 적용하여 "실제 배포되는(effective) 이미지"를
|
||||
# 뽑는다. 없으면 차트 기본값으로 렌더한다.
|
||||
#
|
||||
# 사용:
|
||||
# bash doc/scripts/extract-helm-images.sh [HELM_ROOT] [OUT_DIR]
|
||||
# HELM_ROOT 기본값: <repo>/manifests/helm
|
||||
# OUT_DIR 기본값: 현재 디렉토리
|
||||
#
|
||||
# 산출물:
|
||||
# images_final.tsv chart<TAB>version<TAB>image (정렬/중복제거)
|
||||
# render_status.tsv chart<TAB>version<TAB>mode<TAB>status
|
||||
#
|
||||
# 요구사항: helm v3, bash, awk, sed, grep
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HELM_ROOT="${1:-$(cd "$SCRIPT_DIR/../../manifests/helm" && pwd)}"
|
||||
OUT_DIR="${2:-$(pwd)}"
|
||||
|
||||
# bitnamilegacy 이미지가 있는 차트는 Bitnami common 의 insecureImages 가드에
|
||||
# 걸리므로 렌더 시 허용 플래그가 필요하다.
|
||||
KUBE_VERSION="${KUBE_VERSION:-1.31.0}"
|
||||
COMMON_SET=(--set global.security.allowInsecureImages=true)
|
||||
|
||||
RAW="$OUT_DIR/images_raw.tsv"
|
||||
FINAL="$OUT_DIR/images_final.tsv"
|
||||
STATUS="$OUT_DIR/render_status.tsv"
|
||||
: > "$RAW"; : > "$STATUS"
|
||||
|
||||
cd "$HELM_ROOT"
|
||||
|
||||
extract_images() { # stdin: 렌더된 yaml → stdout: 이미지 문자열
|
||||
grep -ioE '^[[:space:]]*(- )?image:[[:space:]]*"?[^"[:space:]]+' \
|
||||
| sed -E 's/.*image:[[:space:]]*"?//' \
|
||||
| grep -vE '^$'
|
||||
}
|
||||
|
||||
for chart in */; do
|
||||
chart="${chart%/}"
|
||||
for vdir in "$chart"/*/; do
|
||||
[ -f "${vdir}Chart.yaml" ] || continue
|
||||
version="$(basename "$vdir")"
|
||||
|
||||
args=(template t "$vdir" --kube-version "$KUBE_VERSION" "${COMMON_SET[@]}")
|
||||
mode="default"
|
||||
if [ -f "${vdir}custom-values.yaml" ]; then
|
||||
args+=(-f "${vdir}custom-values.yaml")
|
||||
mode="effective"
|
||||
fi
|
||||
|
||||
if out="$(helm "${args[@]}" 2>/tmp/_helmerr)"; then
|
||||
echo -e "${chart}\t${version}\t${mode}\tOK" >> "$STATUS"
|
||||
elif [ "$mode" = "effective" ] && out="$(helm template t "$vdir" --kube-version "$KUBE_VERSION" "${COMMON_SET[@]}" 2>/tmp/_helmerr)"; then
|
||||
echo -e "${chart}\t${version}\tdefault-fallback\tOK" >> "$STATUS"
|
||||
else
|
||||
echo -e "${chart}\t${version}\t${mode}\tFAIL: $(head -1 /tmp/_helmerr | cut -c1-120)" >> "$STATUS"
|
||||
out=""
|
||||
fi
|
||||
|
||||
[ -n "$out" ] || continue
|
||||
while read -r img; do
|
||||
[ -z "$img" ] && continue
|
||||
echo -e "${chart}\t${version}\t${img}" >> "$RAW"
|
||||
done < <(printf '%s\n' "$out" | extract_images)
|
||||
done
|
||||
done
|
||||
|
||||
# 정규화(따옴표 제거) + 정렬 + 중복 제거
|
||||
awk -F'\t' '{gsub(/^['"'"'"]+|['"'"'"]+$/,"",$3); if($3!="") print $1"\t"$2"\t"$3}' "$RAW" \
|
||||
| sort -u > "$FINAL"
|
||||
|
||||
echo "== 렌더 상태 =="
|
||||
awk -F'\t' '{s=$4; sub(/FAIL.*/,"FAIL",s); print s}' "$STATUS" | sort | uniq -c
|
||||
echo "== 커버 차트 : $(cut -f1 "$FINAL" | sort -u | wc -l | tr -d ' ') =="
|
||||
echo "== 고유 이미지: $(cut -f3 "$FINAL" | sort -u | wc -l | tr -d ' ') =="
|
||||
echo "== 산출물 : $FINAL =="
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# generate-sbom.sh
|
||||
# extract-helm-images.sh 가 만든 이미지 인벤토리(images_final.tsv)의 각 이미지에
|
||||
# 대해 **컨테이너 내부에 설치된 `trivy` 바이너리**로 CycloneDX SBOM 을 생성한다.
|
||||
#
|
||||
# 실행 환경: helm/trivy/python3 가 설치된 리눅스 컨테이너 내부(직접 호출).
|
||||
# 이미지는 trivy 가 원격 pull → 분석 후 SBOM 작성. 취약점 스캔은 이후 scan-sbom.sh
|
||||
# 가 이 SBOM 을 입력으로 오프라인 수행한다(이미지 재pull 불필요).
|
||||
#
|
||||
# 사용:
|
||||
# bash doc/scripts/extract-helm-images.sh . . # 선행: images_final.tsv 생성
|
||||
# bash doc/scripts/generate-sbom.sh images_final.tsv . # SBOM 생성
|
||||
#
|
||||
# 인자:
|
||||
# $1 이미지 소스. images_final.tsv(TSV 3열: chart⇥version⇥image) 또는 이미지 1줄씩 텍스트.
|
||||
# $2 출력 디렉토리 (기본: 현재 디렉토리)
|
||||
#
|
||||
# 환경변수:
|
||||
# PARALLEL 1차 동시 생성 개수 (기본 3)
|
||||
# RETRY_PARALLEL 재시도 동시 개수 (기본 1)
|
||||
# RETRIES 실패 이미지 재시도 횟수 (기본 1)
|
||||
# LIMIT 대상 이미지 수 상한 (기본 0 = 전체)
|
||||
# TRIVY_CACHE_DIR trivy 캐시 디렉토리 (미설정 시 trivy 기본 ~/.cache/trivy)
|
||||
# TRIVY_TIMEOUT 이미지당 분석 타임아웃 (기본 15m — 대용량 이미지 여유)
|
||||
# MERGE=1 cyclonedx CLI 가 있으면 catalog.cdx.json 병합 생성 (선택)
|
||||
# 사설 레지스트리 인증: trivy 네이티브 방식 사용 —
|
||||
# - 단일 레지스트리: TRIVY_USERNAME / TRIVY_PASSWORD
|
||||
# - 다중 레지스트리: DOCKER_CONFIG(=docker config.json 이 있는 디렉토리) 또는 ~/.docker/config.json
|
||||
#
|
||||
# 산출물 ($2 기준):
|
||||
# sbom/<img>.cdx.json 이미지별 CycloneDX SBOM
|
||||
# sbom-index.tsv chart⇥version⇥image⇥status⇥seconds⇥sbom_file
|
||||
# sbom-gen.log 실행 로그
|
||||
# catalog.cdx.json (MERGE=1 + cyclonedx CLI 존재 시) 전체 병합 SBOM
|
||||
#
|
||||
# 요구사항(컨테이너 내부): trivy, python3, bash. (선택) cyclonedx
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
command -v trivy >/dev/null || { echo "!! trivy 바이너리가 없습니다 (컨테이너에 설치 필요)" >&2; exit 127; }
|
||||
command -v python3 >/dev/null || { echo "!! python3 가 없습니다" >&2; exit 127; }
|
||||
|
||||
PARALLEL="${PARALLEL:-3}"
|
||||
RETRY_PARALLEL="${RETRY_PARALLEL:-1}"
|
||||
RETRIES="${RETRIES:-1}"
|
||||
LIMIT="${LIMIT:-0}"
|
||||
TRIVY_TIMEOUT="${TRIVY_TIMEOUT:-15m}" # 대용량 이미지(예: openmetadata) 분석 여유
|
||||
[ -n "${TRIVY_CACHE_DIR:-}" ] && export TRIVY_CACHE_DIR # trivy 가 네이티브로 인식
|
||||
|
||||
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
# --- 내부 모드: 이미지 1개 SBOM 생성 (xargs 병렬 워커가 재귀 호출) ---------------
|
||||
# 인자: --gen-one <image> <sbom_dir> <log_file>
|
||||
if [ "${1:-}" = "--gen-one" ]; then
|
||||
img="$2"; sbomdir="$3"; log="${4:-/dev/null}"
|
||||
safe="$(printf '%s' "$img" | tr '/:@' '___')"
|
||||
out="$sbomdir/$safe.cdx.json"
|
||||
t0=$(date +%s)
|
||||
# --cache-backend memory : 분석 캐시를 메모리에 두어 공유 캐시 디렉토리의 bolt 잠금 충돌 방지(병렬 안전).
|
||||
# --skip-db-update : DB 는 메인 모드에서 1회 워밍했으므로 읽기만.
|
||||
if trivy image --quiet --cache-backend memory --skip-db-update \
|
||||
--format cyclonedx --timeout "$TRIVY_TIMEOUT" --output "$out" "$img" 2>"$out.err"; then
|
||||
dur=$(( $(date +%s) - t0 ))
|
||||
if [ -s "$out" ]; then line="$(printf '%s\tOK\t%s\t%s' "$img" "$dur" "$out")"
|
||||
else rm -f "$out"; line="$(printf '%s\tERROR:empty-sbom\t%s\t' "$img" "$dur")"; fi
|
||||
else
|
||||
dur=$(( $(date +%s) - t0 ))
|
||||
reason="$(tr '\n' ' ' < "$out.err" | grep -oiE '(unauthorized|not found|no such|manifest unknown|denied|timeout|unexpected eof|failed to)[^.]*' | head -1)"
|
||||
[ -z "$reason" ] && reason="sbom failed"
|
||||
rm -f "$out"
|
||||
line="$(printf '%s\tERROR:%s\t%s\t' "$img" "${reason:0:50}" "$dur")"
|
||||
fi
|
||||
printf '%s\n' "$line"
|
||||
st="$(printf '%s' "$line" | cut -f2)"
|
||||
printf '%s %-6s %5ss %s\n' "$(ts)" "${st%%:*}" "$dur" "$img" >> "$log"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 메인 모드 --------------------------------------------------------------
|
||||
SRC="${1:?이미지 소스 파일(images_final.tsv 또는 이미지 목록)을 지정하세요}"
|
||||
OUT_DIR="${2:-$(pwd)}"
|
||||
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
SBOM_DIR="$OUT_DIR/sbom"
|
||||
INDEX="$OUT_DIR/sbom-index.tsv"
|
||||
LOG="$OUT_DIR/sbom-gen.log"
|
||||
RESULTS="$OUT_DIR/.sbom-results.tsv"
|
||||
mkdir -p "$SBOM_DIR"; : > "$RESULTS"
|
||||
|
||||
fmt_elapsed() { printf '%dm %02ds' $(($1/60)) $(($1%60)); }
|
||||
|
||||
if awk -F'\t' 'NR==1{exit !(NF>=3)}' "$SRC"; then
|
||||
IMAGES="$(cut -f3 "$SRC" | sed -E 's/^["'"'"']+|["'"'"']+$//g' | grep -vE '^$' | sort -u)"
|
||||
else
|
||||
IMAGES="$(sed -E 's/^["'"'"']+|["'"'"']+$//g' "$SRC" | grep -vE '^$' | sort -u)"
|
||||
fi
|
||||
[ "$LIMIT" -gt 0 ] && IMAGES="$(printf '%s\n' "$IMAGES" | head -n "$LIMIT")"
|
||||
N="$(printf '%s\n' "$IMAGES" | grep -c .)"
|
||||
|
||||
RUN_START=$(date +%s)
|
||||
{
|
||||
echo "==================================================================="
|
||||
echo "SBOM GEN START : $(ts)"
|
||||
echo "trivy : $(trivy --version 2>/dev/null | head -1) format: cyclonedx"
|
||||
echo "parallel: $PARALLEL (retry: $RETRY_PARALLEL x$RETRIES)"
|
||||
echo "images : $N cache: ${TRIVY_CACHE_DIR:-~/.cache/trivy}"
|
||||
echo "==================================================================="
|
||||
} | tee "$LOG" >&2
|
||||
|
||||
# DB 1회 사전 다운로드(워밍). 이후 워커는 --skip-db-update 로 읽기만 하여 동시 갱신 충돌 방지.
|
||||
echo ">> [$(ts)] DB 워밍 중..." | tee -a "$LOG" >&2
|
||||
DB_START=$(date +%s)
|
||||
trivy image --download-db-only >>"$LOG" 2>&1 \
|
||||
|| { echo "!! DB 다운로드 실패" | tee -a "$LOG" >&2; exit 1; }
|
||||
echo ">> [$(ts)] DB 워밍 완료 ($(fmt_elapsed $(( $(date +%s) - DB_START ))))" | tee -a "$LOG" >&2
|
||||
|
||||
echo ">> [$(ts)] SBOM 1차 생성 (parallel=$PARALLEL)" | tee -a "$LOG" >&2
|
||||
P1=$(date +%s)
|
||||
printf '%s\n' "$IMAGES" \
|
||||
| xargs -P "$PARALLEL" -I{} bash "$SELF" --gen-one {} "$SBOM_DIR" "$LOG" \
|
||||
>> "$RESULTS"
|
||||
echo ">> [$(ts)] 1차 완료 ($(fmt_elapsed $(( $(date +%s) - P1 ))))" | tee -a "$LOG" >&2
|
||||
|
||||
attempt=0
|
||||
while [ "$attempt" -lt "$RETRIES" ]; do
|
||||
attempt=$((attempt+1))
|
||||
FAILED="$(comm -23 \
|
||||
<(awk -F'\t' '$2!="OK"{print $1}' "$RESULTS" | sort -u) \
|
||||
<(awk -F'\t' '$2=="OK"{print $1}' "$RESULTS" | sort -u))"
|
||||
nf="$(printf '%s\n' "$FAILED" | grep -c .)"
|
||||
[ "$nf" -eq 0 ] && { echo ">> [$(ts)] 재시도할 실패 없음" | tee -a "$LOG" >&2; break; }
|
||||
echo ">> [$(ts)] 재시도 $attempt/$RETRIES — 실패 $nf개 (parallel=$RETRY_PARALLEL)" | tee -a "$LOG" >&2
|
||||
printf '%s\n' "$FAILED" \
|
||||
| xargs -P "$RETRY_PARALLEL" -I{} bash "$SELF" --gen-one {} "$SBOM_DIR" "$LOG" \
|
||||
>> "$RESULTS"
|
||||
done
|
||||
|
||||
# --- 인덱스 확정: images_final.tsv(chart,version,image) 와 조인 --------------
|
||||
python3 - "$RESULTS" "$SRC" > "$INDEX" <<'PY'
|
||||
import sys, os
|
||||
results, src = sys.argv[1], sys.argv[2]
|
||||
best={}
|
||||
for l in open(results):
|
||||
p=l.rstrip("\n").split("\t")
|
||||
if len(p)<3: continue
|
||||
img=p[0]
|
||||
if p[1]=="OK" or img not in best or best[img][0]!="OK":
|
||||
best[img]=(p[1], p[2] if len(p)>2 else "", p[3] if len(p)>3 else "")
|
||||
rows=[]; seen=set()
|
||||
try:
|
||||
for l in open(src):
|
||||
c=l.rstrip("\n").split("\t")
|
||||
if len(c)>=3 and c[2]:
|
||||
img=c[2].strip('"\'' )
|
||||
st,sec,f=best.get(img,("MISSING","",""))
|
||||
rows.append((c[0],c[1],img,st,sec,os.path.basename(f))); seen.add(img)
|
||||
except Exception: pass
|
||||
for img,(st,sec,f) in best.items():
|
||||
if img not in seen: rows.append(("","",img,st,sec,os.path.basename(f)))
|
||||
for r in sorted(rows): print("\t".join(r))
|
||||
PY
|
||||
|
||||
TOTAL=$(( $(date +%s) - RUN_START ))
|
||||
printf '%s' "$TOTAL" > "$OUT_DIR/.sbom-gen-seconds" 2>/dev/null || true # scan-sbom 요약에서 단계별 시간 표시용
|
||||
OKN=$(awk -F'\t' '$4=="OK"' "$INDEX" | wc -l | tr -d ' ')
|
||||
ERRN=$(awk -F'\t' '$4!="OK" && $4!=""' "$INDEX" | wc -l | tr -d ' ')
|
||||
|
||||
# --- 선택: catalog.cdx.json 병합 (cyclonedx CLI 존재 시) ---------------------
|
||||
if [ "${MERGE:-0}" = "1" ]; then
|
||||
if command -v cyclonedx >/dev/null; then
|
||||
echo ">> [$(ts)] catalog.cdx.json 병합" | tee -a "$LOG" >&2
|
||||
files=$(cd "$SBOM_DIR" && ls *.cdx.json 2>/dev/null)
|
||||
if [ -n "$files" ]; then
|
||||
( cd "$SBOM_DIR" && cyclonedx merge --input-files $files --output-file "$OUT_DIR/catalog.cdx.json" ) >>"$LOG" 2>&1 \
|
||||
&& echo ">> catalog.cdx.json 생성" | tee -a "$LOG" >&2 \
|
||||
|| echo "!! 병합 실패(선택 단계) — 개별 SBOM 은 정상" | tee -a "$LOG" >&2
|
||||
fi
|
||||
else
|
||||
echo "!! MERGE=1 이나 cyclonedx CLI 없음 — 병합 생략" | tee -a "$LOG" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "==================================================================="
|
||||
echo "SBOM GEN END : $(ts) TOTAL: $(fmt_elapsed "$TOTAL") 성공/실패: $OKN / $ERRN"
|
||||
echo "SBOM: $SBOM_DIR/ 인덱스: $INDEX"
|
||||
echo "==================================================================="
|
||||
} | tee -a "$LOG" >&2
|
||||
rm -f "$RESULTS"
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# scan-sbom.sh
|
||||
# generate-sbom.sh 가 만든 CycloneDX SBOM(sbom/*.cdx.json)을 **컨테이너 내부의
|
||||
# `trivy` 바이너리**(`trivy sbom`)로 스캔해 이미지별 취약점을 집계한다.
|
||||
#
|
||||
# SBOM 을 입력으로 쓰므로 **이미지 재pull·레지스트리 인증이 불필요**(오프라인).
|
||||
# 취약점 DB 만 있으면 되며, CVE DB 갱신 시 재pull 없이 재스캔할 수 있다.
|
||||
#
|
||||
# 사용:
|
||||
# bash doc/scripts/generate-sbom.sh images_final.tsv . # 선행: sbom/ 생성
|
||||
# bash doc/scripts/scan-sbom.sh . # SBOM 취약점 스캔
|
||||
#
|
||||
# 인자:
|
||||
# $1 SBOM 작업 디렉토리 (sbom/ 과 sbom-index.tsv 가 있는 곳. 기본: 현재 디렉토리)
|
||||
#
|
||||
# 환경변수:
|
||||
# SEVERITY 집계 심각도 (기본 HIGH,CRITICAL)
|
||||
# PARALLEL 동시 스캔 개수 (기본 4 — 오프라인이라 높여도 안전)
|
||||
# TRIVY_CACHE_DIR trivy 캐시 디렉토리 (미설정 시 trivy 기본 ~/.cache/trivy)
|
||||
#
|
||||
# 산출물 ($1 기준):
|
||||
# trivy-reports/<img>.json 이미지별 원본 스캔 결과
|
||||
# trivy-summary.tsv chart⇥image⇥CRIT⇥HIGH⇥MED⇥LOW⇥status⇥seconds
|
||||
# trivy-summary.md 마크다운 요약
|
||||
# trivy-run.log 실행 로그
|
||||
#
|
||||
# 요구사항(컨테이너 내부): trivy, python3, bash
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
command -v trivy >/dev/null || { echo "!! trivy 바이너리가 없습니다 (컨테이너에 설치 필요)" >&2; exit 127; }
|
||||
command -v python3 >/dev/null || { echo "!! python3 가 없습니다" >&2; exit 127; }
|
||||
|
||||
SEVERITY="${SEVERITY:-HIGH,CRITICAL}"
|
||||
PARALLEL="${PARALLEL:-4}"
|
||||
[ -n "${TRIVY_CACHE_DIR:-}" ] && export TRIVY_CACHE_DIR
|
||||
|
||||
ts() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
||||
|
||||
# --- 내부 모드: SBOM 1개 스캔 (xargs 병렬 워커) ------------------------------
|
||||
# 인자: --scan-one <image> <sbom_file> <reports_dir> <log>
|
||||
if [ "${1:-}" = "--scan-one" ]; then
|
||||
img="$2"; sbomfile="$3"; reports="$4"; log="${5:-/dev/null}"
|
||||
safe="$(printf '%s' "$img" | tr '/:@' '___')"
|
||||
json="$reports/$safe.json"
|
||||
t0=$(date +%s)
|
||||
if trivy sbom --quiet --scanners vuln --severity "$SEVERITY" \
|
||||
--cache-backend memory --skip-db-update \
|
||||
--format json "$sbomfile" > "$json" 2>"$json.err"; then
|
||||
dur=$(( $(date +%s) - t0 ))
|
||||
line="$(python3 - "$img" "$json" "$dur" <<'PY'
|
||||
import sys,json,collections
|
||||
img,path,dur=sys.argv[1],sys.argv[2],sys.argv[3]
|
||||
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}")
|
||||
except Exception as e:
|
||||
print(f"{img}\t-\t-\t-\t-\tPARSE_ERR:{str(e)[:40]}\t{dur}")
|
||||
PY
|
||||
)"
|
||||
else
|
||||
dur=$(( $(date +%s) - t0 ))
|
||||
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")"
|
||||
fi
|
||||
printf '%s\n' "$line"
|
||||
st="$(printf '%s' "$line" | cut -f6)"
|
||||
printf '%s %-6s %5ss %s\n' "$(ts)" "${st%%:*}" "$dur" "$img" >> "$log"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 메인 모드 --------------------------------------------------------------
|
||||
OUT_DIR="${1:-$(pwd)}"
|
||||
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
SBOM_DIR="$OUT_DIR/sbom"
|
||||
INDEX="$OUT_DIR/sbom-index.tsv"
|
||||
REPORTS="$OUT_DIR/trivy-reports"
|
||||
SUMMARY_TSV="$OUT_DIR/trivy-summary.tsv"
|
||||
SUMMARY_MD="$OUT_DIR/trivy-summary.md"
|
||||
LOG="$OUT_DIR/trivy-run.log"
|
||||
RESULTS="$OUT_DIR/.scan-results.tsv"
|
||||
CHARTMAP="$OUT_DIR/.chartmap.tsv"
|
||||
TARGETS="$OUT_DIR/.scan-targets.tsv"
|
||||
[ -d "$SBOM_DIR" ] || { echo "!! SBOM 디렉토리 없음: $SBOM_DIR (generate-sbom.sh 먼저 실행)" >&2; exit 1; }
|
||||
mkdir -p "$REPORTS"; : > "$RESULTS"
|
||||
|
||||
fmt_elapsed() { printf '%dm %02ds' $(($1/60)) $(($1%60)); }
|
||||
|
||||
# 스캔 대상: (image⇥sbom_file). sbom-index.tsv 있으면 그걸로, 없으면 sbom/*.cdx.json 글롭.
|
||||
# 동시에 image->chart 매핑(CHARTMAP) 작성.
|
||||
: > "$CHARTMAP"
|
||||
if [ -f "$INDEX" ]; then
|
||||
awk -F'\t' -v d="$SBOM_DIR" '$4=="OK" && $6!=""{print $3"\t"d"/"$6}' "$INDEX" | sort -u > "$TARGETS"
|
||||
awk -F'\t' '$4=="OK" && $6!=""{print $3"\t"$1"@"$2}' "$INDEX" | sort -u > "$CHARTMAP"
|
||||
else
|
||||
: > "$TARGETS"
|
||||
for f in "$SBOM_DIR"/*.cdx.json; do
|
||||
[ -e "$f" ] || continue
|
||||
img="$(python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));m=d.get("metadata",{}).get("component",{});print(m.get("name","")+((":"+m["version"]) if m.get("version") else ""))' "$f" 2>/dev/null)"
|
||||
[ -z "$img" ] && img="$(basename "$f")"
|
||||
printf '%s\t%s\n' "$img" "$f" >> "$TARGETS"
|
||||
done
|
||||
fi
|
||||
N="$(grep -c . "$TARGETS" 2>/dev/null || echo 0)"
|
||||
|
||||
RUN_START=$(date +%s)
|
||||
{
|
||||
echo "==================================================================="
|
||||
echo "SBOM SCAN START : $(ts)"
|
||||
echo "trivy : $(trivy --version 2>/dev/null | head -1) (trivy sbom, 오프라인)"
|
||||
echo "severity : $SEVERITY parallel: $PARALLEL"
|
||||
echo "SBOMs : $N cache: ${TRIVY_CACHE_DIR:-~/.cache/trivy}"
|
||||
echo "==================================================================="
|
||||
} | tee "$LOG" >&2
|
||||
|
||||
echo ">> [$(ts)] DB 워밍 중..." | tee -a "$LOG" >&2
|
||||
trivy image --download-db-only >>"$LOG" 2>&1 \
|
||||
|| { echo "!! DB 다운로드 실패" | tee -a "$LOG" >&2; exit 1; }
|
||||
|
||||
echo ">> [$(ts)] 스캔 시작 (parallel=$PARALLEL)" | tee -a "$LOG" >&2
|
||||
P1=$(date +%s)
|
||||
# 각 타깃 라인(image⇥sbom_file)을 워커로. -I{} 는 라인 단위로 아이템을 읽으므로
|
||||
# {} 에 탭 포함 전체 라인이 들어온다. bash -c 안에서 cut 으로 탭 분해(BSD/GNU 공통).
|
||||
xargs -P "$PARALLEL" -I{} bash -c '
|
||||
im=$(printf %s "$1" | cut -f1)
|
||||
sf=$(printf %s "$1" | cut -f2)
|
||||
[ -n "$im" ] && bash "$0" --scan-one "$im" "$sf" "$2" "$3"
|
||||
' "$SELF" {} "$REPORTS" "$LOG" < "$TARGETS" \
|
||||
>> "$RESULTS"
|
||||
echo ">> [$(ts)] 스캔 완료 ($(fmt_elapsed $(( $(date +%s) - P1 ))))" | tee -a "$LOG" >&2
|
||||
|
||||
TOTAL=$(( $(date +%s) - RUN_START )) # 취약점 스캔 단계(오프라인) 소요
|
||||
GEN_SECS=$(cat "$OUT_DIR/.sbom-gen-seconds" 2>/dev/null || echo "") # SBOM 생성 단계(이미지 pull) 소요
|
||||
python3 - "$RESULTS" "$CHARTMAP" "$SEVERITY" "$TOTAL" "$(ts)" "$SUMMARY_TSV" "${GEN_SECS:-}" <<'PY' | tee "$SUMMARY_MD" >&2
|
||||
import sys
|
||||
results, chartmap, sev, total, tstamp, tsv_out = sys.argv[1:7]
|
||||
total=int(total)
|
||||
gen = sys.argv[7] if len(sys.argv)>7 and sys.argv[7].strip().isdigit() else None
|
||||
gen = int(gen) if gen is not None else None
|
||||
def dur(s): return f"{s//60}분 {s%60}초"
|
||||
# 표시 심각도 = 실제 스캔한 SEVERITY 만 (순위 내림차순). MED/LOW 는 스캔 안 하면 항상 0이므로 제외.
|
||||
RANK=["CRITICAL","HIGH","MEDIUM","LOW"]; IDX={"CRITICAL":1,"HIGH":2,"MEDIUM":3,"LOW":4}
|
||||
scanned=[s.strip().upper() for s in sev.split(",") if s.strip()]
|
||||
shown=[s for s in RANK if s in scanned] or ["CRITICAL","HIGH"]
|
||||
cm={}
|
||||
try:
|
||||
for l in open(chartmap):
|
||||
p=l.rstrip("\n").split("\t")
|
||||
if len(p)>=2: cm[p[0]]=p[1]
|
||||
except FileNotFoundError: pass
|
||||
best={}
|
||||
for l in open(results):
|
||||
p=l.rstrip("\n").split("\t")
|
||||
if len(p)<7: continue
|
||||
img=p[0]
|
||||
if p[5]=="OK" or img not in best or best[img][5]!="OK": best[img]=p
|
||||
rows=list(best.values())
|
||||
def cnt(r,s):
|
||||
try: return int(r[IDX[s]])
|
||||
except: return 0
|
||||
rows.sort(key=lambda r: tuple(-cnt(r,s) for s in shown))
|
||||
ok=[r for r in rows if r[5]=="OK"]; err=[r for r in rows if r[5]!="OK"]
|
||||
totals={s:sum(cnt(r,s) for r in ok) for s in shown}
|
||||
# TSV: chart, image, <shown counts...>, status (CRITICAL 은 첫 카운트 열 = col3)
|
||||
with open(tsv_out,"w") as f:
|
||||
for r in rows:
|
||||
cols=[cm.get(r[0],""), r[0]] + [str(cnt(r,s)) for s in shown] + [r[5]]
|
||||
f.write("\t".join(cols)+"\n")
|
||||
print("# Trivy SBOM 취약점 요약\n")
|
||||
print(f"- 생성: {tstamp}")
|
||||
print(f"- 대상 SBOM: **{len(rows)}개** (성공 {len(ok)} / 실패 {len(err)})")
|
||||
if gen is not None:
|
||||
print(f"- SBOM 생성 소요: **{dur(gen)}** (이미지 pull 포함, 파이프라인 주 비용)")
|
||||
print(f"- 취약점 스캔 소요: **{dur(total)}** (오프라인, SBOM 입력)")
|
||||
print(f"- 집계 심각도: `{sev}`")
|
||||
print("- 취약점 합계: " + ", ".join(f"**{s} {totals[s]}**" for s in shown))
|
||||
print()
|
||||
print("| Chart | Image | " + " | ".join(shown) + " | Status |")
|
||||
print("|---|---|" + "---:|"*len(shown) + "---|")
|
||||
for r in rows:
|
||||
cells=" | ".join(str(cnt(r,s)) for s in shown)
|
||||
print(f"| {cm.get(r[0],'')} | `{r[0]}` | {cells} | {r[5]} |")
|
||||
PY
|
||||
|
||||
{
|
||||
echo "==================================================================="
|
||||
echo "SBOM SCAN END : $(ts) TOTAL: $(fmt_elapsed "$TOTAL")"
|
||||
echo "요약: $SUMMARY_MD"
|
||||
echo "==================================================================="
|
||||
} | tee -a "$LOG" >&2
|
||||
rm -f "$RESULTS" "$CHARTMAP" "$TARGETS"
|
||||
Reference in New Issue
Block a user