자체 빌드 드리프트 스캔·재빌드 트리거를 제거하고 CI 파이프라인을 정리한다
hardened-containers가 이미 rescan.yml로 매일 자율 재스캔·재빌드하고, sbom.yml이 custom-values.yaml 기준으로 자체 빌드 이미지를 다른 카탈로그 이미지와 동일하게 스캔하고 있어 self-build-drift-check.yml의 트리거·전용 스캔이 순수 중복이었다 (SECURITY_IMAGES_DISPATCH_TOKEN도 등록된 적 없어 트리거 스텝은 항상 실패하던 죽은 코드). check-rebuild-needed.py가 더하던 fixable/no-fix 구분도 cve-gate.py 리포트에 이미 있어 흡수할 필요 없이 삭제했다. 근거는 ADR 0005. 곁들여 CI 위생 문제(trivy DB 캐시 없음, concurrency 없음, catalog-tag-update.yml의 브랜치 누적)를 함께 고치고, hardened-containers의 docs/image-authoring.md가 docs/image-authoring/ 로 분할된 것을 반영해 관련 링크를 정정했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)
|
||||
|
||||
이 skill 은 실행하지 않는다 — 레버를 판단해 실행 skill로 넘긴다. 게이트 실행/해석은
|
||||
`sbom-cve-gate`, 자체 빌드는 **별도 레포 `hardened-containers`**(그 레포의
|
||||
`docs/image-authoring.md`가 단일 출처), 배포 검증은
|
||||
`docs/image-authoring/README.md`가 단일 출처), 배포 검증은
|
||||
[deploy-test-procedure.md](../../deploy-test-procedure.md)가 단일 출처다 — 여기 반복 안 한다.
|
||||
|
||||
## 흐름
|
||||
@@ -37,10 +37,10 @@ description: 게이트가 카탈로그 이미지에서 차단 CVE(CRITICAL/HIGH)
|
||||
비교) 확인한다. 실례: keycloak `CVE-2025-59250`(mssql-jdbc jar 하나가 두 컴포넌트로
|
||||
잡혀 잘린 쪽만 매칭된 오탐). 근거·만료일은 `doc/cve-exceptions.json`.
|
||||
6. 자체 빌드는 별도 레포 `hardened-containers`에서 한다 — 그 레포의
|
||||
`docs/image-authoring.md`가 절차 단일 출처다. 재빌드가 필요하면 그 레포의
|
||||
`build-image.yml`을 `workflow_dispatch`로 부른다(`scripts/build/check-rebuild-needed.py`
|
||||
가 배포 중인 이미지를 스캔해 대상을 판단한다). 태그/베이스 OS 교체는 카탈로그
|
||||
values만 바꾸고 재게이트한다 — 별도 스크립트 없음.
|
||||
`docs/image-authoring/README.md`가 절차 단일 출처다. 재빌드는 그 레포가 `rescan.yml`로
|
||||
매일 자율 수행한다 — dip-catalog는 트리거하지 않는다. `sbom.yml`이 배포 중인 자체
|
||||
빌드 이미지의 CVE 드리프트를 다른 카탈로그 이미지와 동일하게 보고만 한다. 태그/베이스
|
||||
OS 교체는 카탈로그 values만 바꾸고 재게이트한다 — 별도 스크립트 없음.
|
||||
7. 수정 후 반드시 재게이트하고 PASS라도 배포 검증까지 끝나야 종료다 — 1회로 끝난다고
|
||||
가정하지 않는다(keycloak은 1차 수정 후 재스캔에서 micrometer 2건이 새로 잡혔다).
|
||||
8. 착지는 브랜치 push까지다 — 조직 정책상 `GITHUB_TOKEN`으로 PR을 못 연다. 사람이 PR을
|
||||
|
||||
@@ -56,7 +56,7 @@ gh run watch --repo <org>/dip-catalog
|
||||
```
|
||||
|
||||
자체 빌드로 가야 한다면 별도 레포 `hardened-containers`에서 한다 — 그 레포의
|
||||
`docs/image-authoring.md`가 절차 단일 출처다. 판정 로직 상세는
|
||||
`docs/image-authoring/README.md`가 절차 단일 출처다. 판정 로직 상세는
|
||||
`scripts/pipeline/cve-gate.py`의 모듈 docstring을 1차 출처로 본다.
|
||||
|
||||
## 참고
|
||||
|
||||
@@ -6,7 +6,7 @@ name: catalog-tag-update
|
||||
# 차트에 반영할지" 는 이 카탈로그가 판단한다(catalog/image-map/<image>.env).
|
||||
#
|
||||
# public raw URL 로 읽으므로 인증이 필요 없다. hardened-containers 쪽 시크릿·PAT 도 없다 —
|
||||
# 의존 방향은 카탈로그 → 이미지 단방향이다(docs/image-authoring.md "카탈로그 레포와의
|
||||
# 의존 방향은 카탈로그 → 이미지 단방향이다(docs/image-authoring/ci.md "카탈로그 레포와의
|
||||
# 계약", hardened-containers 레포).
|
||||
#
|
||||
# PR 은 자동 생성하지 않는다 — GitHub Actions 는 GITHUB_TOKEN 으로 PR 을 만들 수 없다는
|
||||
@@ -22,6 +22,12 @@ on:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: catalog-tag-update
|
||||
# push 도중 취소를 막는다 — 아래 고정 브랜치(build/catalog-tag-update)를 두고
|
||||
# 두 실행이 경합하는 것을 방지한다.
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
SECURITY_IMAGES_REPO: paasup/hardened-containers
|
||||
|
||||
@@ -51,10 +57,15 @@ jobs:
|
||||
if: steps.apply.outputs.count != '0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH="build/catalog-tag-update-$(date -u +%Y%m%d%H%M%S)"
|
||||
# 고정 브랜치명 하나를 재사용한다 — PR 은 사람이 열어야 해서(조직 정책) 매 실행
|
||||
# 새 브랜치를 만들면 방치된 동일 내용 브랜치가 날마다 쌓인다. 이 브랜치는 이
|
||||
# 워크플로만 쓰므로 force push 가 안전하다. 이미 이 브랜치로 PR 이 열려 있으면
|
||||
# force push 가 그 PR 을 갱신한다(원하는 동작) — PR 이 머지된 뒤면 다음 실행이
|
||||
# main 기준으로 새로 만든다.
|
||||
BRANCH="build/catalog-tag-update"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$BRANCH"
|
||||
git checkout -B "$BRANCH"
|
||||
|
||||
# apply-published-tags.py 가 이미 파일을 고쳤다 — 여기서는 그 결과를 커밋만 한다.
|
||||
FILES="$(python3 -c "
|
||||
@@ -74,7 +85,7 @@ jobs:
|
||||
-m "hardened-containers 레포의 published.json(게이트 PASS + push 확인된 발행 기록)을" \
|
||||
-m "반영한다. 상세는 이 실행의 Job Summary 참고." \
|
||||
-m "Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
git push -u origin "$BRANCH"
|
||||
git push --force origin "HEAD:$BRANCH"
|
||||
|
||||
COMPARE_URL="https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"
|
||||
{
|
||||
|
||||
@@ -32,6 +32,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: cve-edge-post
|
||||
cancel-in-progress: false # 외부로 POST 하는 잡이라 중간 취소보다 직렬화가 안전하다
|
||||
|
||||
jobs:
|
||||
cve-json:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -57,6 +61,20 @@ jobs:
|
||||
- name: git safe.directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: 캐시 키 날짜 계산
|
||||
id: cache-date
|
||||
run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# trivy 취약점 DB 캐시. sbom.yml 과 같은 규칙 — 하루 단위 키, 실패해도 무시.
|
||||
- name: trivy DB 캐시
|
||||
continue-on-error: true
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/sbom-out/cache
|
||||
key: trivy-db-${{ steps.cache-date.outputs.date }}
|
||||
restore-keys: |
|
||||
trivy-db-
|
||||
|
||||
# 사설 레지스트리 인증: 시크릿으로 docker config.json 을 만들어 trivy 가 읽게 한다.
|
||||
- name: 레지스트리 인증 구성
|
||||
env:
|
||||
|
||||
@@ -26,6 +26,12 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: sbom-${{ github.ref }}
|
||||
# PR 증분 diff 는 base_ref...HEAD 기준이라 최신 실행이 이전 커밋들의 변경까지 모두
|
||||
# 포함한다 — 취소해도 누락이 없다. 스케줄 전체 스캔은 취소하지 않는다.
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
sbom:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,6 +64,24 @@ jobs:
|
||||
- name: git safe.directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: 캐시 키 날짜 계산
|
||||
id: cache-date
|
||||
run: echo "date=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# trivy 취약점 DB 캐시. 하루 단위 키라 같은 날 여러 번 돌아도 최초 1회만
|
||||
# 새로 받는다. DB 가 낡아도 trivy 가 스스로 갱신하므로 오래된 캐시(전날 키,
|
||||
# restore-keys 로 폴백)를 복원해도 정확성 문제는 없다 — 다운로드 분량만
|
||||
# 줄어든다. 캐시는 최적화이지 정확성 요건이 아니므로 실패해도 워크플로를
|
||||
# 막지 않는다.
|
||||
- name: trivy DB 캐시
|
||||
continue-on-error: true
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/sbom-out/cache
|
||||
key: trivy-db-${{ steps.cache-date.outputs.date }}
|
||||
restore-keys: |
|
||||
trivy-db-
|
||||
|
||||
# 사설 레지스트리 인증: 시크릿으로 docker config.json 을 만들어 trivy 가 읽게 한다.
|
||||
- name: 레지스트리 인증 구성
|
||||
env:
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
name: self-build-drift-check
|
||||
|
||||
# 배포 중인 자체 빌드 이미지(hardened-containers 레포가 만든 것)가 새 CVE 로 규정을 벗어났는지
|
||||
# 이 카탈로그가 스스로 스캔해 판정한다. "무엇이 배포 중인가"는 이 카탈로그만 안다 —
|
||||
# hardened-containers 는 자기 스스로 이 판단을 하지 않는다
|
||||
# (hardened-containers 레포 docs/image-authoring.md "재빌드는 이 레포가 스스로 트리거하지 않는다").
|
||||
#
|
||||
# 판정(scripts/build/check-rebuild-needed.py)이 재빌드 대상이라고 하면 hardened-containers 의
|
||||
# `build-image.yml` 을 `workflow_dispatch` 로 부른다. 그 워크플로가 실제 빌드·검증·게이트·
|
||||
# push·`published.json` 갱신을 한다 — 이 워크플로는 트리거만 한다.
|
||||
#
|
||||
# 레지스트리 push 와 카탈로그 반영은 이 워크플로가 하지 않는다. `catalog-tag-update.yml` 이
|
||||
# `published.json` 을 읽어가는 별도 경로로 반영한다.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1' # 매주 월요일 02:00 UTC
|
||||
|
||||
env:
|
||||
SECURITY_IMAGES_REPO: paasup/hardened-containers
|
||||
|
||||
jobs:
|
||||
drift-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: trivy 설치
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||
| sh -s -- -b /usr/local/bin
|
||||
trivy --version | head -1
|
||||
|
||||
- name: 배포 중인 자체 빌드 이미지 스캔
|
||||
id: scan
|
||||
run: |
|
||||
set -uo pipefail
|
||||
mkdir -p /tmp/drift-reports
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs > /tmp/refs.tsv
|
||||
while IFS=$'\t' read -r img ref fname; do
|
||||
[ -n "${fname:-}" ] || continue
|
||||
echo "스캔: $ref"
|
||||
trivy image --quiet --scanners vuln \
|
||||
--severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL \
|
||||
--format json "$ref" > "/tmp/drift-reports/$fname" \
|
||||
|| { echo "::warning::$img: 스캔 실패 ($ref) — 이 이미지는 판정에서 빠진다"
|
||||
rm -f "/tmp/drift-reports/$fname"; }
|
||||
done < /tmp/refs.tsv
|
||||
|
||||
python3 scripts/build/check-rebuild-needed.py \
|
||||
--reports /tmp/drift-reports \
|
||||
--summary-md /tmp/drift.md \
|
||||
--json-out /tmp/drift.json
|
||||
cat /tmp/drift.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
python3 - <<'PY' >> "$GITHUB_OUTPUT"
|
||||
import json
|
||||
rows = json.load(open("/tmp/drift.json"))
|
||||
print("images=" + json.dumps([r["image"] for r in rows if r["status"] == "rebuild"]))
|
||||
PY
|
||||
|
||||
- name: hardened-containers 재빌드 트리거
|
||||
if: steps.scan.outputs.images != '' && steps.scan.outputs.images != '[]'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.SECURITY_IMAGES_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[ -n "$GH_TOKEN" ] || { echo "::error::SECURITY_IMAGES_DISPATCH_TOKEN 시크릿 없음 — ${SECURITY_IMAGES_REPO} 에 workflow_dispatch 를 걸 권한이 있는 PAT 필요"; exit 1; }
|
||||
python3 -c "import json,sys; print('\n'.join(json.loads(sys.argv[1])))" '${{ steps.scan.outputs.images }}' | \
|
||||
while read -r img; do
|
||||
[ -n "$img" ] || continue
|
||||
echo "재빌드 트리거: $img"
|
||||
gh workflow run self-build-image --repo "$SECURITY_IMAGES_REPO" -f "image=$img"
|
||||
done
|
||||
{
|
||||
echo
|
||||
echo "재빌드를 트리거했다: ${{ steps.scan.outputs.images }}"
|
||||
echo "진행 상황: https://github.com/${SECURITY_IMAGES_REPO}/actions/workflows/self-build-image.yml"
|
||||
echo
|
||||
echo "게이트 PASS + push 성공 시 그 레포의 \`published.json\` 이 갱신되고,"
|
||||
echo "\`catalog-tag-update.yml\` 이 다음 실행에서 카탈로그에 반영한다."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,6 +1,9 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# 로컬 Claude Code 도구 설정(개인 경로·권한 포함, 팀 공유 대상 아님)
|
||||
/.claude/settings.json
|
||||
|
||||
# 로컬 테스트 산출물 (커밋 금지) — 멀티테넌시 격리 검증 스크립트·문서·결과
|
||||
/tenant-verification/
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ dip-catalog/
|
||||
│ └── status.md # 구현 현황
|
||||
├── scripts/
|
||||
│ ├── pipeline/ # SBOM 생성 + CVE 스캔 + 게이트 판정 (sbom.yml/cve-edge-post.yml 이 쓴다)
|
||||
│ ├── build/ # 자체 빌드 이미지 축의 카탈로그 쪽 절반 — 드리프트 탐지
|
||||
│ │ # (check-rebuild-needed.py) + 발행 태그 반영
|
||||
│ │ # (apply-published-tags.py). 빌드 자체는 hardened-containers 레포
|
||||
│ ├── build/ # 자체 빌드 이미지 축의 카탈로그 쪽 절반 — 발행 태그 반영
|
||||
│ │ # (apply-published-tags.py). CVE 스캔은 sbom.yml 이 다른
|
||||
│ │ # 카탈로그 이미지와 동일하게 하고, 빌드 자체는 hardened-containers 레포
|
||||
│ └── deploy-test/ # 배포 검증 스크립트 + fixtures (helm/kubectl 실행 전담)
|
||||
├── catalog/
|
||||
│ └── image-map/<image>.env # 자체 빌드 이미지 → 차트·필드 매핑 (목록은 이 디렉토리가 단일 출처)
|
||||
@@ -154,7 +154,7 @@ python3 agent/update_catalog/skills/helm_diff/scripts/run.py \
|
||||
| [sbom-cve-gate](.claude/skills/sbom-cve-gate/SKILL.md) | SBOM 생성·CVE 스캔·게이트 판정 실행 및 결과 해석 (`scripts/pipeline`) |
|
||||
|
||||
자체 빌드 하드닝 이미지 추가·변경은 이 레포의 일이 아니다 — 별도 레포 `hardened-containers`
|
||||
에서 하고, 그 레포의 `docs/image-authoring.md`가 단일 출처다(이관 배경:
|
||||
에서 하고, 그 레포의 `docs/image-authoring/README.md`가 단일 출처다(이관 배경:
|
||||
[doc/migrations/](doc/migrations/)).
|
||||
|
||||
각 Skill 은 절차 본문을 복제하지 않고 권위 있는 문서(`doc/sbom-pipeline.md` 등)를
|
||||
@@ -175,7 +175,7 @@ python3 agent/update_catalog/skills/helm_diff/scripts/run.py \
|
||||
| 축 | 질문 | 실행 위치 | 워크플로(이 레포) | 게이트 | 단일 출처 |
|
||||
|----|------|----------|------------------|--------|----------|
|
||||
| **차트 카탈로그** | 우리가 배포하는 이미지에 무엇이 있는가 | 이 레포 | `sbom.yml`<br>`cve-edge-post.yml` | warn-only<br>**호출 안 함** | [doc/sbom-pipeline.md](doc/sbom-pipeline.md) |
|
||||
| **자체 빌드** | 그 이미지를 어떻게 만드는가 | 별도 레포 `hardened-containers` | `self-build-drift-check.yml`(탐지·트리거)<br>`catalog-tag-update.yml`(반영) | **강제**(그 레포 소유) | 그 레포의 `docs/image-authoring.md` |
|
||||
| **자체 빌드** | 그 이미지를 어떻게 만드는가 | 별도 레포 `hardened-containers` | `catalog-tag-update.yml`(반영) | **강제**(그 레포 소유) | 그 레포의 `docs/image-authoring/README.md` |
|
||||
|
||||
- **차트 축은 warn-only 다** — 게이트가 실패해도 CI/PR 을 막지 않는다. 카탈로그 차트 전체가
|
||||
이 게이트로 트리아지된 적이 없다. **자체 빌드 축의 게이트는 이미 강제다**(단, 그 게이트는
|
||||
@@ -186,10 +186,10 @@ python3 agent/update_catalog/skills/helm_diff/scripts/run.py \
|
||||
레버 판단은 [cve-remediation](.claude/skills/cve-remediation/SKILL.md) skill 이 갖는다.
|
||||
실행은 `hardened-containers` 레포에서 `workflow_dispatch` 로만 한다.
|
||||
- **이 레포는 "무엇을 배포 중인가"만 안다.** 어느 이미지를 자체 빌드하고 있는지는
|
||||
`catalog/image-map/` 이 단일 출처다. `scripts/build/check-rebuild-needed.py`(주간
|
||||
`self-build-drift-check.yml`)가 배포 중인 이미지를 직접 스캔해 재빌드 대상을 판단하고,
|
||||
필요하면 `hardened-containers` 의 `build-image.yml` 을 트리거만 한다 — 핀을 무엇으로
|
||||
올릴지는 그 레포가 판단한다.
|
||||
`catalog/image-map/` 이 단일 출처다. 배포 중인 자체 빌드 이미지의 CVE 드리프트는
|
||||
`sbom.yml` 이 다른 카탈로그 이미지와 동일하게 스캔·보고한다 — 재빌드 여부·시점은
|
||||
전적으로 `hardened-containers` 의 자체 `rescan.yml` 이 매일 자율 결정한다. 이 레포는
|
||||
트리거하지 않는다(ADR [0005](doc/decisions/0005-self-build-drift-check-removed.md)).
|
||||
- **카탈로그 반영은 pull 방식이다.** `hardened-containers` 는 이 카탈로그를 모른다 — 게이트
|
||||
PASS + push 성공 시 자기 레포의 `published.json` 만 갱신한다. `catalog-tag-update.yml`
|
||||
이 그 파일을 public raw URL 로 읽어가 `custom-values.yaml`/`dip-values.yaml` 을 패치한다.
|
||||
@@ -261,3 +261,4 @@ python3 agent/update_catalog/skills/helm_diff/scripts/run.py \
|
||||
| [agent/update_catalog/docs/decisions/001-agentic-first.md](agent/update_catalog/docs/decisions/001-agentic-first.md) | 파이프라인 오케스트레이션 건너뛰기 결정 배경 |
|
||||
| [agent/update_catalog/docs/status.md](agent/update_catalog/docs/status.md) | 구현 현황 상세 |
|
||||
| [doc/sbom-pipeline.md](doc/sbom-pipeline.md) | SBOM 생성·CVE 스캔·게이트 파이프라인 상세 |
|
||||
| [doc/architecture.md](doc/architecture.md) | `.github/workflows/` 4개 워크플로의 트리거 조건·잡 흐름도 (CI 파이프라인 아키텍처) |
|
||||
|
||||
@@ -49,13 +49,11 @@
|
||||
지우면 게이트가 PASS 로 떨어진다. 직전 버전을 없애는 결정이라 PR #28 에서 보류했다.
|
||||
- **자체 빌드 이미지 레포 분리는 완료됐다** — 프레임워크·이미지 정의·ADR 이
|
||||
`hardened-containers` 레포로 나갔다. 카탈로그 쪽은 `catalog/image-map/`(어느 차트를
|
||||
가리키는지) + `check-rebuild-needed.py`(드리프트 탐지) + `apply-published-tags.py`
|
||||
(발행 태그 반영)만 남았다. 배경·결합점 전체는
|
||||
가리키는지) + `apply-published-tags.py`(발행 태그 반영)만 남았다. CVE 드리프트
|
||||
스캔·재빌드 트리거는 별도로 두지 않는다 — `sbom.yml`이 자체 빌드 이미지도 함께 스캔·보고하고,
|
||||
재빌드는 `hardened-containers`의 `rescan.yml`이 자율 수행한다(ADR
|
||||
[0005](doc/decisions/0005-self-build-drift-check-removed.md)). 배경·결합점 전체는
|
||||
[doc/migrations/](doc/migrations/self-build-images-to-hardened-containers.md).
|
||||
**아직 설정 안 된 것**: `self-build-drift-check.yml` 이 `hardened-containers` 의
|
||||
`build-image.yml` 을 트리거하려면 그 레포에 `workflow_dispatch` 권한이 있는 PAT 을
|
||||
`SECURITY_IMAGES_DISPATCH_TOKEN` 시크릿으로 등록해야 한다. 등록 전까지 트리거 스텝은
|
||||
실패한다(의도된 명시적 실패 — 조용히 넘어가지 않는다).
|
||||
- **카탈로그 태그가 클러스터보다 앞서 있고 재검증 경로가 없다** — 게이트 PASS 만으로
|
||||
`patch-catalog-tag.py`(이제 `catalog-tag-update.yml` 을 통해)가 태그를 올린다. 실측:
|
||||
클러스터에 배포된 태그가 카탈로그가 가리키는 태그보다 뒤처진 사례가 있었다. 소스 안
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
hardened-containers 레포가 발행하는 자체 빌드 이미지가 이 카탈로그의 어느 차트·어느 필드를
|
||||
가리키는지 선언한다. **이 디렉토리에 파일이 있는 이미지 이름 = 이 카탈로그가 추적하는
|
||||
자체 빌드 이미지 목록**이다(`scripts/build/check-rebuild-needed.py`와
|
||||
`.github/workflows/catalog-tag-update.yml`이 이 목록을 기준으로 동작한다).
|
||||
자체 빌드 이미지 목록**이다(`.github/workflows/catalog-tag-update.yml`이 이 목록을 기준으로
|
||||
동작한다).
|
||||
|
||||
이관 배경: 이 정보는 원래 hardened-containers(당시 `images/<image>/catalog.env`)에 있었다.
|
||||
"어느 이미지를 쓰는가"는 카탈로그가 알아야 하고 "그 이미지를 어떻게 만드는가"는
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# CI 파이프라인 아키텍처
|
||||
|
||||
이 문서는 `.github/workflows/`에 있는 3개 워크플로가 **무엇을 트리거로 도는지**, **잡 흐름이
|
||||
어떻게 이어지는지**를 한눈에 보여준다. 각 워크플로 내부 메커니즘(스캔 방식, 게이트 판정
|
||||
로직 등)의 단일 출처는 별도 문서다 — 여기서 복제하지 않는다. 스타일은 별도 레포
|
||||
`hardened-containers`의 `docs/architecture.md`를 참고했다.
|
||||
|
||||
## 1. 두 축, 세 워크플로
|
||||
|
||||
카탈로그는 "무엇을 배포 중인가"(차트 카탈로그 축)와 "그 이미지를 어떻게 만드는가"(자체
|
||||
빌드 축)를 분리해서 다룬다 — 축·소유 레포·게이트 강제력 비교는
|
||||
[CLAUDE.md](../CLAUDE.md)의 "CVE/SBOM 게이트 작업" 표 참고. 이 레포에 있는 워크플로 3개는
|
||||
그 두 축 위에 이렇게 나뉜다.
|
||||
|
||||
| 워크플로 | 축 | 트리거 | 게이트 |
|
||||
|---|---|---|---|
|
||||
| `sbom.yml` | 차트 카탈로그(자체 빌드 이미지 포함) | `pull_request`(`manifests/helm/**`) · `workflow_dispatch` · 매주 일 18:00 UTC | warn-only |
|
||||
| `cve-edge-post.yml` | 차트 카탈로그 | `workflow_dispatch`만 (스케줄 주석 처리됨) | 없음 — 원시 집계 POST |
|
||||
| `catalog-tag-update.yml` | 자체 빌드 | `workflow_dispatch` · 매일 03:00 UTC | 없음 — 반영만 |
|
||||
|
||||
**PR 이벤트에 반응하는 워크플로는 `sbom.yml` 하나뿐이다.** 나머지 두 개는 사람이 Actions
|
||||
탭에서 수동 실행하거나 스케줄로만 돈다.
|
||||
|
||||
> 자체 빌드 이미지의 CVE 드리프트 스캔·재빌드 트리거를 위한 전용 워크플로
|
||||
> (`self-build-drift-check.yml`)는 없다 — `sbom.yml`이 자체 빌드 이미지도 다른 카탈로그
|
||||
> 이미지와 동일하게 스캔·보고하고, 재빌드는 전적으로 `hardened-containers`의 `rescan.yml`이
|
||||
> 자율 수행한다. 근거는 ADR [0005](decisions/0005-self-build-drift-check-removed.md).
|
||||
|
||||
## 2. 트리거 조건 → Job 흐름
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
PR["pull_request<br/>manifests/helm/** 변경"]
|
||||
WD1["workflow_dispatch<br/>chart, limit"]
|
||||
SCH1["schedule<br/>매주 일 18:00 UTC"]
|
||||
|
||||
subgraph SBOM["sbom.yml — 차트 카탈로그 SBOM/CVE 스캔 (자체 빌드 이미지 포함)"]
|
||||
S1[이미지 인벤토리 추출]
|
||||
S2["PR: diff 증분만 대상<br/>스케줄·수동: 전체 또는 지정 차트"]
|
||||
S3[SBOM 생성 CycloneDX]
|
||||
S4[취약점 스캔]
|
||||
S5["CVE 게이트 판정<br/>warn-only — 실패해도 PR 안 막음"]
|
||||
S6[아티팩트 업로드 + Job Summary]
|
||||
S1 --> S2 --> S3 --> S4 --> S5 --> S6
|
||||
end
|
||||
PR --> S1
|
||||
WD1 --> S1
|
||||
SCH1 --> S1
|
||||
|
||||
WD2["workflow_dispatch<br/>chart, limit<br/>(스케줄 없음 — 주석 처리)"]
|
||||
|
||||
subgraph EDGE["cve-edge-post.yml — 외부 집계 전송"]
|
||||
V1[이미지 인벤토리 추출]
|
||||
V2[SBOM 생성 + 취약점 스캔]
|
||||
V3[CVE JSON 집계]
|
||||
V4["POST edge.gke.paasup.io<br/>승인 예외·실효 등급 미적용"]
|
||||
V1 --> V2 --> V3 --> V4
|
||||
end
|
||||
WD2 --> V1
|
||||
|
||||
subgraph HC["hardened-containers — 다른 레포, 독립 루프 (이 레포는 트리거하지 않는다)"]
|
||||
HR1["rescan.yml<br/>매일 자율 재스캔"]
|
||||
HR2{드리프트 있음?}
|
||||
HB1[build-image.yml<br/>빌드→검증→SBOM→스캔→게이트]
|
||||
HB2["PASS → 레지스트리 push + attest<br/>+ published.json 갱신"]
|
||||
HR1 --> HR2
|
||||
HR2 -->|예| HB1
|
||||
HB1 --> HB2
|
||||
end
|
||||
|
||||
WD4["workflow_dispatch"]
|
||||
SCH4["schedule<br/>매일 03:00 UTC"]
|
||||
|
||||
subgraph SYNC["catalog-tag-update.yml — 발행 태그 카탈로그 반영"]
|
||||
C1[published.json 조회]
|
||||
C2["반영 대상 계산<br/>apply-published-tags.py"]
|
||||
C3{변경 있음?}
|
||||
C4["고정 브랜치로 push<br/>+ compare 링크 Job Summary"]
|
||||
C5[변경 없음 — 종료]
|
||||
C1 --> C2 --> C3
|
||||
C3 -->|예| C4
|
||||
C3 -->|아니오| C5
|
||||
end
|
||||
WD4 --> C1
|
||||
SCH4 --> C1
|
||||
HB2 -.->|"public raw URL 폴링<br/>인증 없음, pull 방식"| C1
|
||||
|
||||
C4 -.->|"CI는 GITHUB_TOKEN으로 PR 생성 불가<br/>(조직 정책, 실측 확정)"| HumanPR["사람이 compare 링크로<br/>PR 직접 오픈"]
|
||||
```
|
||||
|
||||
세 갈래를 눈여겨볼 것:
|
||||
|
||||
1. **`sbom.yml`만 PR에 반응한다.** `cve-edge-post.yml`·`catalog-tag-update.yml`은 스케줄·수동뿐이다.
|
||||
2. **레포 경계를 넘는 방향은 하나뿐이다 — pull.** `catalog-tag-update.yml`이
|
||||
`published.json`을 폴링해서 가져올 뿐, 자체 빌드가 언제 도는지는 이 레포가 전혀
|
||||
모른다 — `hardened-containers`의 `rescan.yml`이 전적으로 자율 결정하고, 이 레포는
|
||||
어느 쪽으로도 그 레포를 트리거하지 않는다.
|
||||
3. **끝은 항상 사람이다.** `catalog-tag-update.yml`이 브랜치까지는 push하지만 PR은 열지
|
||||
않는다 — CI가 `GITHUB_TOKEN`으로 PR을 못 여는 게 조직 정책으로 실측 확정된 제약이라서다
|
||||
(완화 방법은 [MEMORY.md](../MEMORY.md), [.claude/skills/cve-remediation/SKILL.md](../.claude/skills/cve-remediation/SKILL.md) 참고).
|
||||
|
||||
## 3. 워크플로별 상세
|
||||
|
||||
### `sbom.yml` — 차트 카탈로그 SBOM/CVE 스캔 (warn-only)
|
||||
|
||||
메커니즘 상세(스캔 대상 결정, `cve-gate.py` 판정 로직, 아티팩트 구조, trivy DB 캐싱·
|
||||
concurrency)의 단일 출처는 [doc/sbom-pipeline.md](sbom-pipeline.md)다. 여기서는 트리거·잡
|
||||
경계만 짚는다.
|
||||
|
||||
- PR에서는 `git diff`로 변경된 차트만 골라 스캔 범위를 줄인다(전체 스캔은 무겁다).
|
||||
- 스케줄·수동 실행은 전체(또는 `chart` 입력으로 지정한 하나)를 스캔한다.
|
||||
- **자체 빌드 이미지(`docker.io/paasup/*`)도 같은 대상이다** — `extract-helm-images.sh`가
|
||||
`custom-values.yaml`을 적용해 렌더하므로 별도 취급 없이 그대로 스캔된다. 자체 빌드
|
||||
이미지 CVE 드리프트를 따로 보는 워크플로는 없다 — 이 리포트가 유일한 경로다.
|
||||
- 실행 컨테이너(`vars.SBOM_PIPELINE_IMAGE`)에 helm/trivy/python3/bash/git이 없으면
|
||||
preflight 스텝에서 즉시 실패한다.
|
||||
- 게이트가 실패해도(`--warn-only`) 워크플로 자체는 성공으로 끝난다 — Job Summary와
|
||||
아티팩트로만 결과를 남긴다.
|
||||
|
||||
### `cve-edge-post.yml` — 외부 집계 전송
|
||||
|
||||
- `sbom.yml`과 이미지 인벤토리·SBOM·스캔 스텝을 그대로 재사용하지만, 게이트 판정
|
||||
(`cve-gate.py`)을 부르지 않는다 — 승인 예외·실효 등급(`max(벤더,NVD)`)이 적용 안 된
|
||||
**원시** 집계라는 뜻이다. `sbom.yml`의 결과와 CRITICAL 건수가 다르게 보일 수 있다.
|
||||
- 결과는 `(catalog, version, image)` 조합 단위 JSON으로 만들어져
|
||||
`https://edge.gke.paasup.io/api/v1/cve-scans`로 POST된다(`CVE_API_KEY` 헤더 인증,
|
||||
TLS 검증은 `--insecure`로 스킵).
|
||||
- 스케줄이 주석 처리돼 있어 현재는 **수동 실행만** 된다.
|
||||
|
||||
### `catalog-tag-update.yml` — 발행 태그 카탈로그 반영
|
||||
|
||||
- `hardened-containers`는 이 카탈로그의 존재를 모른다 — 게이트 PASS + push가 실제로
|
||||
일어났을 때만 자기 레포의 `published.json`을 갱신해둘 뿐이다. 이 워크플로가 그 파일을
|
||||
**public raw URL로 폴링**해서 가져온다(인증 불필요, 상세 계약은
|
||||
[doc/migrations/self-build-images-to-hardened-containers.md](migrations/self-build-images-to-hardened-containers.md)).
|
||||
역방향 알림(`repository_dispatch` 등)은 의도적으로 안 쓴다 — 그러려면
|
||||
`hardened-containers` 쪽에 이 카탈로그 쓰기 권한 PAT을 둬야 하는데, 그 레포의 "외부
|
||||
의존성 없이 단독 동작" 원칙과 충돌하기 때문이다.
|
||||
- `scripts/build/apply-published-tags.py`가 `gate == "pass"`인 이미지만 골라
|
||||
`custom-values.yaml`/`dip-values.yaml`을 패치한다(`patch-catalog-tag.py`를 내부에서
|
||||
호출) — 실패한 빌드의 태그가 반영되는 일은 없다.
|
||||
- 변경이 있으면 고정 브랜치(`build/catalog-tag-update`)로 force push까지만 하고
|
||||
멈춘다 — 매 실행 새 브랜치를 만들지 않으므로 방치된 동일 내용 브랜치가 쌓이지 않는다.
|
||||
**PR은 자동으로 열리지 않는다** — Job Summary의 compare 링크를 보고 사람이 직접 연다.
|
||||
|
||||
## 4. 참고
|
||||
|
||||
- 두 축 개념·소유 레포·문서 단일 출처 매핑: [CLAUDE.md](../CLAUDE.md) "CVE/SBOM 게이트 작업"
|
||||
- 차트 카탈로그 축 스캔 메커니즘·CI 위생(캐싱·concurrency) 상세: [doc/sbom-pipeline.md](sbom-pipeline.md)
|
||||
- 자체 빌드 축 이관 배경·계약: [doc/migrations/](migrations/)
|
||||
- 자체 빌드 드리프트 전용 워크플로를 없앤 근거: ADR [0005](decisions/0005-self-build-drift-check-removed.md)
|
||||
- 차단 CVE 대응 시 레버 판단: [.claude/skills/cve-remediation/SKILL.md](../.claude/skills/cve-remediation/SKILL.md)
|
||||
- `hardened-containers` 자체 파이프라인(빌드→검증→SBOM→스캔→게이트→push, 매일 자율
|
||||
재스캔 `rescan.yml`) 상세는 그 레포의 `docs/architecture.md`가 단일 출처다 — 이 문서는
|
||||
그 파이프라인의 트리거·출력 지점만 다룬다.
|
||||
@@ -35,7 +35,7 @@ dev 클러스터 `apisix` 네임스페이스에 이미 `data-apisix-etcd-0` PVC
|
||||
1. 패치가 멈춘 이미지는 시간이 지날수록 미수정 CVE 가 구조적으로 누적된다 —
|
||||
"CRITICAL/HIGH 0건" 을 유지 가능한 상태로 지속할 수 없다.
|
||||
2. 버전 고정 없는 `latest`-only 배포는 **롤링 태그 금지** 규칙과 애초에 양립하지 않는다
|
||||
(자체 빌드 이미지에 적용되는 이 규칙은 `hardened-containers` 레포의 `docs/image-authoring.md`
|
||||
(자체 빌드 이미지에 적용되는 이 규칙은 `hardened-containers` 레포의 `docs/image-authoring/README.md`
|
||||
가 갖는다).
|
||||
|
||||
`groundhog2k/etcd` 는 업스트림 etcd 프로젝트의 원본 이미지(`quay.io/coreos/etcd`)를 그대로
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# 0005. 자체 빌드 드리프트 스캔·재빌드 트리거를 dip-catalog에서 제거한다
|
||||
|
||||
- 날짜: 2026-08-26
|
||||
- 상태: 확정
|
||||
|
||||
## 결정
|
||||
|
||||
dip-catalog에서 자체 빌드 이미지 재빌드 트리거와 전용 드리프트 스캔을 모두 제거한다 —
|
||||
`.github/workflows/self-build-drift-check.yml`과 `scripts/build/check-rebuild-needed.py`를
|
||||
삭제한다. 자체 빌드 이미지의 CVE는 `sbom.yml`이 다른 카탈로그 이미지와 동일하게
|
||||
스캔·보고하고, 재빌드 여부·시점은 전적으로 `hardened-containers`의 `rescan.yml`이
|
||||
자율 결정한다.
|
||||
|
||||
## 배경
|
||||
|
||||
`self-build-drift-check.yml`은 두 가지를 했다: (1) 배포 중인 자체 빌드 이미지를 스캔해
|
||||
재빌드 대상을 판정하고, (2) 대상이 있으면 별도 레포 `hardened-containers`의
|
||||
`build-image.yml`을 `workflow_dispatch`로 트리거했다.
|
||||
|
||||
## 근거
|
||||
|
||||
자체 빌드 이미지의 CVE는 이미 세 겹으로 덮여 있었다.
|
||||
|
||||
1. **빌드 시점** — `hardened-containers`가 강제 zero-CVE 게이트를 통과한 이미지만 push하고,
|
||||
`published.json`은 게이트 PASS + 실제 push된 것만 기록한다. 새로 발행된 이미지는
|
||||
구조적으로 깨끗하다.
|
||||
2. **빌드 이후 드리프트** — 그 레포의 `rescan.yml`이 매일 재스캔하고 자율적으로 재빌드한다.
|
||||
dip-catalog가 별도로 트리거를 보내는 건 이 루프를 중복시킬 뿐이다. 실제로도
|
||||
`SECURITY_IMAGES_DISPATCH_TOKEN` 시크릿이 한 번도 등록된 적 없어 트리거 스텝은
|
||||
실행할 때마다 실패하는 죽은 코드였다.
|
||||
3. **dip-catalog가 실제 배포 중인 것** — `sbom.yml`이 이미 스캔한다. `extract-helm-images.sh`가
|
||||
`custom-values.yaml`을 적용해 `helm template`로 렌더하므로 `docker.io/paasup/*`가 그대로
|
||||
스캔 대상에 들어가고, `cve-gate.py`가 같은 게이트 규칙(실효 등급 `max(벤더,NVD)` ·
|
||||
승인 예외)으로 판정한다. 같은 이미지를 주 2회(일 18:00 · 월 02:00) 따로 스캔하고
|
||||
있었을 뿐이다.
|
||||
|
||||
`check-rebuild-needed.py`가 유일하게 더하던 "수정 버전이 있는가(fixable/no-fix)" 분류조차
|
||||
새 정보가 아니었다 — `cve-gate.py`의 리포트 테이블에 이미 "수정 버전" 컬럼이 있고
|
||||
`FixedVersion`으로 채워진다. 이 스크립트는 같은 findings를 자체 빌드 8개 기준으로 다시
|
||||
묶어 보여줄 뿐이었다.
|
||||
|
||||
### 탈락한 후보
|
||||
|
||||
- **(a) 현행 유지** — 트리거 + 별도 워크플로 그대로. 위 근거대로 순수 중복이라 기각.
|
||||
- **(b) 트리거만 제거, 워크플로는 스캔·보고용으로 존치** — `sbom.yml`과 같은 이미지를
|
||||
또 스캔하는 문제(주 2회 중복)가 남아 기각.
|
||||
- **(c) `check-rebuild-needed.py`를 `sbom.yml`의 리포트 스텝으로 흡수** — SBOM/리포트
|
||||
파일 명명 규칙이 두 파이프라인에서 동일해(`tr '/:@' '___'` vs `re.sub(r"[/:@]","_",ref)`)
|
||||
기술적으로는 가능했다. 하지만 `cve-gate.py` 리포트에 이미 "수정 버전" 컬럼이 있어
|
||||
흡수해도 **새 정보가 생기지 않음**을 확인해 기각했다 — 같은 findings의 재그룹핑을
|
||||
위해 스크립트와 워크플로 스텝을 유지할 이유가 없었다.
|
||||
|
||||
## 받아들인 비용
|
||||
|
||||
- 자체 빌드 8개만 좁혀 보는 전용 표가 사라진다 — `sbom.yml` 리포트에서
|
||||
`docker.io/paasup/*`를 골라 봐야 한다.
|
||||
- 자체 빌드 이미지 리포트 주기가 월요일 02:00 → 일요일 18:00(`sbom.yml` 스케줄)로
|
||||
바뀌고, 그 워크플로가 실패하면 자체 빌드 리포트도 함께 보이지 않는다.
|
||||
- dip-catalog가 옛 태그에 멈춰 있어도 스스로 재빌드를 재촉할 수단이 없다 — Job Summary
|
||||
리포트를 사람이 보고 수동으로 판단해야 한다. 단, `catalog-tag-update.yml`이 매일
|
||||
폴링해 보통 하루 안에 따라잡으므로 드문 시나리오다.
|
||||
|
||||
## 알려진 잠재 공백 (이번 결정이 만든 것이 아니라 기존부터 있던 것 — 기록만)
|
||||
|
||||
`extract-helm-images.sh`는 `custom-values.yaml`만 적용해 렌더하므로 `dip-values.yaml`에만
|
||||
있는 태그는 스캔되지 않는다. 현재 카탈로그에서는 두 파일의 paasup 태그가 일치해 실질
|
||||
영향이 없음을 확인했다(cnpg-cluster 1.0.0/1.1.0). `manifests/applicationset/**` 미스캔
|
||||
(MEMORY.md #42)과 같은 성격의 공백이다.
|
||||
|
||||
## 재검토 조건
|
||||
|
||||
- `hardened-containers`의 `rescan.yml`이 장기간 중단되거나 신뢰할 수 없게 되면 — 그 경우
|
||||
dip-catalog 쪽에 독립적인 드리프트 감시가 다시 필요해진다.
|
||||
- `dip-values.yaml`에만 존재하는 자체 빌드 태그가 실제로 생기면 — 이 결정의 전제("두
|
||||
values 파일의 paasup 태그가 일치한다")가 깨지므로 스캔 커버리지를 다시 검토해야 한다.
|
||||
@@ -9,14 +9,14 @@ CVE 건수·커버리지·패키지 버전 같은 수치는 게이트가 매 실
|
||||
| # | 결정 | 날짜 | 상태 |
|
||||
|---|---|---|---|
|
||||
| [0003](0003-etcd-chart-selection.md) | etcd 는 `groundhog2k/etcd` 단일 차트 · replicas=1 로 배포한다 | 2026-07-31 | 확정 |
|
||||
| [0005](0005-self-build-drift-check-removed.md) | 자체 빌드 드리프트 스캔·재빌드 트리거를 dip-catalog에서 제거한다(sbom.yml + hardened-containers의 rescan.yml로 대체) | 2026-08-26 | 확정 |
|
||||
|
||||
**이미지 자체 빌드 ADR(0001·0002·0004)은 별도 레포 `hardened-containers` 로 이관됐다** —
|
||||
그 이미지들의 빌드 정의와 함께 그 레포의 `docs/decisions/` 에 있다. 카탈로그가 무엇을
|
||||
배포하는가(차트)와 그 이미지를 어떻게 만드는가(자체 빌드)가 다른 레포에 놓이며 근거도
|
||||
함께 옮긴 것이다. 이관 배경은
|
||||
[doc/migrations/self-build-images-to-hardened-containers.md](../migrations/self-build-images-to-hardened-containers.md).
|
||||
번호가 0003 하나만 남아 이어지지 않아 보이지만, 다른 세 번호가 예약돼 있던 것일 뿐이고
|
||||
새 ADR 은 0005 부터 잇는다.
|
||||
0004까지는 그 세 번호가 예약돼 있던 것이고, 새 ADR 은 0006 부터 잇는다.
|
||||
|
||||
## 이 문서들의 출처
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
| `scripts/build/suggest-go-upgrades.py` | `--apply`/`--dry-run` 신규 추가 |
|
||||
| `scripts/pipeline/scan-sbom.sh` · `cve-gate.py` (이미지 판정 부분만) | `scripts/gate/scan-image.sh` · `image-gate.py` (슬림화) |
|
||||
| `.github/workflows/build-image.yml` | 카탈로그 무의존으로 재구성(`catalog.env` 의존 제거, drift 모드 제거) |
|
||||
| `.claude/image-authoring.md` | `docs/image-authoring.md` (정책 중심으로 재작성) |
|
||||
| `.claude/image-authoring.md` | `docs/image-authoring.md` (정책 중심으로 재작성, 이후 `docs/image-authoring/`로 추가 분할됨) |
|
||||
| `doc/decisions/0001·0002·0004`(이미지 ADR) | `docs/decisions/`(같은 번호 유지) |
|
||||
| `.claude/skills/self-build-image/` | 그대로(문서 경로만 수정) |
|
||||
|
||||
@@ -38,12 +38,16 @@
|
||||
| 경로 | 역할 |
|
||||
|---|---|
|
||||
| `catalog/image-map/<image>.env` | 자체 빌드 이미지 → 어느 차트의 어느 필드(`CHART_DIRS`·`TAG_STYLE`·`TAG_BLOCK`). 옛 `images/<image>/catalog.env` 의 카탈로그 레이아웃 정보만 뗀 것 |
|
||||
| `scripts/build/check-rebuild-needed.py` | 드리프트 탐지(A 파트만) — 배포 중인 이미지가 새 CVE 로 규정을 벗어났는가. 핀 판단(B 파트)은 제거했다 — `hardened-containers` 의 `suggest-go-upgrades.py` 가 갖는다 |
|
||||
| `scripts/build/apply-published-tags.py` | `hardened-containers` 의 `published.json` 을 카탈로그 values 에 반영 |
|
||||
| `scripts/build/patch-catalog-tag.py` | 변경 없음(원래도 카탈로그 파일만 다루는 순수 도구였다) |
|
||||
| `.github/workflows/self-build-drift-check.yml` | 주간, 배포 중인 이미지 스캔 → 재빌드 필요하면 `hardened-containers` 의 `build-image.yml` 을 `workflow_dispatch` 로 트리거만 |
|
||||
| `.github/workflows/catalog-tag-update.yml` | 매일, `published.json` 조회 → 카탈로그 values 패치 → 브랜치 push |
|
||||
|
||||
> 드리프트 탐지 전용 스크립트(`scripts/build/check-rebuild-needed.py`)와 그것을 주간으로
|
||||
> 돌리던 `.github/workflows/self-build-drift-check.yml`은 이후 제거됐다 — `sbom.yml`이
|
||||
> 자체 빌드 이미지도 다른 카탈로그 이미지와 동일하게 스캔·보고하고, 재빌드는
|
||||
> `hardened-containers`의 `rescan.yml`이 전적으로 자율 수행하므로 별도 경로가 불필요했다.
|
||||
> 근거는 ADR [0005](../decisions/0005-self-build-drift-check-removed.md).
|
||||
|
||||
## 계약 — `published.json` 하나뿐
|
||||
|
||||
`hardened-containers` 는 **이 카탈로그를 모른다.** 게이트 PASS + 레지스트리 push 가 실제로
|
||||
@@ -81,13 +85,6 @@
|
||||
|
||||
## 아직 안 된 것
|
||||
|
||||
- **`SECURITY_IMAGES_DISPATCH_TOKEN` 시크릿 미등록.** `self-build-drift-check.yml` 이
|
||||
`hardened-containers` 의 `build-image.yml` 을 `workflow_dispatch` 로 부르려면
|
||||
그 레포에 `workflow_dispatch` 권한이 있는 PAT 이 필요하다. 등록 전까지 트리거 스텝은
|
||||
명시적으로 실패한다.
|
||||
- **`hardened-containers` 레포 자체가 아직 GitHub 에 없다.** 로컬 레포만 있는 상태에서
|
||||
이 이관을 진행했다 — GitHub 레포 생성·push, `DOCKERHUB_USER`/`DOCKERHUB_TOKEN` 시크릿
|
||||
등록이 선행돼야 위 두 워크플로가 실제로 동작한다.
|
||||
- **`published.json` 의 `digest` 필드 대부분 공란.** `hardened-containers` 가 아직 실제로
|
||||
이미지를 재빌드·push 하지 않아서다. 다음 빌드가 채운다 — MEMORY.md 의 "카탈로그 태그가
|
||||
클러스터보다 앞서 있다" 항목이 이 필드를 쓸 계획이다.
|
||||
|
||||
+13
-7
@@ -12,8 +12,8 @@ GitHub Actions([.github/workflows/sbom.yml](../.github/workflows/sbom.yml))로
|
||||
|---|---|---|
|
||||
| 질문 | 우리가 배포하는 이미지에 무엇이 있는가 | 그 이미지를 어떻게 만드는가 |
|
||||
| 실행 위치 | 이 레포 | 별도 레포 `hardened-containers` |
|
||||
| 워크플로 | `sbom.yml` · `cve-edge-post.yml` | `hardened-containers` 의 `build-image.yml`(이 레포에서는 `self-build-drift-check.yml` 이 필요할 때 그것을 부른다) |
|
||||
| 단일 출처 | **이 문서** | `hardened-containers` 레포의 `docs/image-authoring.md` |
|
||||
| 워크플로 | `sbom.yml` · `cve-edge-post.yml` | `hardened-containers` 의 `build-image.yml`·`rescan.yml`(이 레포는 트리거하지 않는다) |
|
||||
| 단일 출처 | **이 문서** | `hardened-containers` 레포의 `docs/image-authoring/README.md` |
|
||||
|
||||
**카탈로그 values 가 자체 빌드 이미지(`docker.io/paasup/*`)를 가리키므로 그 이미지도 이 문서의
|
||||
스캔 대상이다** — 축이 갈린 것은 "누가 만들고 결정하는가" 이고 "누가 스캔되는가" 가 아니다.
|
||||
@@ -137,6 +137,12 @@ docker buildx build --platform linux/amd64 \
|
||||
> python 으로 갖고 있어 **승인 예외(`cve-exceptions.json`)도 실효 등급(`max(벤더,NVD)`)도
|
||||
> 적용하지 않는다.** 같은 스캔 데이터에서 다른 숫자가 나올 수 있다.
|
||||
|
||||
두 워크플로 모두 날짜 단위 키(`trivy-db-<YYYYMMDD>`)로 trivy 취약점 DB를 `actions/cache`에
|
||||
캐싱한다 — 실패해도 워크플로를 막지 않는다(`continue-on-error`, 캐시는 최적화일 뿐 정확성
|
||||
요건이 아니다). `concurrency` 그룹도 있다 — `sbom.yml`은 `sbom-${{ github.ref }}`(PR은
|
||||
새 커밋이 오면 이전 실행 취소, 스케줄은 취소 안 함), `cve-edge-post.yml`은 `cve-edge-post`
|
||||
(항상 직렬화, 외부 POST 잡이라 취소 대신 대기).
|
||||
|
||||
**PR 을 실제로 막는 게이트는 이 축에 없다** — `sbom.yml` 은 warn-only 다. 강제 게이트는
|
||||
자체 빌드 축(`hardened-containers` 레포의 `images/**` PR)에만 있다.
|
||||
|
||||
@@ -208,11 +214,11 @@ Repo Secret `CVE_API_KEY`).
|
||||
## 자체 빌드 축은 이 문서가 다루지 않는다
|
||||
|
||||
게이트가 상위 태그 교체·베이스 OS 교체로 해소되지 않는 차단 CVE 를 찾으면 자체 빌드로 간다.
|
||||
그 축은 **별도 레포 `hardened-containers`** 가 갖는다 — 빌드·검증·게이트·push 전부 그 레포
|
||||
안에서 이루어지고, 단일 출처는 그 레포의 `docs/image-authoring.md` 다. 이 카탈로그에는
|
||||
"어느 차트가 그 이미지를 가리키는가"(`catalog/image-map/`)와 드리프트 탐지
|
||||
(`scripts/build/check-rebuild-needed.py`, 주간 `self-build-drift-check.yml`)만 남아 있다 —
|
||||
이관 배경은 [doc/migrations/](migrations/self-build-images-to-hardened-containers.md).
|
||||
그 축은 **별도 레포 `hardened-containers`** 가 갖는다 — 빌드·검증·게이트·push·드리프트 재빌드
|
||||
전부 그 레포 안에서 이루어지고(`rescan.yml`이 매일 자율 재스캔·재빌드), 단일 출처는 그
|
||||
레포의 `docs/image-authoring/README.md` 다. 이 카탈로그에는 "어느 차트가 그 이미지를
|
||||
가리키는가"(`catalog/image-map/`)만 남아 있다 — 이관 배경은
|
||||
[doc/migrations/](migrations/self-build-images-to-hardened-containers.md).
|
||||
|
||||
이 문서가 알아야 할 것은 하나뿐이다: **자체 빌드 이미지도 카탈로그가 가리키는 한 위 스캔·게이트
|
||||
대상이다.** 실제로 `docker.io/paasup/*` 가 게이트 리포트에 등장한다.
|
||||
|
||||
@@ -10,7 +10,7 @@ postgresql:
|
||||
# (이 레포에는 없다).
|
||||
#
|
||||
# 태그에 빌드일을 포함한다. 같은 앱 버전이라도 베이스 업데이트 결과가 시점마다 다르므로
|
||||
# 롤링 태그를 쓰지 않는다(hardened-containers 레포의 docs/image-authoring.md).
|
||||
# 롤링 태그를 쓰지 않는다(hardened-containers 레포의 docs/image-authoring/README.md).
|
||||
imageName: "docker.io/paasup/cnpg-postgresql:18.4-bci15.7-hardened-20260820"
|
||||
#
|
||||
# trivy 는 SLES 15.7 을 정상 커버한다 — 실효 C/H 0/0 은 측정된 결과이며 게이트 PASS 다.
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check-rebuild-needed.py — 배포 중인 자체 빌드 이미지가 재빌드로 고쳐지는지 판정한다.
|
||||
|
||||
왜 필요한가
|
||||
-----------
|
||||
자체 빌드 이미지는 **소스를 안 바꿔도 시간이 지나면 게이트를 벗어난다.** 새 CVE 가 공개되고,
|
||||
베이스 OS 패키지가 갱신되고, 툴체인 패치가 나오는 동안 우리가 참조해 둔 태그는 그대로다.
|
||||
의도적으로 보는 장치가 없으면 문서 전용 PR 이 우연히 검증 빌드를 돌릴 때만 발견된다.
|
||||
|
||||
`self-build-drift-check.yml` 이 주간으로 이것을 돌린다.
|
||||
|
||||
무엇이 "재빌드로 고쳐지는가" 인가
|
||||
--------------------------------
|
||||
"핀이 뒤처진 경우" 만으로는 부족하다. 재빌드로 고쳐지는 경우는 셋이다.
|
||||
|
||||
1. 핀이 뒤처졌다 → 핀을 올리고 빌드
|
||||
2. 핀은 맞는데 이미지가 그 핀으로 안 빌드됐다 → 그냥 빌드
|
||||
3. 베이스 OS 패키지가 뒤처졌다 → 그냥 빌드 (재빌드하면 zypper 가 최신을 깐다)
|
||||
|
||||
그래서 트리거는 **"수정 버전이 있는 차단 CVE 가 있는가"** 다. 이 스크립트는 그 판정만
|
||||
한다 — 핀을 무엇으로 올려야 하는지는 판단하지 않는다(아래 "레포 분리" 참고).
|
||||
|
||||
수정 버전이 없는 차단(`affected`·`will_not_fix`)은 재빌드해도 그대로이므로 트리거가 아니다 —
|
||||
`no-fix` 로 따로 보고해 사람이 다른 레버(태그 교체·예외 승인)를 판단하게 한다.
|
||||
|
||||
판정 규칙은 게이트와 같은 출처를 쓴다
|
||||
-----------------------------------
|
||||
실효 등급 `max(벤더, NVD)` · 승인 예외(`doc/cve-exceptions.json`) 적용 — 전부
|
||||
`scripts/pipeline/cve-gate.py` 의 함수를 그대로 가져다 쓴다. 규칙이 두 곳으로 갈리면
|
||||
"게이트는 PASS 인데 재빌드하라고 한다" 가 나온다.
|
||||
|
||||
기준은 카탈로그가 실제로 가리키는 이미지다
|
||||
----------------------------------------
|
||||
catalog/image-map/<image>.env → 카탈로그 values 의 현재 ref
|
||||
→ 그 ref 의 스캔 리포트
|
||||
|
||||
재빌드해 보지 않고도 "지금 배포 중인 것이 규정을 벗어났는가" 를 답한다.
|
||||
|
||||
레포 분리 이후 — 이 스크립트는 절반이다
|
||||
----------------------------------------
|
||||
커스텀 이미지는 hardened-containers(별도 레포)로 나갔다. "무엇을 배포 중인가"는 이 카탈로그가
|
||||
알고, "그 이미지를 어떻게 만드는가"(핀 판단·재빌드)는 hardened-containers 가 안다. 이 스크립트는
|
||||
**탐지만** 한다 — 재빌드가 필요하다고 판단되면 `self-build-drift-check.yml` 이
|
||||
hardened-containers 의 `build-image.yml` 을 `workflow_dispatch` 로 부른다. 핀을 무엇으로
|
||||
올릴지는 그 레포의 `suggest-go-upgrades.py --apply` 가 한다.
|
||||
|
||||
사용
|
||||
----
|
||||
python3 scripts/build/check-rebuild-needed.py --list-refs # 무엇을 스캔해야 하나
|
||||
python3 scripts/build/check-rebuild-needed.py --reports <디렉토리>
|
||||
|
||||
종료 코드
|
||||
0 정상 (재빌드 대상이 있어도 0 — --fail-on-drift 를 주면 1)
|
||||
1 재빌드 대상 있음 + --fail-on-drift
|
||||
2 실행 오류
|
||||
"""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.normpath(os.path.join(HERE, "..", ".."))
|
||||
IMAGE_MAP_DIR = os.path.join(ROOT, "catalog", "image-map")
|
||||
DEFAULT_EXCEPTIONS = os.path.join(ROOT, "doc", "cve-exceptions.json")
|
||||
|
||||
# 카탈로그 values 는 이 순서로 찾는다. 자체 빌드 태그는 custom-values.yaml 이 기준이고
|
||||
# dip-values.yaml 은 같은 값을 쓰는 배포 파라미터화용이다.
|
||||
VALUES_CANDIDATES = ("custom-values.yaml", "dip-values.yaml")
|
||||
|
||||
|
||||
def load(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
PATCH = load(os.path.join(HERE, "patch-catalog-tag.py"), "patch_catalog_tag")
|
||||
GATE = load(os.path.join(HERE, "..", "pipeline", "cve-gate.py"), "cve_gate")
|
||||
|
||||
|
||||
def read_env(path):
|
||||
"""`KEY=value` 셸 할당만 읽는다. 값의 따옴표는 벗긴다."""
|
||||
out = {}
|
||||
if not os.path.isfile(path):
|
||||
return out
|
||||
for line in pathlib.Path(path).read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
if not re.match(r"^[A-Z_][A-Z0-9_]*$", k):
|
||||
continue
|
||||
out[k] = v.strip().strip('"').strip("'")
|
||||
return out
|
||||
|
||||
|
||||
def report_path(reports_dir, image_ref):
|
||||
"""이미지 ref → 리포트 파일 경로. `trivy image <ref>` 스캔 결과를
|
||||
`tr '/:@' '___'` 로 만든 파일명으로 저장했다고 가정한다."""
|
||||
return os.path.join(reports_dir, re.sub(r"[/:@]", "_", image_ref) + ".json")
|
||||
|
||||
|
||||
def resolve_current_ref(chart_env):
|
||||
"""카탈로그 values 에서 현재 이미지 ref 를 읽는다. (ref, 읽은 파일) 또는 (None, 이유)."""
|
||||
chart_dirs = (chart_env.get("CHART_DIRS") or "").split()
|
||||
style = chart_env.get("TAG_STYLE") or ""
|
||||
block = chart_env.get("TAG_BLOCK") or "image"
|
||||
if not chart_dirs or style not in ("imageName", "split"):
|
||||
return None, "catalog/image-map/<image>.env 에 CHART_DIRS/TAG_STYLE 이 없다"
|
||||
for d in chart_dirs:
|
||||
for fname in VALUES_CANDIDATES:
|
||||
p = os.path.join(ROOT, d, fname)
|
||||
if not os.path.isfile(p):
|
||||
continue
|
||||
text = pathlib.Path(p).read_text()
|
||||
ref = (PATCH.read_image_name(text) if style == "imageName"
|
||||
else PATCH.read_split_block(text, block))
|
||||
if ref:
|
||||
return ref, os.path.relpath(p, ROOT)
|
||||
return None, f"{chart_dirs} 에서 태그를 읽지 못했다 (style={style}, block={block})"
|
||||
|
||||
|
||||
def blocking_cves(report, image_ref, rank, floor, exceptions):
|
||||
"""실효 등급 floor 이상 + 승인 예외 제외한 고유 CVE.
|
||||
|
||||
반환: {cve: {"pkg", "sev", "fixable"}}
|
||||
"""
|
||||
out = {}
|
||||
for res in report.get("Results") or []:
|
||||
for v in res.get("Vulnerabilities") or []:
|
||||
cve = v.get("VulnerabilityID")
|
||||
if not cve:
|
||||
continue
|
||||
eff = GATE.effective_severity(
|
||||
v.get("Severity"),
|
||||
GATE.nvd_severity(((v.get("CVSS") or {}).get("nvd") or {}).get("V3Score")),
|
||||
)
|
||||
if rank.get(eff, 0) < floor:
|
||||
continue
|
||||
if any(GATE.exception_applies(e, cve, image_ref) for e in exceptions):
|
||||
continue
|
||||
prev = out.get(cve)
|
||||
fixable = bool(v.get("FixedVersion"))
|
||||
if prev is None:
|
||||
out[cve] = {"pkg": v.get("PkgName"), "sev": eff, "fixable": fixable}
|
||||
elif fixable:
|
||||
prev["fixable"] = True
|
||||
return out
|
||||
|
||||
|
||||
def check_image(image, reports_dir, rank, floor, exceptions):
|
||||
res = {"image": image, "status": "ok", "notes": [], "blocking": 0, "fixable": 0}
|
||||
|
||||
chart_env = read_env(os.path.join(IMAGE_MAP_DIR, f"{image}.env"))
|
||||
ref, where = resolve_current_ref(chart_env)
|
||||
if not ref:
|
||||
res["status"] = "skip"
|
||||
res["notes"].append(where)
|
||||
return res
|
||||
res["ref"] = ref
|
||||
res["ref_from"] = where
|
||||
|
||||
rp = report_path(reports_dir, ref)
|
||||
if not os.path.isfile(rp):
|
||||
res["status"] = "no-report"
|
||||
res["notes"].append("이 ref 의 스캔 리포트가 없다 — 스캔 대상에 없었거나 태그가 갱신됐다")
|
||||
return res
|
||||
|
||||
with open(rp) as f:
|
||||
report = json.load(f)
|
||||
cves = blocking_cves(report, ref, rank, floor, exceptions)
|
||||
fixable = {c: d for c, d in cves.items() if d["fixable"]}
|
||||
res["blocking"] = len(cves)
|
||||
res["fixable"] = len(fixable)
|
||||
res["packages"] = sorted({d["pkg"] for d in cves.values() if d["pkg"]})
|
||||
|
||||
if not cves:
|
||||
return res # ok
|
||||
|
||||
if fixable:
|
||||
res["status"] = "rebuild"
|
||||
else:
|
||||
res["status"] = "no-fix"
|
||||
res["notes"].append("차단 CVE 에 수정 버전이 없다 — 재빌드로 해소되지 않는다. "
|
||||
"상위 태그 교체나 예외 승인을 검토한다")
|
||||
return res
|
||||
|
||||
|
||||
def render_md(results):
|
||||
rebuild = [r for r in results if r["status"] == "rebuild"]
|
||||
nofix = [r for r in results if r["status"] == "no-fix"]
|
||||
ok = [r for r in results if r["status"] == "ok"]
|
||||
out = ["## 🔁 자체 빌드 이미지 재빌드 판정", ""]
|
||||
|
||||
if not rebuild and not nofix:
|
||||
out.append(f"배포 중인 자체 빌드 이미지 {len(ok)}개에 차단 CVE 가 없다.")
|
||||
if rebuild:
|
||||
out += [f"**{len(rebuild)}개 이미지가 재빌드 대상이다.** 차단 CVE 에 수정 버전이 있다 "
|
||||
"— hardened-containers 레포에서 재빌드하면 해소될 가능성이 높다. 핀 조정이 "
|
||||
"필요한지는 그 레포의 `suggest-go-upgrades.py` 가 판단한다.", "",
|
||||
"| 이미지 | 현재 ref | 차단 | 수정 가능 | 패키지 |",
|
||||
"|---|---|---:|---:|---|"]
|
||||
for r in rebuild:
|
||||
pkgs = ", ".join(f"`{p}`" for p in r["packages"][:4])
|
||||
out.append(f"| `{r['image']}` | `{r['ref']}` | {r['blocking']} | {r['fixable']} | {pkgs} |")
|
||||
if nofix:
|
||||
out += ["", f"**{len(nofix)}개 이미지는 재빌드로 해소되지 않는다.** 다른 레버가 필요하다.",
|
||||
"", "| 이미지 | 차단 | 패키지 |", "|---|---:|---|"]
|
||||
for r in nofix:
|
||||
out.append(f"| `{r['image']}` | {r['blocking']} | "
|
||||
f"{', '.join(f'`{p}`' for p in r['packages'][:4])} |")
|
||||
|
||||
notes = [(r["image"], n) for r in results for n in r["notes"]
|
||||
if r["status"] in ("rebuild", "no-fix")]
|
||||
if notes:
|
||||
out += ["", "**수동 확인 필요**", ""]
|
||||
out += [f"- `{img}` — {n}" for img, n in notes]
|
||||
|
||||
skipped = [r for r in results if r["status"] in ("skip", "no-report")]
|
||||
if skipped:
|
||||
out += ["", f"<details><summary>판정하지 않은 이미지 {len(skipped)}개</summary>", ""]
|
||||
out += [f"- `{r['image']}` — {'; '.join(r['notes'])}" for r in skipped]
|
||||
out += ["", "</details>"]
|
||||
|
||||
out += ["", "> 판정 기준은 게이트와 같다 — 실효 등급 `max(벤더, NVD)` · 승인 예외 적용 · "
|
||||
"기본 HIGH 이상. 재빌드는 hardened-containers 레포의 `build-image.yml` 을 "
|
||||
"`workflow_dispatch` 로 부른다.", ""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="배포 중인 자체 빌드 이미지의 재빌드 필요 여부 판정")
|
||||
ap.add_argument("--reports", help="trivy 리포트(JSON) 디렉토리 (--list-refs 면 불필요)")
|
||||
ap.add_argument("--image", default="", help="이 이미지만 판정 (catalog/image-map/<image>.env)")
|
||||
ap.add_argument("--exceptions", default=DEFAULT_EXCEPTIONS,
|
||||
help="승인 예외 목록 (기본 doc/cve-exceptions.json)")
|
||||
ap.add_argument("--min-severity", default="HIGH",
|
||||
choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"],
|
||||
help="이 등급 이상만 차단으로 본다 (기본 HIGH — 게이트와 동일)")
|
||||
ap.add_argument("--list-refs", action="store_true",
|
||||
help="`이미지<TAB>ref<TAB>리포트파일명` 만 출력하고 종료. CI 가 무엇을 "
|
||||
"스캔해야 하는지 알아야 할 때 쓴다 — ref 해석 규칙이 워크플로로 "
|
||||
"새지 않게 한다")
|
||||
ap.add_argument("--summary-md", metavar="FILE", help="마크다운 요약을 이 파일에 쓴다")
|
||||
ap.add_argument("--json-out", metavar="FILE", help="결과 JSON 을 이 파일에 쓴다")
|
||||
ap.add_argument("--fail-on-drift", action="store_true",
|
||||
help="재빌드 대상이 있으면 1 로 종료 (기본은 보고만 하고 0)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.isdir(IMAGE_MAP_DIR):
|
||||
print(f"::error::catalog/image-map/ 를 찾지 못했다: {IMAGE_MAP_DIR}", file=sys.stderr)
|
||||
return 2
|
||||
if not args.list_refs and not args.reports:
|
||||
print("::error::--reports 가 필요하다", file=sys.stderr)
|
||||
return 2
|
||||
if args.reports and not os.path.isdir(args.reports):
|
||||
print(f"::error::리포트 디렉토리가 없다: {args.reports}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
names = sorted(f[:-4] for f in os.listdir(IMAGE_MAP_DIR) if f.endswith(".env"))
|
||||
if args.image:
|
||||
names = [n for n in names if n == args.image]
|
||||
if not names:
|
||||
print(f"::error::catalog/image-map/{args.image}.env 가 없다", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.list_refs:
|
||||
for n in names:
|
||||
ref, why = resolve_current_ref(read_env(os.path.join(IMAGE_MAP_DIR, f"{n}.env")))
|
||||
if not ref:
|
||||
print(f"::warning::{n}: ref 를 읽지 못했다 — {why}", file=sys.stderr)
|
||||
continue
|
||||
print(f"{n}\t{ref}\t{os.path.basename(report_path('', ref))}")
|
||||
return 0
|
||||
|
||||
rank = GATE.RANK
|
||||
floor = rank[args.min_severity]
|
||||
# load_exceptions 는 (유효, 만료) 를 낸다. 만료된 예외는 게이트가 인정하지 않으므로
|
||||
# 여기서도 인정하지 않는다 — 재검토를 강제하는 것이 그 설계의 의도다.
|
||||
exceptions, expired = GATE.load_exceptions(args.exceptions)
|
||||
if expired:
|
||||
print(f"::warning::만료된 예외 {len(expired)}건은 인정하지 않는다 — "
|
||||
f"{', '.join(e.get('id', '?') for e in expired)}")
|
||||
|
||||
results = [check_image(n, args.reports, rank, floor, exceptions) for n in names]
|
||||
|
||||
for r in results:
|
||||
if r["status"] == "rebuild":
|
||||
print(f"::warning::{r['image']}: 재빌드 대상 — 차단 {r['blocking']}건"
|
||||
f"(수정 가능 {r['fixable']})")
|
||||
elif r["status"] == "no-fix":
|
||||
print(f"::warning::{r['image']}: 차단 {r['blocking']}건이나 수정 버전 없음 — "
|
||||
"재빌드로 해소되지 않는다")
|
||||
for n in r["notes"]:
|
||||
if r["status"] in ("rebuild", "no-fix"):
|
||||
print(f"::warning::{r['image']}: {n}")
|
||||
|
||||
md = render_md(results)
|
||||
print()
|
||||
print(md)
|
||||
|
||||
if args.summary_md:
|
||||
pathlib.Path(args.summary_md).write_text(md + "\n")
|
||||
if args.json_out:
|
||||
pathlib.Path(args.json_out).write_text(
|
||||
json.dumps(results, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
return 1 if ([r for r in results if r["status"] == "rebuild"] and args.fail_on_drift) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""카탈로그 values 파일의 자체 빌드 이미지 태그를 갱신한다.
|
||||
|
||||
check-rebuild-needed.py / apply-published-tags.py 가 `catalog-tag-update.yml` 경유로
|
||||
호출한다. 카탈로그가 실제로 쓰는 두 표기 스타일을 지원한다:
|
||||
apply-published-tags.py 가 `catalog-tag-update.yml` 경유로 호출한다. 카탈로그가 실제로
|
||||
쓰는 두 표기 스타일을 지원한다:
|
||||
- imageName: 단일 필드 문자열 (cnpg-cluster 유형 — `imageName: "repo:tag"`)
|
||||
- split: image:/initImage: 등 블록 아래 registry/repository/tag 세 필드로 분리
|
||||
(cloudnative-pg, etcd 유형)
|
||||
|
||||
@@ -85,8 +85,7 @@ def nvd_severity(score):
|
||||
def effective_severity(vendor_sev, nvd_sev):
|
||||
"""실효 등급 = max(벤더 등급, NVD 등급). 벤더의 하향 등급으로 게이트를
|
||||
통과하는 것을 막는다. 이 게이트가 심각도 규칙의 단일 출처다 —
|
||||
`check-rebuild-needed.py`(드리프트 탐지)와 자체 빌드 레포의 `image-gate.py`
|
||||
가 이 함수를 그대로 재사용한다."""
|
||||
자체 빌드 레포의 `image-gate.py`가 이 함수를 그대로 재사용한다."""
|
||||
vend = vendor_sev or "UNKNOWN"
|
||||
nvd = nvd_sev or "UNKNOWN"
|
||||
return vend if RANK.get(vend, 0) >= RANK.get(nvd, 0) else nvd
|
||||
|
||||
Reference in New Issue
Block a user