feat: verify frontend supply chain
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# 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을 혼동한다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. `pnpm-lock.yaml`의 모든 `packages` row와 `pnpm list --depth Infinity`의 실제
|
||||
graph를 결합해 직접/전이, production/development, required/platform-optional,
|
||||
version, SHA-512 SRI, license와 dependency edge를 기록한다.
|
||||
2. inventory row 수는 lockfile package row 수와 같아야 한다. 누락된 전이
|
||||
dependency, malformed integrity와 non-optional `NOASSERTION`은 local gate를
|
||||
실패시킨다.
|
||||
3. license는 설치된 package manifest에서 읽고 closed allow/deny policy로
|
||||
검사한다. 현재 OS에 materialize되지 않은 platform optional만
|
||||
`NOASSERTION`과 그 이유를 명시적으로 허용한다.
|
||||
4. 승인 dependency baseline과 approval digest를 보존하고 현재 lock inventory와
|
||||
actual add/remove/change/upgrade diff를 계산한다. 새 direct production
|
||||
dependency는 owner와 서로 다른 reviewer, reason과 rollback evidence가
|
||||
필요하다.
|
||||
5. inventory를 CycloneDX 1.6 SBOM으로 투영한다. component 수, lockfile digest,
|
||||
SRI, license와 dependency edge가 inventory와 일치해야 한다.
|
||||
6. local in-toto/SLSA 형태 provenance statement는 source set, lockfile, SBOM과
|
||||
`dist` digest를 연결하되 `LOCAL_UNSIGNED`로 표시한다. 외부 attestation은
|
||||
provider, signer와 동일 dist subject digest가 있어야 한다.
|
||||
7. vulnerability adapter는 `VULNERABILITY_REPORT_PATH`가 가리키는
|
||||
machine-readable provider report를 검증한다. report의 lock digest, provider,
|
||||
severity와 exception owner/reviewer/reason/expiry가 유효해야 한다.
|
||||
8. provider report가 없으면 local inventory/license/SBOM/coherence는 `PASS`,
|
||||
promotion은 `FAIL_UNVERIFIED`다. 빈 finding을 만들어 vulnerability PASS로
|
||||
표시하지 않는다.
|
||||
9. 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만 남긴다.
|
||||
10. `SOURCE_DATE_EPOCH`를 지원하고 같은 source/lock/config의 production build를
|
||||
두 번 실행해 전체 dist digest 일치를 검증한 뒤 일반 build를 복원한다.
|
||||
|
||||
## 실행 경계와 증적
|
||||
|
||||
```text
|
||||
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.mjs`
|
||||
- coherence: `scripts/verify-supply-chain-artifacts.mjs`
|
||||
- secret scan: `scripts/security-scan.mjs`
|
||||
- reproducibility: `scripts/verify-reproducible-build.mjs`
|
||||
- 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로 변경하지 않는다.
|
||||
@@ -39,16 +39,15 @@ recovery 계약, 제거 가능한 reference 수직 슬라이스, form/page, desi
|
||||
i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다. 현재 선행 해결
|
||||
대상은 다음과 같다.
|
||||
|
||||
1. 공급망의 transitive inventory/license/vulnerability/SBOM/provenance 검증
|
||||
2. optional adapter의 opt-in 경계와 제거 가능한 recipe
|
||||
1. optional adapter의 opt-in 경계와 제거 가능한 recipe
|
||||
|
||||
따라서 현재 상태를 “프론트 공통부가 모두 구현됐다”고 표현하면 범위가 과장된다.
|
||||
더 정확한 표현은 다음과 같다.
|
||||
|
||||
> application API, 서버 상태, 폼, 라우팅, 페이지, 디자인 시스템과 테스트 증적의
|
||||
> 표준 수직 경로는 갖춰졌다. 현재 남은 저장소 내부 범위는 공급망 검증과
|
||||
> opt-in adapter recipe이며 실제 hosting·IdP·운영 provider는 프로젝트 통합
|
||||
> 범위다.
|
||||
> application API, 서버 상태, 폼, 라우팅, 페이지, 디자인 시스템, 테스트와
|
||||
> local 공급망 증적의 표준 수직 경로는 갖춰졌다. 현재 남은 저장소 내부 범위는
|
||||
> opt-in adapter recipe이며 실제 hosting·IdP·취약점/서명/운영 provider는
|
||||
> 프로젝트 통합 범위다.
|
||||
|
||||
## 3. 판정 기준
|
||||
|
||||
@@ -89,7 +88,7 @@ i18n 실행 경계와 diagnostics/telemetry production wiring은 구현됐다.
|
||||
| UI 회귀 검증 | 준비됨 | dev-only Storybook interaction/axe와 pinned Chromium visual baseline 4종 | cloud review와 다중 OS/device는 프로젝트 선택 |
|
||||
| 샘플 제거 | 준비됨 | feature/catalog/test 제거 후 type/architecture/registry/test/home/build 9단계 검증 | 새 contribution도 같은 제거 gate에 포함 |
|
||||
| registry·compatibility 집행 | 준비됨 | 10개 registry type/reference/consumer/orphan, 승인 digest와 actual semantic diff, breaking evidence | public 계약 변경 시 baseline review 유지 |
|
||||
| 공급망 검사 | 부분 준비 | lockfile·문서·gate는 있으나 실제 transitive 취약점/license/SBOM 깊이가 부족 | pinned scanner와 policy exception/증적 연결 |
|
||||
| 공급망 검사 | 준비됨/프로젝트 선택 | 561개 transitive inventory/integrity/license, actual diff, CycloneDX, local provenance, secret/reproducible build gate | 실제 vulnerability scanner와 signed attestation 없이는 promotion `FAIL_UNVERIFIED` |
|
||||
| realtime·offline·file 등 | 프로젝트 선택 | 현재 없음 | port/adapter recipe와 선택 기준 제공 |
|
||||
|
||||
## 5. 우선순위별 발견 사항
|
||||
|
||||
@@ -892,6 +892,31 @@ owner와 만료 시한이 있는 quarantine만 허용한다.
|
||||
RP-11은 P1 최종 저장소 기준선이다. scanner outage를 무검증 승인으로 우회하지
|
||||
않고 promotion을 보류한다.
|
||||
|
||||
**구현 증거 (2026-07-26)**
|
||||
|
||||
- VD-09에서 frozen pnpm graph와 lockfile을 local SSOT로, package manifest
|
||||
license policy와 CycloneDX 1.6을 local evidence로 채택했다. 외부 vulnerability
|
||||
report와 signed attestation이 없으면 promotion은 `FAIL_UNVERIFIED`다.
|
||||
- 현재 직접 35개, 전체 전이 561개 dependency의 name/version, direct/scope/
|
||||
optional, SHA-512 integrity, license와 dependency edge를 deterministic
|
||||
inventory로 생성한다. lockfile row와 inventory가 양방향 일치하지 않으면
|
||||
실패한다.
|
||||
- 승인 baseline digest와 actual add/remove/change/upgrade diff를 계산하고 새
|
||||
direct production dependency에는 owner와 다른 reviewer, reason과 rollback을
|
||||
요구한다.
|
||||
- CycloneDX SBOM component/edge와 local in-toto/SLSA 형태 provenance의
|
||||
source/lock/SBOM/dist digest를 coherence gate로 다시 계산한다.
|
||||
- license allow/deny, vulnerability severity와 독립·만료 exception 정책,
|
||||
provider lock digest와 attestation subject를 machine-readable하게 검증한다.
|
||||
provider fixture는 promotion PASS를 증명한 뒤 unconfigured
|
||||
`FAIL_UNVERIFIED`를 복원한다.
|
||||
- secret scan을 source/scripts/tests/config/schema/public/dist/generated release
|
||||
metadata로 확장하고 원문 대신 rule/path/line/fingerprint만 SARIF에 남긴다.
|
||||
test-only allowlist도 owner/reason/expiry를 강제한다.
|
||||
- `SOURCE_DATE_EPOCH` 기반 동일 build 2회 digest, 실제 frozen install mismatch,
|
||||
transitive omission/integrity/baseline/self-review/license/vulnerability/
|
||||
provider/SBOM/provenance/secret negative fixture를 blocking gate에 연결했다.
|
||||
|
||||
## 9. P1 exit gate
|
||||
|
||||
- 현실적인 form의 validation/dirty/pending/422/conflict가 작동한다.
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
# Build and supply-chain gate
|
||||
|
||||
Merge and release controls:
|
||||
## Local blocking controls
|
||||
|
||||
- frozen `pnpm-lock.yaml` installation; drift is blocking
|
||||
- clean production build with hashed assets and build manifest
|
||||
- machine-readable bundle sizes and checksums
|
||||
- source plus built-asset credential-pattern scan
|
||||
- direct dependency inventory and lockfile digest
|
||||
- base/head dependency diff review record
|
||||
- `pnpm install --frozen-lockfile` and a real manifest/lock mismatch fixture
|
||||
- all direct and transitive lockfile rows with package SHA-512 integrity
|
||||
- production/development, direct/transitive and platform-optional classification
|
||||
- package-manifest license allow/deny policy
|
||||
- approved inventory baseline digest and actual add/remove/change/upgrade diff
|
||||
- independent review for new direct production dependencies
|
||||
- CycloneDX 1.6 SBOM and inventory component/edge coherence
|
||||
- source/lock/SBOM/dist-linked local provenance statement
|
||||
- source, scripts, tests, tracked config/schema, public, built asset and generated
|
||||
release metadata secret scan
|
||||
- two-build `SOURCE_DATE_EPOCH` reproducibility check
|
||||
|
||||
Organization-specific vulnerability severity, denied-license list, SBOM format,
|
||||
and scanner selection remain policy inputs. An approved suppression must record
|
||||
reason, owner, expiry, affected package, and compensating control. Expired
|
||||
suppressions are blocking.
|
||||
The canonical commands are:
|
||||
|
||||
`artifacts/security/dependency-diff.json` is a local baseline. CI replaces it
|
||||
with the actual base/head direct and transitive lockfile diff before release.
|
||||
```bash
|
||||
corepack pnpm verify:lockfile
|
||||
corepack pnpm verify:reproducible-build
|
||||
corepack pnpm build:release
|
||||
corepack pnpm verify:supply-chain
|
||||
corepack pnpm check:supply-chain:fixtures
|
||||
```
|
||||
|
||||
`config/security/dependency-baseline.json` is the approved local baseline.
|
||||
Changing it requires `DEPENDENCY_BASELINE_OWNER` and
|
||||
`DEPENDENCY_BASELINE_REASON`; editing the digest or hardcoding an empty diff is
|
||||
rejected.
|
||||
|
||||
## External promotion controls
|
||||
|
||||
The vulnerability adapter reads the file named by
|
||||
`VULNERABILITY_REPORT_PATH`. It requires a provider, the exact lockfile digest,
|
||||
severity findings and valid independent, unexpired exception evidence.
|
||||
`PROVENANCE_ATTESTATION_PATH` must name a provider, signer and the exact built
|
||||
dist subject digest.
|
||||
|
||||
If either provider input is absent, local verification remains meaningful but
|
||||
`artifacts/security/supply-chain-verification.json` records
|
||||
`promotionStatus: FAIL_UNVERIFIED`. `verify:supply-chain:promotion` then exits
|
||||
non-zero. Scanner or signing outages are not converted to an empty PASS.
|
||||
|
||||
Approved vulnerability exceptions require vulnerability/package identity,
|
||||
owner, a different reviewer, reason and expiry. Expired or self-approved
|
||||
exceptions are blocking.
|
||||
|
||||
Reference in New Issue
Block a user