39bb879495
doc/scripts/ -> scripts/pipeline/ (security-catalog 의 최상위 scripts/ 관례 채택). 스크립트 로직은 변경 없음 (상대경로 깊이 동일).
190 lines
8.9 KiB
Bash
Executable File
190 lines
8.9 KiB
Bash
Executable File
#!/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"
|