5.4 KiB
VD-09: 공급망 inventory, license, vulnerability, SBOM과 provenance
- 상태: Accepted
- 결정일: 2026-07-26
- 적용 브랜치:
feature-frontend-supply-chain-verification - 재검토: 조직 vulnerability scanner, signing/attestation provider와 dependency exception 승인 체계가 선정될 때
배경
기존 release script는 package.json의 직접 dependency 이름과 버전, lockfile
전체 digest, dist checksum만 기록했다. 전이 dependency, 패키지별 integrity와
license, 실제 baseline diff가 없었고 highRiskUnreviewed: []는 계산 결과가 아닌
고정값이었다. secret scan도 src와 dist만 검사해 config, scripts, test와
generated release metadata를 놓쳤다.
반면 저장소에는 조직이 선택한 vulnerability source, severity exception 승인자, signing identity와 attestation 저장소가 없다. 외부 provider가 없는 상태를 빈 finding과 서명 성공으로 표현하면 local 검증과 release promotion을 혼동한다.
결정
pnpm-lock.yaml의 모든packagesrow와pnpm list --depth Infinity의 실제 graph를 결합해 직접/전이, production/development, required/platform-optional, version, SHA-512 SRI, license와 dependency edge를 기록한다.- inventory row 수는 lockfile package row 수와 같아야 한다. 누락된 전이
dependency, malformed integrity와 non-optional
NOASSERTION은 local gate를 실패시킨다. - license는 설치된 package manifest에서 읽고 closed allow/deny policy로
검사한다. 현재 OS에 materialize되지 않은 platform optional만
NOASSERTION과 그 이유를 명시적으로 허용한다. - 승인 dependency baseline과 approval digest를 보존하고 현재 lock inventory와 actual add/remove/change/upgrade diff를 계산한다. 새 direct production dependency는 owner와 서로 다른 reviewer, reason과 rollback evidence가 필요하다.
- inventory를 CycloneDX 1.6 SBOM으로 투영한다. component 수, lockfile digest, SRI, license와 dependency edge가 inventory와 일치해야 한다.
- local in-toto/SLSA 형태 provenance statement는 source set, lockfile, SBOM과
distdigest를 연결하되LOCAL_UNSIGNED로 표시한다. 외부 attestation은 provider, signer와 동일 dist subject digest가 있어야 한다. - vulnerability adapter는
VULNERABILITY_REPORT_PATH가 가리키는 machine-readable provider report를 검증한다. report의 lock digest, provider, severity와 exception owner/reviewer/reason/expiry가 유효해야 한다. - provider report가 없으면 local inventory/license/SBOM/coherence는
PASS, promotion은FAIL_UNVERIFIED다. 빈 finding을 만들어 vulnerability PASS로 표시하지 않는다. - secret scan은 source, scripts, tests, tracked config/schema, public,
dist와 generated release metadata를 검사한다. allowlist는 test path에만 허용하며 owner, reason과 expiry가 필요하다. 발견한 secret 원문은 artifact에 쓰지 않고 rule, path, line과 fingerprint만 남긴다. SOURCE_DATE_EPOCH를 지원하고 같은 source/lock/config의 production build를 두 번 실행해 전체 dist digest 일치를 검증한 뒤 일반 build를 복원한다.
실행 경계와 증적
package.json + frozen pnpm-lock.yaml + installed graph
-> deterministic dependency inventory
-> license policy + approved actual baseline diff
-> CycloneDX SBOM
source/config/lock + production dist
-> local provenance statement
-> optional vulnerability/attestation provider inputs
-> LOCAL PASS | promotion PASS/FAIL_UNVERIFIED
- policy:
config/security/ - generator:
scripts/generate-supply-chain.ts - coherence:
scripts/verify-supply-chain-artifacts.ts - secret scan:
scripts/security-scan.ts - reproducibility:
scripts/verify-reproducible-build.ts - inventory:
artifacts/release/dependency-inventory.json - SBOM/provenance:
artifacts/release/sbom.cdx.json,artifacts/release/provenance.json - local/promotion status:
artifacts/security/supply-chain-verification.json
검증
- 현재 lockfile의 561개 package row와 inventory row가 양방향 일치한다.
- ordering-only digest, removal, integrity tamper, baseline tamper, high-risk self approval, denied license, critical vulnerability와 만료 exception, provider/digest 오류, SBOM/provenance 불일치 fixture를 검사한다.
- synthetic provider/attestation fixture는 promotion
PASS를 증명한 후 기본FAIL_UNVERIFIED상태를 복원한다. - frozen install은 manifest/lock mismatch fixture를 실제 pnpm으로 거절한다.
- source/config/dist 각각의 synthetic secret fixture가 실제 scan을 실패시키고 scoped test allowlist만 통과한다.
한계와 재검토 조건
로컬 manifest license는 법률 검토가 아니며 vulnerability report도 외부 scanner가 제공한 데이터의 최신성 자체를 보증하지 않는다. 실제 프로젝트는 provider 버전, database freshness, network outage, exception 승인 조직, signing identity, attestation transparency/retention과 비밀 관리를 결정해야 한다.
Rollback
외부 scanner/attestor adapter는 환경 입력을 제거하면 즉시
FAIL_UNVERIFIED로 돌아간다. local inventory, lock integrity, license, SBOM,
secret, reproducibility와 actual diff gate는 유지한다. scanner 장애를 이유로
promotion을 PASS로 변경하지 않는다.