diff --git a/docs/schemas/package_supply_chain_baseline_v1.schema.json b/docs/schemas/package_supply_chain_baseline_v1.schema.json new file mode 100644 index 00000000..f3134cf1 --- /dev/null +++ b/docs/schemas/package_supply_chain_baseline_v1.schema.json @@ -0,0 +1,202 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/package_supply_chain_baseline_v1.schema.json", + "title": "Package Supply Chain Baseline v1", + "type": "object", + "required": [ + "schema_version", + "status", + "mode", + "generated_at", + "git_commit", + "summary", + "package_json_manifests", + "pyproject_manifests", + "requirements_files", + "lockfiles", + "dockerfiles", + "compose_files", + "gaps", + "execution_boundaries" + ], + "properties": { + "schema_version": { + "const": "package_supply_chain_baseline_v1" + }, + "status": { + "const": "repo_only_inventory_ready_needs_owner_policy" + }, + "mode": { + "const": "repo_snapshot_only_no_install_no_network_no_cve_scan" + }, + "generated_at": { + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "package_manager": { + "type": "string" + }, + "summary": { + "type": "object", + "required": [ + "package_json_count", + "pyproject_count", + "requirements_file_count", + "lockfile_count", + "dockerfile_count", + "compose_file_count", + "gap_count", + "owner_response_received_count", + "owner_response_accepted_count", + "runtime_gate_count", + "action_button_count" + ], + "properties": { + "package_json_count": { + "type": "integer", + "minimum": 0 + }, + "pyproject_count": { + "type": "integer", + "minimum": 0 + }, + "requirements_file_count": { + "type": "integer", + "minimum": 0 + }, + "lockfile_count": { + "type": "integer", + "minimum": 0 + }, + "dockerfile_count": { + "type": "integer", + "minimum": 0 + }, + "compose_file_count": { + "type": "integer", + "minimum": 0 + }, + "gap_count": { + "type": "integer", + "minimum": 0 + }, + "owner_response_received_count": { + "const": 0 + }, + "owner_response_accepted_count": { + "const": 0 + }, + "runtime_gate_count": { + "const": 0 + }, + "action_button_count": { + "const": 0 + } + }, + "additionalProperties": true + }, + "package_json_manifests": { + "type": "array" + }, + "pyproject_manifests": { + "type": "array" + }, + "requirements_files": { + "type": "array" + }, + "lockfiles": { + "type": "array" + }, + "dockerfiles": { + "type": "array" + }, + "compose_files": { + "type": "array" + }, + "gaps": { + "type": "array", + "items": { + "type": "string" + } + }, + "next_owner_evidence_fields": { + "type": "array", + "items": { + "type": "string" + } + }, + "execution_boundaries": { + "type": "object", + "required": [ + "package_install_authorized", + "dependency_upgrade_authorized", + "lockfile_rewrite_authorized", + "cve_scan_authorized", + "docker_build_authorized", + "docker_pull_authorized", + "docker_push_authorized", + "registry_login_authorized", + "secret_value_collection_allowed", + "workflow_modification_authorized", + "production_deploy_authorized", + "runtime_gate_count", + "action_button_count", + "not_authorization" + ], + "properties": { + "package_install_authorized": { + "const": false + }, + "dependency_upgrade_authorized": { + "const": false + }, + "lockfile_rewrite_authorized": { + "const": false + }, + "cve_scan_authorized": { + "const": false + }, + "docker_build_authorized": { + "const": false + }, + "docker_pull_authorized": { + "const": false + }, + "docker_push_authorized": { + "const": false + }, + "registry_login_authorized": { + "const": false + }, + "secret_value_collection_allowed": { + "const": false + }, + "workflow_modification_authorized": { + "const": false + }, + "production_deploy_authorized": { + "const": false + }, + "runtime_gate_count": { + "const": 0 + }, + "action_button_count": { + "const": 0 + }, + "not_authorization": { + "const": true + } + }, + "additionalProperties": true + }, + "operator_interpretation": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true +} diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 2525a223..5db7d62c 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -193,7 +193,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | P2 | AWOOOI / AwoooP / IwoooS frontend runtime config | `apps/web/next.config.js`、`apps/web/src/lib/config.ts`、i18n | web owner | NEXT_PUBLIC public-domain only、no internal transcript, desktop/mobile smoke | | P2 | VibeWork product boundary | VibeWork owner docs / future evidence refs | VibeWork owner | independent product boundary、repo / deploy / admin / backup scope | | P2 | StockPlatform / Tsenyang / Bitan / VTuber routes | Nginx templates、product runbooks | product owner | domain / admin / API / backup / owner matrix | -| P2 | Package / supply-chain baselines | `pnpm-lock.yaml`、`package.json`、Dockerfiles、inventory snapshots | repo owner | lockfile drift, CVE / license policy, image digest evidence | +| P2 | Package / supply-chain baselines | `pnpm-lock.yaml`、`package.json`、`pyproject.toml`、`requirements.txt`、Dockerfiles、docker-compose、`docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md`、`docs/security/package-supply-chain-baseline.snapshot.json` | repo / registry owner | package manager policy、lockfile owner、Python lock policy、CVE / license / SBOM window、image digest evidence、registry owner、rollback owner | | P3 | Runbook / endpoint docs / snapshots | `docs/reference/*`、`docs/runbooks/*`、`docs/security/*.snapshot.json` | doc owner | no secret value, stale endpoint flag, owner-reviewed evidence refs | 2026-06-14 P0-20 已新增 `docs/security/K8S-ARGOCD-MANIFEST-INVENTORY.md` 與 `docs/security/k8s-argocd-manifest-inventory.snapshot.json`,把 K8s / ArgoCD / Velero / monitoring repo source 固定為 `files=49`、`c0=36`、`yaml=45`、`unique_kinds=20`、`blocked_actions=13` 的只讀清冊。P0-21 再新增 `docs/security/K8S-ARGOCD-OWNER-REQUEST-DRAFT.md` 與 `docs/security/k8s-argocd-owner-request-draft.snapshot.json`,將四個 scan group 轉成 `drafts=4`、`c0=3`、`owner_fields=11` 的 owner request draft。2026-06-15 P0-25 再新增 `docs/security/K8S-ARGOCD-OWNER-RESPONSE-ACCEPTANCE.md` 與 `docs/security/k8s-argocd-owner-response-acceptance.snapshot.json`,固定 `candidates=4`、`c0=3`、`owner_fields=11`、`reviewer_checks=12`、`outcome_lanes=7`、`blocked_actions=18` 的 owner response acceptance 只讀帳本。2026-06-15 再新增 `docs/security/K8S-ARGOCD-CHANGE-EVIDENCE-ACCEPTANCE.md` 與 `docs/security/k8s-argocd-change-evidence-acceptance.snapshot.json`,固定 `candidates=4`、`c0=3`、`write_capable=4`、`required_evidence_fields=18`、`reviewer_checks=18`、`outcome_lanes=8`、`blocked_actions=28` 的 GitOps 變更證據驗收只讀帳本。這些都不是 live cluster read、ArgoCD API read、ArgoCD sync、kubectl action、Helm upgrade、secret collection、manual pod restart、scale workload、RBAC / NetworkPolicy change、restore backup、production write 或 runtime gate。 @@ -254,6 +254,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | 端口 / 防火牆變更證據驗收 | `100%` | 已新增 `port_firewall_change_evidence_acceptance_v1`,14 個 candidate、6 個 write-capable、16 個 reviewer checks、8 條 outcome lanes、24 類 blocked action;成熟度 `58% -> 60%` | | K8s / ArgoCD GitOps 變更證據驗收 | `100%` | 已新增 `k8s_argocd_change_evidence_acceptance_v1`,4 個 candidate、3 個 C0、4 個 write-capable、18 個 reviewer checks、8 條 outcome lanes、28 類 blocked action;成熟度 `62% -> 64%` | | Public / Admin / API runtime config 變更證據驗收 | `100%` | 已新增 `public_runtime_config_change_evidence_acceptance_v1`,6 個 candidate、5 個 C0、21 個 reviewer checks、8 條 outcome lanes、32 類 blocked action;成熟度 `62% -> 64%`;raw namespace / repo slug / 內部狀態碼 / 內部協作內容外洩列為拒收或隔離條件 | +| Package / Docker supply-chain repo-only baseline | `100%` | 已新增 `package_supply_chain_baseline_v1`,盤點 `package_json=6`、`pyproject=4`、`requirements=2`、`dockerfiles=2`、`compose=6`、`gaps=5`;不 install、不掃 CVE、不改 image、不部署 | | Backup / restore / escrow owner request draft | `100%` | 已將 38 個 backup / restore / escrow surface 轉成 owner request draft;request sent / received / accepted、backup run、restore run、offsite sync、remote delete、escrow marker write、retention change 仍為 0 | | CI blocking / workflow gate | `0%` | 本階段刻意不修改 `.gitea/workflows`,避免初期資安流程摩擦過大 | | owner-provided live Nginx file compare | `70%` | 工具可吃 owner 匯出的 live conf 檔比較;本階段不主動 SSH 取得 | diff --git a/docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md b/docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md new file mode 100644 index 00000000..af13eeea --- /dev/null +++ b/docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md @@ -0,0 +1,99 @@ +# Package / Docker 供應鏈基線 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-15 | +| 狀態 | `repo_only_inventory_ready_needs_owner_policy` | +| 腳本 | `scripts/security/package-supply-chain-baseline.py` | +| Snapshot | `docs/security/package-supply-chain-baseline.snapshot.json` | +| Schema | `docs/schemas/package_supply_chain_baseline_v1.schema.json` | +| 模式 | repo snapshot only,不 install、不連外、不做 CVE scan、不改 image | +| runtime gate | `0` | + +## 1. 目的 + +此 baseline 把 AWOOOI repo 內的 package manifest、Python dependency file、lockfile、Dockerfile 與 docker-compose image refs 收成一份只讀供應鏈證據。它先回答「目前有哪些供應鏈入口需要控管」,不直接處理 CVE、升級套件、重寫 lockfile、pin digest、pull image 或部署。 + +本檔目前是 P2 repo-only evidence artifact,尚未列入 `security-supply-chain-contract-manifest.snapshot.json` 的 36 個正式 AwoooP 消費 contract。若後續要讓 AwoooP / IwoooS 前台直接消費,必須另行更新 manifest、readiness、route、rollup、dry-run、posture projection 與 guard count,不可只改本檔。 + +## 2. 目前盤點 + +| 指標 | 數量 | 判讀 | +|------|------|------| +| `package.json` | `6` | Node / pnpm workspace manifest 已可由 root `pnpm-lock.yaml` 追蹤 | +| `pyproject.toml` | `4` | Python project metadata 已盤點 | +| `requirements.txt` | `2` | 共 `26` 條 entry,目前皆非 `==` pin | +| lockfile | `1` | `pnpm-lock.yaml` 存在;未發現 `package-lock.json` / `yarn.lock` | +| Python lockfile | `0` | 尚未有 `poetry.lock` / `uv.lock` / `Pipfile.lock` | +| Dockerfile | `2` | 外部 `FROM` image 共 `3` 個,digest pinning `0` | +| Docker `COPY --from` 外部 image | `1` | digest pinning `0` | +| docker-compose | `6` | image refs 共 `16` 個,digest pinning `0` | +| owner response received / accepted | `0 / 0` | 尚未進入 owner policy 驗收 | +| runtime gate | `0` | 不提供執行或修復按鈕 | + +## 3. 目前缺口 + +| 缺口 | 說明 | 本階段處置 | +|------|------|------------| +| `python_lockfile_absent` | Python 專案尚未有 lock policy / lockfile 基線 | 先列 owner policy gap,不自動產生 lockfile | +| `requirements_unpinned_entries_present` | `requirements.txt` entry 目前未使用 `==` pin | 先列相容性 / policy gap,不自動 pin | +| `docker_base_images_not_all_digest_pinned` | Dockerfile 外部 base image 未全數 digest pinning | 先列 image policy gap,不自動改 tag | +| `docker_copy_from_images_not_all_digest_pinned` | Dockerfile 外部 `COPY --from` image 未 digest pinning | 先列 image policy gap,不自動改 tag | +| `compose_images_not_all_digest_pinned` | docker-compose image refs 未全數 digest pinning | 先列 compose image policy gap,不自動改 compose | + +## 4. Owner Evidence 欄位 + +後續若要把 baseline 往驗收推進,只收下列 metadata,不收 secret value: + +1. `package_manager_policy` +2. `lockfile_owner` +3. `python_lock_policy` +4. `docker_base_image_policy` +5. `compose_image_policy` +6. `registry_owner` +7. `cve_scan_window` +8. `rollback_owner` + +## 5. 指令 + +```bash +python3 scripts/security/package-supply-chain-baseline.py \ + --root . \ + --output docs/security/package-supply-chain-baseline.snapshot.json +``` + +固定 committed snapshot 時間: + +```bash +python3 scripts/security/package-supply-chain-baseline.py \ + --root . \ + --generated-at 2026-06-15T06:20:00+08:00 \ + --output docs/security/package-supply-chain-baseline.snapshot.json +``` + +預期輸出: + +```text +PACKAGE_SUPPLY_CHAIN_BASELINE_OK package_json=6 pyproject=4 requirements=2 dockerfiles=2 compose=6 gaps=5 runtime_gate=0 +``` + +## 6. 邊界 + +此 baseline 通過不代表: + +- 套件已安裝、升級、降級或修補。 +- CVE、license、SBOM、Trivy、npm audit、pip audit 已完成。 +- Docker image 已 pull、build、push、retag 或 digest pinning。 +- registry login、Harbor policy、image immutability 或 scanner policy 已驗收。 +- workflow、runner、secret、production deploy 或 runtime gate 已授權。 + +## 7. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| Package / Docker supply-chain repo-only baseline | `100%` | 已新增腳本、snapshot 與人讀文件 | +| Node lockfile 基線 | `80%` | `pnpm-lock.yaml` 存在;仍需 owner policy 確認 lockfile owner / update window | +| Python lock policy | `30%` | 已盤點 pyproject / requirements;尚缺 owner policy 與 lockfile 決策 | +| Docker / compose image policy | `35%` | 已盤點 image refs;尚缺 digest pinning policy、registry owner、rollback owner | +| CVE / license / SBOM 驗證 | `0%` | 未執行外部掃描;需 owner window 與工具策略 | +| runtime gate | `0%` | 未開啟任何執行期閘門 | diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 9dc75c04..45875d28 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -3,13 +3,14 @@ | 項目 | 內容 | |------|------| | 日期 | 2026-06-15 | -| 狀態 | IwoooS 64% 只讀治理推進中;CD / Runner / Secret 注入變更證據驗收與 Public / Admin / API runtime config 變更證據驗收只讀帳本已本地完成;端口 / 防火牆變更證據驗收與 K8s / ArgoCD GitOps 變更證據驗收已正式部署驗證;S4.9 owner response gate 仍是第一優先 | +| 狀態 | IwoooS 64% 只讀治理推進中;高價值配置集中 guard 與 Package / Docker 供應鏈 repo-only baseline 已完成;CD / Runner / Secret 注入變更證據驗收與 Public / Admin / API runtime config 變更證據驗收只讀帳本已本地完成;端口 / 防火牆變更證據驗收與 K8s / ArgoCD GitOps 變更證據驗收已正式部署驗證;S4.9 owner response gate 仍是第一優先 | | 本階段完成 | 資安供應鏈 contract manifest + Source Control Approval Board + Draft Reconcile Plan + Ref Detail Diff + Ref Truth Classification + Source Control Ref Truth Owner Response 收件包 + GitHub Primary Readiness Gate + GitHub Primary Rollback ADR + GitHub Target Owner Decision Response 收件包 + Gitea 認證清冊匯出請求 + Gitea 認證清冊匯入驗收契約 + Gitea 清冊覆蓋 Owner Attestation + Gitea Owner Attestation Approval Lane 對齊 + Gitea Owner Attestation Response 收件包 + Workflow / Secret Name Inventory + Workflow / Secret Name Local Evidence + Workflow / Secret Name Redacted Export Request + Workflow / Secret Name Owner Response 收件包 + Source Control Owner Response Validation Rollup + Kali 112 live integration status + Security Finding contract + Kali scan scope approval package + Security Approval Queue + S3 人工批准 Gate + S3 人工決策紀錄 + S3 人工審查封包 + S3 人工決策狀態轉移 + S3 後續 runtime gate 準備契約 + 鏡像 readiness index + 鏡像接收計畫 + 鏡像事件信封 + 鏡像路由矩陣 + 鏡像驗收契約 + 鏡像隔離契約 + 鏡像 dry-run 報告契約 + 鏡像狀態彙整契約 + IwoooS 前端態勢入口 + IwoooS posture projection contract + IwoooS 既有前端資安頁面整合 + IwoooS 覆蓋與邊界矩陣 + IwoooS 只讀資安處理旅程 + IwoooS owner evidence readiness board + IwoooS host coverage view + IwoooS host action gate matrix + IwoooS host evidence readiness board + IwoooS host evidence collection order + IwoooS host evidence intake preflight + IwoooS host evidence review outcome lanes + IwoooS host evidence review handoff packets + IwoooS host evidence reviewer checklist + IwoooS host evidence reviewer outcome lanes + IwoooS host owner decision candidate packets + IwoooS host owner decision review checklist + IwoooS host owner decision review outcome lanes + IwoooS host owner decision record draft packets + IwoooS host owner decision record draft review checklist + IwoooS host owner decision record draft review outcome lanes + IwoooS host owner decision record write-up packets + IwoooS host owner decision record write-up review checklist + IwoooS host owner decision record write-up review outcome lanes + IwoooS host owner decision record formal candidate packets + IwoooS host owner decision record formal candidate review checklist + IwoooS host owner decision record formal candidate review outcome lanes + IwoooS host owner decision record formal record queue packets + IwoooS host owner decision record formal record queue review checklist + IwoooS host owner decision record formal record queue review outcome lanes + IwoooS host owner decision record human handoff readiness packets + IwoooS host owner decision record human handoff readiness review checklist + IwoooS host owner decision record human handoff readiness review outcome lanes + IwoooS host owner decision record human record owner review candidate packets + IwoooS host owner decision record human record owner review candidate checklist + IwoooS host owner decision record human record owner review candidate outcome lanes + IwoooS host owner decision record human record owner review preparation packets + IwoooS host owner decision record human record owner review preparation checklist + IwoooS progress acceleration lanes + IwoooS owner response next-action focus + IwoooS S4.9 owner response preflight + IwoooS S4.9 owner response request templates + IwoooS progress hold movement gates + IwoooS AwoooP read-only landing readiness + IwoooS AwoooP cross-session handoff packets + AwoooP 首頁 IwoooS 資安鏡像候選 + AwoooP 工作鏈路 IwoooS 資安鏡像候選 + AwoooP 審批佇列 IwoooS owner response 只讀焦點 | | 本階段追加 | AwoooP 合約儀表板 IwoooS 資安契約只讀候選 + AwoooP 租戶管理 IwoooS 資安租戶範圍只讀候選 + AwoooP 執行監控 IwoooS 執行狀態只讀候選 + 既有安全 / 合規頁面 IwoooS 只讀反向橋接 + 告警 / 錯誤 / 授權 / 治理頁面 IwoooS 只讀反向橋接 + 稽核 / 工程審查頁面 IwoooS 深色只讀反向橋接 + IwoooS 前端資安頁面連接狀態板 + IwoooS GitHub 主要來源就緒度只讀狀態板 + AwoooP 工作鏈路 GitHub 主要來源就緒度只讀工作項 + AwoooP 合約儀表板 GitHub 主要來源就緒度合約只讀候選 + AwoooP 審批佇列 GitHub 主要來源就緒度審批邊界 + AwoooP 首頁 GitHub 主要來源就緒度只讀摘要 + AwoooP 租戶管理 GitHub 主要來源就緒度租戶範圍 + AwoooP 執行監控 GitHub 主要來源就緒度執行邊界 + IwoooS / AwoooP 資安可視區塊繁體中文呈現防護檢查 + AwoooP 執行詳情 / 審批詳情繁體中文呈現防護檢查 + AwoooP 首頁負責人回覆驗收總覽 + AwoooP 工作鏈路負責人回覆驗收只讀工作項 + AwoooP 合約儀表板負責人回覆驗收契約只讀候選 + AwoooP 審批佇列負責人回覆驗收只讀審查邊界 + AwoooP 租戶管理負責人回覆驗收租戶範圍 + AwoooP 執行監控負責人回覆驗收執行邊界 + AwoooP 執行詳情負責人回覆驗收詳情邊界 + AwoooP 審批決策負責人回覆驗收審批邊界 + IwoooS AwoooP 資安入口覆蓋狀態板 + IwoooS 階段式資安收斂節奏圖 + IwoooS 下一步人工收件作戰板 + IwoooS 人工回覆安全驗收閘道 + IwoooS 人工回覆審查結果分流 + IwoooS 人工決策準備佇列 + IwoooS 人工決策紀錄草稿防誤用 + IwoooS 人工決策正式紀錄負責人指派確認準備包 + IwoooS 人工決策正式紀錄負責人指派確認清單 + IwoooS 人工決策正式紀錄負責人指派確認結果分流 + IwoooS 人工決策正式紀錄負責人指派決策準備包 + IwoooS 人工決策正式紀錄負責人指派決策檢查清單 + IwoooS S4.9 負責人回覆封套欄位 + IwoooS S4.9 負責人回覆封套送件前檢查 + IwoooS S4.9 負責人回覆封套送件前結果分流 + IwoooS S4.9 負責人回覆送件請求草稿 + IwoooS S4.9 負責人回覆送件鏈路摘要 + IwoooS 低摩擦分階段收斂主控 + IwoooS 低摩擦下一步行動邊界 + IwoooS 64% 進度移動訊號驗收條 + IwoooS 第一個進度解鎖路徑 + IwoooS 第一解鎖證據包 + IwoooS 第一解鎖證據包預檢分流 + IwoooS 第一解鎖證據包補件路徑 + IwoooS 第一解鎖證據包補件送審前檢查 + IwoooS 第一解鎖證據包補件送審結果分流 + IwoooS 第一解鎖證據包 reviewer 指派準備包 + IwoooS 第一解鎖證據包 reviewer 指派前檢查 + IwoooS 第一解鎖證據包 reviewer 指派前檢查結果分流 + IwoooS 正式只讀 landing 與 Kali 112 只讀證據進度重估 | | 本階段追加補充 | IwoooS 目前具體工作地圖 + IwoooS 目前具體交付清單 + IwoooS 目前阻塞與解除條件 + IwoooS 三軸進度與全產品套用範圍 + IwoooS 全產品分階段套用台帳 + IwoooS 全產品 rollout 波次驗收門檻 + IwoooS 全產品 rollout 驗收結果分流 + IwoooS 全產品證據接線地圖 + IwoooS 全產品證據接線預檢 + IwoooS 全產品證據接線預檢結果分流 + IwoooS 全產品預檢補件回收台帳 + IwoooS 全產品補件重試門檻 + IwoooS 全產品重試結果分流 + IwoooS 全產品人工審查候選準備 + IwoooS 全產品人工審查候選預檢 + IwoooS 全產品人工審查候選預檢結果分流 + IwoooS 全產品人工審查候選預檢補件回收台帳 + IwoooS 全產品人工審查候選預檢補件重試門檻 + IwoooS 全產品只讀套用快照 + P2-145 owner response acceptance gate 正式驗證完成 | | P0 追加 | IwoooS P0 配置控管優先序前台正式驗證完成;Nginx public gateway、DNS / TLS / certbot、K8s / ArgoCD / production manifests、Workflow / runner / secret metadata、Public / admin / API runtime config、agent-bounty runtime / treasury 六類先列為即時風險配置;高價值配置 Gate 已補上 `k8s/nginx/**`、`scripts/ops/**/*cert*`、`scripts/ops/**/*tls*`,sample 從 `matched=0 / C0=0` 收斂到 `matched=3 / C0=2`;Gate 預設工作樹 preflight 已可讀取 staged / unstaged / untracked,本地 smoke 對臨時 `k8s/nginx/*` 檔命中 C0;Owner Packet snapshot 已同步為 `packets=3 / c0=2`,Coverage snapshot 已同步最新 patterns;IwoooS / AwoooP 前台 Owner Packet 摘要已正式驗證 `packet=3 / c0=2`,feature commit `e999c16b`、deploy marker `16c6b983`、Gitea code-review `2973` / CD `2972` success;IwoooS posture projection snapshot / schema / guard 已同步 `packet=3 / c0=2`,不再保留舊 `1 / 0` 口徑;高價值配置 Owner Packet 收件預檢已新增 `checks=9 / lanes=5 / required_fields=27 / blocked_requests=16`;高價值配置 Owner Request 草稿包已新增 `drafts=3 / handoff_fields=11 / forbidden_payloads=12 / sent=0`;Public Gateway live conf 匯出請求包已新增 `requests=3 / c0=2 / redaction_rules=8 / received=0`;Public Gateway redacted export 收件預檢已新增 `candidates=3 / c0=2 / checks=10 / rejection_guards=12 / received=0 / accepted=0`;Public Gateway rendered diff / nginx gate 草稿已新增 `candidates=3 / c0=2 / stages=7 / blocked=14 / rendered_diff=0 / runtime=0`;Public Gateway owner response acceptance 只讀帳本已新增 `candidates=3 / c0=2 / fields=12 / checks=12 / lanes=7 / blocked=18 / accepted=0 / runtime=0`;DNS / TLS / certbot Owner Confirmation Request 已新增 `requests=4 / c0=4 / fields=9 / questions=5 / guards=12 / received=0 / accepted=0`;K8s / ArgoCD manifest repo-only 清冊已新增 `files=49 / c0=36 / yaml=45 / kinds=20 / blocked=13 / runtime=0`;K8s / ArgoCD Owner Request Draft 已新增 `drafts=4 / c0=3 / fields=11 / sent=0 / runtime=0`;K8s / ArgoCD owner response acceptance 只讀帳本已新增 `candidates=4 / c0=3 / fields=11 / checks=12 / lanes=7 / blocked=18 / accepted=0 / runtime=0`;K8s / ArgoCD GitOps 變更證據驗收已新增 `candidates=4 / c0=3 / write_capable=4 / evidence_fields=18 / checks=18 / lanes=8 / blocked=28 / accepted=0 / runtime=0`;CD / Runner / Secret 注入變更證據驗收已新增 `candidates=5 / c0=4 / write_capable=5 / workflow_files=33 / secret_names=42 / runner_labels=5 / evidence_fields=19 / checks=19 / lanes=8 / blocked=32 / accepted=0 / runtime=0`;Public / Admin / API runtime config 變更證據驗收已新增 `candidates=6 / c0=5 / write_capable=6 / source_refs=20 / evidence_fields=21 / checks=21 / lanes=8 / blocked=32 / accepted=0 / runtime=0`,並把 raw namespace、repo slug、內部狀態碼與內部協作內容外洩列為拒收 / 隔離;Backup / Restore / Escrow owner response acceptance 只讀帳本已新增 `candidates=38 / write_capable=27 / fields=14 / checks=13 / lanes=7 / blocked=22 / accepted=0 / runtime=0`;SSH / Firewall / Network Access owner response acceptance 只讀帳本已新增 `candidates=16 / write_capable=6 / fields=13 / checks=15 / lanes=7 / blocked=22 / accepted=0 / runtime=0`;端口 / 防火牆變更證據驗收只讀帳本已新增 `candidates=14 / write_capable=6 / policy_or_exposure=5 / evidence_fields=16 / checks=16 / lanes=8 / blocked=24 / accepted=0 / runtime=0`;owner response / live evidence / runtime gate / action buttons 仍全部為 0 | | P0 agent-bounty 追加 | agent-bounty-protocol Owner Request Draft 已新增 `drafts=11 / control=4 / surface=7 / write_capable=8 / treasury=4 / mcp_a2a=5 / fields=22 / forbidden_inputs=25 / blocked=28 / sent=0 / runtime=0`;這是 repo / refs、deployment、data classification、MCP / A2A、cron / daemon、admin / treasury、webhook / traffic 的人工送件前草稿,不是 owner response、repo push、refs sync、workflow 修改、secret 收集、deploy、compose restart、DB migration、claim / submit、payout / withdrawal、cron / daemon、external send、host write 或 runtime gate | | P1 追加 | Docker / systemd / Host Service Owner Request Draft 已新增 `drafts=9 / write_capable=3 / fields=12 / blocked=14 / sent=0 / runtime=0`;SSH / Firewall / Network Access Owner Request Draft 已新增 `drafts=16 / write_capable=6 / fields=13 / blocked=16 / sent=0 / runtime=0`;Backup / Restore / Escrow Owner Request Draft 已新增 `drafts=38 / write_capable=27 / fields=14 / blocked=18 / sent=0 / runtime=0`;Backup / Restore / Escrow Owner Response Acceptance 已新增 `candidates=38 / write_capable=27 / reviewer_checks=13 / lanes=7 / blocked=22 / accepted=0 / runtime=0`;Monitoring / Alerting / Observability Owner Request Draft 已新增 `drafts=60 / write_capable=11 / fields=14 / blocked=24 / sent=0 / runtime=0`;上述全部仍是人工送件前草稿或只讀 acceptance 帳本,不是 owner response、live evidence、reload、restart、backup、restore、Telegram send、alert smoke、host write 或 runtime gate | +| P2 供應鏈追加 | Package / Docker 供應鏈 repo-only baseline 已新增 `package_json=6 / pyproject=4 / requirements=2 / dockerfiles=2 / compose=6 / gaps=5 / runtime=0`;缺口為 Python lockfile 缺席、requirements 未 pin、Docker base image 未全數 digest pinning、Docker `COPY --from` 外部 image 未 digest pinning、compose image 未 digest pinning;目前尚未列入 36 個正式 AwoooP 消費 contract,後續若要前台消費需同步 manifest / readiness / route / rollup / dry-run / posture projection / guard count;本輪不 install、不 upgrade、不跑 CVE、不 pull / build / push image、不改 tag、不登入 registry、不部署 | | 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | | P0 主控板 | `docs/workplans/2026-06-04-iwooos-security-governance-p0.md` | diff --git a/docs/security/package-supply-chain-baseline.snapshot.json b/docs/security/package-supply-chain-baseline.snapshot.json new file mode 100644 index 00000000..125d90b3 --- /dev/null +++ b/docs/security/package-supply-chain-baseline.snapshot.json @@ -0,0 +1,256 @@ +{ + "schema_version": "package_supply_chain_baseline_v1", + "status": "repo_only_inventory_ready_needs_owner_policy", + "mode": "repo_snapshot_only_no_install_no_network_no_cve_scan", + "generated_at": "2026-06-15T06:20:00+08:00", + "git_commit": "03813c63", + "package_manager": "pnpm@9.0.0", + "summary": { + "package_json_count": 6, + "pyproject_count": 4, + "requirements_file_count": 2, + "requirements_entry_count": 26, + "requirements_unpinned_entry_count": 26, + "lockfile_count": 1, + "pnpm_lock_present": true, + "npm_lock_present": false, + "yarn_lock_present": false, + "python_lockfile_count": 0, + "dockerfile_count": 2, + "docker_base_image_count": 3, + "docker_base_digest_pinned_count": 0, + "docker_copy_from_image_count": 1, + "docker_copy_from_digest_pinned_count": 0, + "compose_file_count": 6, + "compose_image_ref_count": 16, + "compose_digest_pinned_image_ref_count": 0, + "gap_count": 5, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0 + }, + "package_json_manifests": [ + { + "path": "apps/web/package.json", + "name": "@awoooi/web", + "private": true, + "package_manager": null, + "dependency_count": 33, + "has_scripts": true + }, + { + "path": "package.json", + "name": "awoooi", + "private": true, + "package_manager": "pnpm@9.0.0", + "dependency_count": 5, + "has_scripts": true + }, + { + "path": "packages/eslint-config/package.json", + "name": "@awoooi/eslint-config", + "private": true, + "package_manager": null, + "dependency_count": 6, + "has_scripts": false + }, + { + "path": "packages/lewooogo-core/package.json", + "name": "@awoooi/lewooogo-core", + "private": true, + "package_manager": null, + "dependency_count": 5, + "has_scripts": true + }, + { + "path": "packages/shared-types/package.json", + "name": "@awoooi/shared-types", + "private": false, + "package_manager": null, + "dependency_count": 2, + "has_scripts": true + }, + { + "path": "packages/tsconfig/package.json", + "name": "@awoooi/tsconfig", + "private": true, + "package_manager": null, + "dependency_count": 0, + "has_scripts": false + } + ], + "pyproject_manifests": [ + { + "path": "apps/api/pyproject.toml", + "name": "awoooi-api", + "dependency_count": 33, + "has_build_system": true + }, + { + "path": "packages/lewooogo-brain/pyproject.toml", + "name": "lewooogo-brain", + "dependency_count": 13, + "has_build_system": true + }, + { + "path": "packages/lewooogo-data/pyproject.toml", + "name": "lewooogo-data", + "dependency_count": 16, + "has_build_system": true + }, + { + "path": "scripts/aider_watch_client/pyproject.toml", + "name": "aider-watch-client", + "dependency_count": 0, + "has_build_system": true + } + ], + "requirements_files": [ + { + "path": "apps/api/requirements.txt", + "entry_count": 25, + "pinned_entry_count": 0, + "unpinned_entry_count": 25 + }, + { + "path": "apps/sensor/requirements.txt", + "entry_count": 1, + "pinned_entry_count": 0, + "unpinned_entry_count": 1 + } + ], + "lockfiles": [ + "pnpm-lock.yaml" + ], + "dockerfiles": [ + { + "path": "apps/api/Dockerfile", + "from_images": [ + "python:3.11-slim", + "python:3.11-slim" + ], + "from_image_count": 2, + "digest_pinned_from_image_count": 0, + "copy_from_images": [ + "ghcr.io/astral-sh/uv:0.6.9" + ], + "copy_from_image_count": 1, + "digest_pinned_copy_from_image_count": 0 + }, + { + "path": "apps/web/Dockerfile", + "from_images": [ + "node:20-alpine" + ], + "from_image_count": 1, + "digest_pinned_from_image_count": 0, + "copy_from_images": [], + "copy_from_image_count": 0, + "digest_pinned_copy_from_image_count": 0 + } + ], + "compose_files": [ + { + "path": "apps/api/docker-compose.test.yml", + "image_refs": [ + "pgvector/pgvector:pg16", + "redis:7-alpine", + "python:3.11-slim" + ], + "image_ref_count": 3, + "digest_pinned_image_ref_count": 0 + }, + { + "path": "docker-compose.yml", + "image_refs": [ + "postgres:16-alpine", + "redis:7-alpine" + ], + "image_ref_count": 2, + "digest_pinned_image_ref_count": 0 + }, + { + "path": "infra/langfuse/docker-compose.yml", + "image_refs": [ + "langfuse/langfuse:2", + "postgres:15-alpine" + ], + "image_ref_count": 2, + "digest_pinned_image_ref_count": 0 + }, + { + "path": "k8s/monitoring/docker-compose-110.yml", + "image_refs": [ + "gcr.io/cadvisor/cadvisor:latest", + "prom/prometheus:latest", + "grafana/grafana:latest", + "prom/blackbox-exporter:latest", + "prom/alertmanager:latest", + "promhippie/github-exporter:latest" + ], + "image_ref_count": 6, + "digest_pinned_image_ref_count": 0 + }, + { + "path": "ops/monitoring/docker-compose.exporters.yaml", + "image_refs": [ + "prometheuscommunity/postgres-exporter:v0.15.0", + "oliver006/redis_exporter:v1.58.0" + ], + "image_ref_count": 2, + "digest_pinned_image_ref_count": 0 + }, + { + "path": "ops/sentry-self-hosted/docker-compose.yml", + "image_refs": [ + "alpine:latest" + ], + "image_ref_count": 1, + "digest_pinned_image_ref_count": 0 + } + ], + "gaps": [ + "python_lockfile_absent", + "docker_base_images_not_all_digest_pinned", + "docker_copy_from_images_not_all_digest_pinned", + "compose_images_not_all_digest_pinned", + "requirements_unpinned_entries_present" + ], + "next_owner_evidence_fields": [ + "package_manager_policy", + "lockfile_owner", + "python_lock_policy", + "docker_base_image_policy", + "compose_image_policy", + "registry_owner", + "cve_scan_window", + "rollback_owner" + ], + "execution_boundaries": { + "package_install_authorized": false, + "dependency_upgrade_authorized": false, + "lockfile_rewrite_authorized": false, + "npm_audit_authorized": false, + "pip_audit_authorized": false, + "cve_scan_authorized": false, + "docker_build_authorized": false, + "docker_pull_authorized": false, + "docker_push_authorized": false, + "image_tag_change_authorized": false, + "image_digest_pin_change_authorized": false, + "registry_login_authorized": false, + "secret_value_collection_allowed": false, + "workflow_modification_authorized": false, + "production_deploy_authorized": false, + "runtime_gate_count": 0, + "action_button_count": 0, + "not_authorization": true + }, + "operator_interpretation": [ + "此 baseline 只代表 repo 供應鏈來源盤點,不代表 CVE / license / SBOM 已驗收。", + "Docker image 未全數 digest pinning 是 policy gap,不在本輪自動改 image tag。", + "Python lockfile 缺口是 owner policy gap,不在本輪自動產生 lockfile。", + "不得把此 snapshot 當成 install、upgrade、docker pull、registry login 或 deploy 授權。" + ] +} diff --git a/scripts/security/package-supply-chain-baseline.py b/scripts/security/package-supply-chain-baseline.py new file mode 100644 index 00000000..e16775dd --- /dev/null +++ b/scripts/security/package-supply-chain-baseline.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""產生 AWOOOI package / Docker 供應鏈 repo-only baseline。 + +本工具只掃描 repo 內的 manifest、lockfile、Dockerfile 與 docker-compose +檔案,不安裝套件、不連外、不跑 CVE scan、不讀 secret、不修改 workflow 或 +runtime。輸出用於 IwoooS 供應鏈治理的低摩擦證據基線。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 fallback + tomllib = None # type: ignore[assignment] + + +TAIPEI = timezone(timedelta(hours=8)) + +IGNORED_DIRS = { + ".git", + ".next", + ".turbo", + "__pycache__", + "node_modules", + "test-results", +} + +PACKAGE_JSON_NAMES = {"package.json"} +PYPROJECT_NAMES = {"pyproject.toml"} +REQUIREMENTS_PATTERN = re.compile(r"requirements(?:[-_.a-zA-Z0-9]*)?\.txt$") +DOCKERFILE_PATTERN = re.compile(r"(?:^|/)Dockerfile(?:\.[A-Za-z0-9_.-]+)?$") +COMPOSE_PATTERN = re.compile(r"(?:^|/)(?:docker-compose|compose)(?:[A-Za-z0-9_.-]*)?\.ya?ml$") +FROM_PATTERN = re.compile(r"^\s*FROM\s+(?:--platform=\S+\s+)?(?P\S+)", re.IGNORECASE) +FROM_ALIAS_PATTERN = re.compile(r"\s+AS\s+(?P[A-Za-z0-9_.-]+)\s*$", re.IGNORECASE) +COPY_FROM_PATTERN = re.compile(r"^\s*COPY\s+--from=(?P\S+)", re.IGNORECASE) +IMAGE_PATTERN = re.compile(r"^\s*image\s*:\s*[\"']?(?P[^\"'#\s]+)", re.IGNORECASE) + +LOCKFILE_NAMES = { + "pnpm-lock.yaml", + "package-lock.json", + "yarn.lock", + "poetry.lock", + "uv.lock", + "Pipfile.lock", +} + +EXECUTION_BOUNDARIES = { + "package_install_authorized": False, + "dependency_upgrade_authorized": False, + "lockfile_rewrite_authorized": False, + "npm_audit_authorized": False, + "pip_audit_authorized": False, + "cve_scan_authorized": False, + "docker_build_authorized": False, + "docker_pull_authorized": False, + "docker_push_authorized": False, + "image_tag_change_authorized": False, + "image_digest_pin_change_authorized": False, + "registry_login_authorized": False, + "secret_value_collection_allowed": False, + "workflow_modification_authorized": False, + "production_deploy_authorized": False, + "runtime_gate_count": 0, + "action_button_count": 0, + "not_authorization": True, +} + + +def should_skip(path: Path) -> bool: + return any(part in IGNORED_DIRS for part in path.parts) + + +def git_commit(root: Path) -> str: + try: + return subprocess.check_output( + ["git", "rev-parse", "--short=8", "HEAD"], + cwd=root, + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (OSError, subprocess.CalledProcessError): + return "unknown" + + +def read_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def package_manager_from_root(root: Path) -> str: + package_json = root / "package.json" + if not package_json.exists(): + return "unknown" + data = read_json(package_json) + value = data.get("packageManager") + if isinstance(value, str): + return value + if (root / "pnpm-lock.yaml").exists(): + return "pnpm-lock-present" + return "unknown" + + +def scan_package_json(root: Path, path: Path) -> dict[str, Any]: + data = read_json(path) + rel = path.relative_to(root).as_posix() + dependency_keys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] + dependency_count = sum(len(data.get(key, {})) for key in dependency_keys if isinstance(data.get(key), dict)) + return { + "path": rel, + "name": data.get("name", "(unnamed)"), + "private": data.get("private", False), + "package_manager": data.get("packageManager"), + "dependency_count": dependency_count, + "has_scripts": isinstance(data.get("scripts"), dict) and bool(data.get("scripts")), + } + + +def scan_pyproject(root: Path, path: Path) -> dict[str, Any]: + text = path.read_text(encoding="utf-8") + if tomllib is None: + name_match = re.search(r"(?m)^\s*name\s*=\s*[\"'](?P[^\"']+)", text) + return { + "path": path.relative_to(root).as_posix(), + "name": name_match.group("name") if name_match else "(unnamed)", + "dependency_count": len(re.findall(r"(?m)^\s*[\"'][^\"']+[\"']\s*,?\s*$", text)), + "has_build_system": "[build-system]" in text, + } + + data = tomllib.loads(text) + project = data.get("project", {}) + poetry = data.get("tool", {}).get("poetry", {}) + name = project.get("name") or poetry.get("name") or "(unnamed)" + dependencies = project.get("dependencies", []) + optional = project.get("optional-dependencies", {}) + poetry_deps = poetry.get("dependencies", {}) + dependency_count = 0 + if isinstance(dependencies, list): + dependency_count += len(dependencies) + if isinstance(optional, dict): + dependency_count += sum(len(value) for value in optional.values() if isinstance(value, list)) + if isinstance(poetry_deps, dict): + dependency_count += len(poetry_deps) + return { + "path": path.relative_to(root).as_posix(), + "name": name, + "dependency_count": dependency_count, + "has_build_system": "build-system" in data, + } + + +def scan_requirements(root: Path, path: Path) -> dict[str, Any]: + lines = path.read_text(encoding="utf-8").splitlines() + entries = [ + line.strip() + for line in lines + if line.strip() and not line.lstrip().startswith("#") and not line.lstrip().startswith("-r ") + ] + pinned = [line for line in entries if "==" in line] + return { + "path": path.relative_to(root).as_posix(), + "entry_count": len(entries), + "pinned_entry_count": len(pinned), + "unpinned_entry_count": len(entries) - len(pinned), + } + + +def scan_dockerfile(root: Path, path: Path) -> dict[str, Any]: + images: list[str] = [] + copy_from_images: list[str] = [] + stage_aliases: set[str] = set() + for line in path.read_text(encoding="utf-8").splitlines(): + match = FROM_PATTERN.match(line) + if match: + image = match.group("image") + if image not in stage_aliases: + images.append(image) + alias_match = FROM_ALIAS_PATTERN.search(line) + if alias_match: + stage_aliases.add(alias_match.group("alias")) + continue + copy_match = COPY_FROM_PATTERN.match(line) + if copy_match: + image = copy_match.group("image") + if image not in stage_aliases: + copy_from_images.append(image) + return { + "path": path.relative_to(root).as_posix(), + "from_images": images, + "from_image_count": len(images), + "digest_pinned_from_image_count": sum(1 for image in images if "@" in image), + "copy_from_images": copy_from_images, + "copy_from_image_count": len(copy_from_images), + "digest_pinned_copy_from_image_count": sum(1 for image in copy_from_images if "@" in image), + } + + +def scan_compose(root: Path, path: Path) -> dict[str, Any]: + images: list[str] = [] + for line in path.read_text(encoding="utf-8").splitlines(): + match = IMAGE_PATTERN.match(line) + if match: + images.append(match.group("image")) + return { + "path": path.relative_to(root).as_posix(), + "image_refs": images, + "image_ref_count": len(images), + "digest_pinned_image_ref_count": sum(1 for image in images if "@" in image), + } + + +def iter_repo_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*"): + if path.is_file() and not should_skip(path.relative_to(root)): + files.append(path) + return sorted(files) + + +def build_snapshot(root: Path, generated_at: str | None = None) -> dict[str, Any]: + generated_at = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + files = iter_repo_files(root) + + package_json = [scan_package_json(root, path) for path in files if path.name in PACKAGE_JSON_NAMES] + pyprojects = [scan_pyproject(root, path) for path in files if path.name in PYPROJECT_NAMES] + requirements = [scan_requirements(root, path) for path in files if REQUIREMENTS_PATTERN.fullmatch(path.name)] + dockerfiles = [ + scan_dockerfile(root, path) + for path in files + if DOCKERFILE_PATTERN.search(path.relative_to(root).as_posix()) + ] + compose_files = [ + scan_compose(root, path) + for path in files + if COMPOSE_PATTERN.search(path.relative_to(root).as_posix()) + ] + lockfiles = [ + path.relative_to(root).as_posix() + for path in files + if path.name in LOCKFILE_NAMES + ] + + docker_base_image_count = sum(item["from_image_count"] for item in dockerfiles) + docker_base_digest_count = sum(item["digest_pinned_from_image_count"] for item in dockerfiles) + docker_copy_from_image_count = sum(item["copy_from_image_count"] for item in dockerfiles) + docker_copy_from_digest_count = sum(item["digest_pinned_copy_from_image_count"] for item in dockerfiles) + compose_image_count = sum(item["image_ref_count"] for item in compose_files) + compose_digest_count = sum(item["digest_pinned_image_ref_count"] for item in compose_files) + requirements_entry_count = sum(item["entry_count"] for item in requirements) + requirements_unpinned_count = sum(item["unpinned_entry_count"] for item in requirements) + + gaps = [] + if "pnpm-lock.yaml" not in lockfiles: + gaps.append("pnpm_lock_missing") + if any(path.endswith(("package-lock.json", "yarn.lock")) for path in lockfiles): + gaps.append("unexpected_node_lockfile_present") + if pyprojects and not any(path.endswith(("poetry.lock", "uv.lock", "Pipfile.lock")) for path in lockfiles): + gaps.append("python_lockfile_absent") + if docker_base_image_count and docker_base_digest_count < docker_base_image_count: + gaps.append("docker_base_images_not_all_digest_pinned") + if docker_copy_from_image_count and docker_copy_from_digest_count < docker_copy_from_image_count: + gaps.append("docker_copy_from_images_not_all_digest_pinned") + if compose_image_count and compose_digest_count < compose_image_count: + gaps.append("compose_images_not_all_digest_pinned") + if requirements_unpinned_count: + gaps.append("requirements_unpinned_entries_present") + + return { + "schema_version": "package_supply_chain_baseline_v1", + "status": "repo_only_inventory_ready_needs_owner_policy", + "mode": "repo_snapshot_only_no_install_no_network_no_cve_scan", + "generated_at": generated_at, + "git_commit": git_commit(root), + "package_manager": package_manager_from_root(root), + "summary": { + "package_json_count": len(package_json), + "pyproject_count": len(pyprojects), + "requirements_file_count": len(requirements), + "requirements_entry_count": requirements_entry_count, + "requirements_unpinned_entry_count": requirements_unpinned_count, + "lockfile_count": len(lockfiles), + "pnpm_lock_present": "pnpm-lock.yaml" in lockfiles, + "npm_lock_present": any(path.endswith("package-lock.json") for path in lockfiles), + "yarn_lock_present": any(path.endswith("yarn.lock") for path in lockfiles), + "python_lockfile_count": sum( + 1 for path in lockfiles if path.endswith(("poetry.lock", "uv.lock", "Pipfile.lock")) + ), + "dockerfile_count": len(dockerfiles), + "docker_base_image_count": docker_base_image_count, + "docker_base_digest_pinned_count": docker_base_digest_count, + "docker_copy_from_image_count": docker_copy_from_image_count, + "docker_copy_from_digest_pinned_count": docker_copy_from_digest_count, + "compose_file_count": len(compose_files), + "compose_image_ref_count": compose_image_count, + "compose_digest_pinned_image_ref_count": compose_digest_count, + "gap_count": len(gaps), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + }, + "package_json_manifests": package_json, + "pyproject_manifests": pyprojects, + "requirements_files": requirements, + "lockfiles": lockfiles, + "dockerfiles": dockerfiles, + "compose_files": compose_files, + "gaps": gaps, + "next_owner_evidence_fields": [ + "package_manager_policy", + "lockfile_owner", + "python_lock_policy", + "docker_base_image_policy", + "compose_image_policy", + "registry_owner", + "cve_scan_window", + "rollback_owner", + ], + "execution_boundaries": EXECUTION_BOUNDARIES, + "operator_interpretation": [ + "此 baseline 只代表 repo 供應鏈來源盤點,不代表 CVE / license / SBOM 已驗收。", + "Docker image 未全數 digest pinning 是 policy gap,不在本輪自動改 image tag。", + "Python lockfile 缺口是 owner policy gap,不在本輪自動產生 lockfile。", + "不得把此 snapshot 當成 install、upgrade、docker pull、registry login 或 deploy 授權。", + ], + } + + +def write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--root", + default=Path(__file__).resolve().parents[2], + type=Path, + help="Repository root. Defaults to the current script's repository.", + ) + parser.add_argument("--generated-at", help="Override generated_at for committed snapshots.") + parser.add_argument("--output", type=Path, help="Write snapshot JSON to this path.") + args = parser.parse_args() + + root = args.root.resolve() + snapshot = build_snapshot(root, generated_at=args.generated_at) + if args.output: + output = args.output + if not output.is_absolute(): + output = root / output + write_json(output, snapshot) + + summary = snapshot["summary"] + print( + "PACKAGE_SUPPLY_CHAIN_BASELINE_OK " + f"package_json={summary['package_json_count']} " + f"pyproject={summary['pyproject_count']} " + f"requirements={summary['requirements_file_count']} " + f"dockerfiles={summary['dockerfile_count']} " + f"compose={summary['compose_file_count']} " + f"gaps={summary['gap_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + + +if __name__ == "__main__": + main()