test(iwooos): 新增 package docker 供應鏈基線 [skip ci]

This commit is contained in:
Your Name
2026-06-15 06:06:09 +08:00
parent 03813c638c
commit 1ab85f5171
6 changed files with 935 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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 draftrequest 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 取得 |

View File

@@ -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%` | 未開啟任何執行期閘門 |

View File

@@ -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/*` 檔命中 C0Owner Packet snapshot 已同步為 `packets=3 / c0=2`Coverage snapshot 已同步最新 patternsIwoooS / AwoooP 前台 Owner Packet 摘要已正式驗證 `packet=3 / c0=2`feature commit `e999c16b`、deploy marker `16c6b983`、Gitea code-review `2973` / CD `2972` successIwoooS 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` |

View File

@@ -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 授權。"
]
}

View File

@@ -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<image>\S+)", re.IGNORECASE)
FROM_ALIAS_PATTERN = re.compile(r"\s+AS\s+(?P<alias>[A-Za-z0-9_.-]+)\s*$", re.IGNORECASE)
COPY_FROM_PATTERN = re.compile(r"^\s*COPY\s+--from=(?P<image>\S+)", re.IGNORECASE)
IMAGE_PATTERN = re.compile(r"^\s*image\s*:\s*[\"']?(?P<image>[^\"'#\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<name>[^\"']+)", 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()