test(iwooos): 新增 package docker 供應鏈基線 [skip ci]
This commit is contained in:
202
docs/schemas/package_supply_chain_baseline_v1.schema.json
Normal file
202
docs/schemas/package_supply_chain_baseline_v1.schema.json
Normal 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
|
||||
}
|
||||
@@ -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 取得 |
|
||||
|
||||
99
docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md
Normal file
99
docs/security/PACKAGE-SUPPLY-CHAIN-BASELINE.md
Normal 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%` | 未開啟任何執行期閘門 |
|
||||
@@ -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` |
|
||||
|
||||
|
||||
256
docs/security/package-supply-chain-baseline.snapshot.json
Normal file
256
docs/security/package-supply-chain-baseline.snapshot.json
Normal 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 授權。"
|
||||
]
|
||||
}
|
||||
374
scripts/security/package-supply-chain-baseline.py
Normal file
374
scripts/security/package-supply-chain-baseline.py
Normal 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()
|
||||
Reference in New Issue
Block a user