diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 0d24ab0c..69cf3e93 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,29 @@ +## 2026-05-12 | Security Supply Chain docs-only contract manifest + +**背景**:統帥批准 Kali `192.168.0.112`、開發主機 `192.168.0.111` / `192.168.0.168`、Code Review -> Codex、Gitea -> GitHub 長期遷移納入同一個資安工作項目;同時要求初期不要把資安等級一次拉太高,避免產品、架構與流程變得過度複雜。 + +**本次交付**: +- 建立 Kali / Code Review / GitHub / Gitea / Codex / AwoooP 的 docs-only security supply chain scaffold。 +- 建立 `security_finding_v1`、`coding_task_v1`、`source_control_migration_event_v1`、`gitea_repo_inventory_v1`、`local_git_remote_inventory_v1`、`github_target_probe_v1`、`github_target_decision_v1`、`github_target_repo_approval_package_v1`、`local_repo_canonical_probe_v1`、`git_remote_refs_probe_v1`、`approval_required_event_v1`、`security_rollout_policy_v1`、`security_supply_chain_contract_manifest_v1` schema 草案。 +- 產出 Gitea/GitHub refs diff、Gitea public-only inventory、local remote inventory、GitHub target probe、canonical lineage、110 refs probe、repo-by-repo approval package 與 contract manifest snapshot。 +- 明確採低摩擦 `observe-first` / `mirror_only`:LOW / MEDIUM observation 先 observe / warn;只有 read-only token、repo creation、visibility change、refs sync、secret、deploy、primary switch 等高風險動作才進 approval。 + +**邊界**: +- 本輪只做文件、schema、read-only scripts 與 redacted snapshots。 +- 未建立 repo、未修改 visibility、未同步 refs、未切 GitHub primary、未部署、未碰 runtime enforcement。 +- AwoooP 只可 mirror / read-only policy / approval candidate,不可把 manifest 當 execution router。 + +**驗證**: +- JSON / schema parse 通過。 +- `scripts/security/*.py` 可編譯。 +- `git diff --check` 通過。 +- 新增 / 修改內容未命中本輪敏感 token / credential pattern。 + +**下一步**: +- 等 Gitea read-only inventory approval 被批准後,補 private/internal server-side repo list。 +- 逐 repo 取得 owner / visibility / canonical 決策。 +- 對 refs blocked repos 產生 reconcile plan;GitHub primary 仍保持 blocked。 + ## 2026-05-12 | Truth-chain T0 read-only API 第一版 **背景**:完成 Telegram / AwoooP truth-chain live audit 後,下一步先做不改 runtime 的 T0 查詢端點,避免再只靠 Telegram 文案或人工 SQL 判斷流程卡點。 diff --git a/docs/operations/CODE-REVIEW-CODEX-GITEA-OPTIMIZATION.md b/docs/operations/CODE-REVIEW-CODEX-GITEA-OPTIMIZATION.md new file mode 100644 index 00000000..4bfbfc50 --- /dev/null +++ b/docs/operations/CODE-REVIEW-CODEX-GITEA-OPTIMIZATION.md @@ -0,0 +1,324 @@ +# Code Review 接 Codex 與 Gitea 推版優化藍圖 + +| 項目 | 內容 | +|------|------| +| 狀態 | 規劃完成,尚未開始實作 | +| 日期 | 2026-05-06 | +| 範圍 | Code Review、Codex coding 接力、Gitea/GitHub CI/CD 邊界、AI Agent 資安分工 | +| 原則 | 長期以 GitHub 作為雲端主控面,Gitea 保留本地 mirror / fallback,先盤點與同步,再逐步切換 | +| AwoooP 同步 | `docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md` | + +## 背景 + +過去使用 GitHub 作為主要 CI/CD 與 Claude Code 工作入口時,曾發生兩類問題: + +1. Code Review 後需要 coding 的工作沒有穩定接力,導致修補、推版、部署流程變得耗時且複雜。 +2. Claude Code 或 GitHub Actions 工作流使用 GitHub-hosted runner 時,容易消耗 GitHub 免費額度。 + +本專案已依 ADR-039 遷移為: + +```text +Gitea 作為主倉與 CI/CD 執行面 +GitHub 作為只讀備份 +``` + +但從長期可用性角度看,Gitea 部署在本地端,會受到 110 重開機、Docker/Gitea runner、內網服務與儲存狀態影響。若 Git repository、PR、Code Review 與 Codex 接力全部依賴本地 Gitea,開發控制面會跟著本地機房狀態一起中斷。 + +因此本輪修訂長期方向為: + +```text +GitHub 作為雲端主控面:主 repo、PR、Code Review、Codex / Codex Security 接力 +本地 runner / deployment plane:build、Harbor push、K8s deploy、內網驗證 +Gitea 作為本地 mirror / fallback:保留完整版本與緊急離線備援 +``` + +這不是回到舊的「GitHub-hosted runner 全包 CI/CD」模式,而是把穩定的協作與審查控制面放回 GitHub,同時把高耗時、內網依賴、production deploy 的工作留在 self-hosted runner 與本地部署面。 + +## 官方事實基線 + +### GitHub Actions 額度 + +依 GitHub 官方文件: + +- self-hosted runner 使用 GitHub Actions 不消耗 GitHub-hosted runner minutes。 +- public repository 使用標準 GitHub-hosted runner 通常免費。 +- private repository 使用 GitHub-hosted runner 會消耗帳號方案內的免費 minutes、artifact storage、cache storage,超額後可能被阻擋或計費。 +- larger runner 另計,不應視為免費額度。 + +官方來源:[GitHub Actions billing](https://docs.github.com/en/billing/managing-billing-for-your-products/about-billing-for-github-actions)。 + +因此統帥的理解是正確的:GitHub 可以正常使用,但要避免把私有 repo 的日常 CI/CD 跑在 GitHub-hosted runner 上。若必須在 GitHub 跑 workflow,優先使用 self-hosted runner 或只保留極短、手動、低頻的檢查。 + +### Codex 費用與接法 + +依 OpenAI 官方文件: + +- Codex 是可讀取、修改、執行程式碼的 coding agent,可在雲端、CLI、IDE、SDK 等不同介面使用。 +- Codex 可用於 code review、bug fix、測試補齊、資安弱點修補等工作。 +- Codex app / CLI / cloud 會受到使用者方案、credits、rate limits 與工作環境設定影響。 +- 若使用 OpenAI API / Responses API / Codex coding model 自建流程,則走 API token 費用與 API rate limit。 + +官方來源:[Codex cloud docs](https://platform.openai.com/docs/codex/overview)、[Using Codex with your ChatGPT plan](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan%252525252525252525252525252525252525252525252525252525252528.pdf)、[OpenAI API Pricing](https://openai.com/api/pricing/)。 + +因此結論是:可以把 Code Review 後的 coding 工作串接到 Codex,但要分成「使用 Codex 產品額度」與「自建 API 工作流費用」兩種路線管理。 + +## 決策 + +1. 長期目標改為 GitHub primary:GitHub 作為 AWOOOI 的雲端主 repo、PR、Code Review、Codex / Codex Security 接力入口。 +2. Gitea 降級為本地 mirror / fallback / 離線保險,不再長期承擔唯一主控面。 +3. Code Review 後的 coding 工作可以串接 Codex,但第一階段採半自動、人工批准,不直接自動合併或部署。 +4. Claude Code 不作為推版主控;可保留為開發輔助或審查席位,但不得讓它觸發高頻 GitHub-hosted workflow。 +5. Codex 若透過 API 深度整合,需在實作前另外確認費用、token 上限、資料外送與權限邊界。 +6. NemoTron 值得作為資安深度分析與工具調用候選升級,但不取代 OpenClaw 主控。 +7. 所有目前只存在於 Gitea 的專案、分支、tag、release、workflow 版本與部署相關 commit,都必須完整盤點並轉移或同步到 GitHub;完成前不得切換主控面。 + +## 目標流程 + +```text +GitHub push / PR + -> GitHub Code Review / Codex Security / deterministic review + -> deterministic review report + -> coding_task envelope + -> OpenClaw 分派與風險分級 + -> Codex 產生修補分支或 patch + -> 測試與 diff 證據 + -> ElephantAlpha / vuln-verifier 複核 + -> 人工批准 + -> GitHub merge + -> self-hosted runner / 本地 deployment plane + -> Harbor / K8s / Prometheus 驗證 + -> Gitea mirror 同步 + -> LOGBOOK / AI Governance Event / Memory 摘要 +``` + +第一階段只允許 `suggest-only` 與 `patch-only`,不允許自動 merge、force push、production deploy、secret rotation、RBAC/NetworkPolicy/firewall 修改。 + +## Code Review 到 Coding Task 的資料契約 + +建議新增 `coding_task` envelope,承接目前 `.gitea/workflows/code-review.yaml` 與 `scripts/ci_code_review.py` 的 review report。 + +```json +{ + "schema_version": "coding_task_v1", + "source": "github_code_review", + "repo": "awoooi", + "branch": "main", + "base_sha": "string", + "head_sha": "string", + "risk": "LOW|MEDIUM|HIGH|CRITICAL", + "summary": "繁體中文摘要", + "findings": [ + { + "id": "string", + "severity": "LOW|MEDIUM|HIGH|CRITICAL", + "file": "string", + "reason": "繁體中文原因", + "recommended_action": "繁體中文建議" + } + ], + "allowed_actions": [ + "create_patch", + "add_tests", + "open_draft_pr" + ], + "blocked_actions": [ + "auto_merge", + "production_deploy", + "force_push", + "secret_rotation", + "network_policy_change" + ], + "required_reviewers": [ + "critic", + "vuln-verifier" + ] +} +``` + +## Codex 串接策略 + +### 第一階段:Codex 半自動接力 + +適用情境: + +- Code Review 發現格式、測試、低中風險程式問題。 +- 需要補測試、修小 bug、整理型別或錯誤處理。 +- 不涉及 production secret、K8s 權限、資料庫破壞性 migration。 + +建議做法: + +1. GitHub / Gitea Code Review 產生 `coding_task_v1`。 +2. 由 OpenClaw 或人工把 task 交給 Codex CLI / Codex app。 +3. Codex 在隔離 worktree 產生 patch。 +4. 本機或 Gitea runner 跑測試。 +5. 人工確認後再進 GitHub merge,並由 self-hosted runner / 本地 deployment plane 接續驗證。 + +優點:最快落地,費用與權限較容易控管。 + +限制:仍需要人工觸發或人工接力,不是全自動。 + +### 第二階段:Codex SDK / API 工作流 + +適用情境: + +- 需要把 Code Review finding 自動轉成修補分支。 +- 需要整合 AWOOOI approval、AOL、AI governance event、Telegram。 +- 需要精準記錄 token、費用、工具呼叫與安全審批。 + +建議做法: + +1. 新增 `code_review_to_coding_task` job。 +2. 新增 `codex_patch_runner`,僅允許寫入臨時 worktree。 +3. 產生 patch、測試報告與 rollback note。 +4. 只建立 draft PR 或 Gitea branch,不自動合併。 +5. `HIGH/CRITICAL` 一律需要 `vuln-verifier` 與人工批准。 + +優點:可追蹤、可治理、可接進 AWOOOI 飛輪。 + +限制:可能產生額外 API token 費用;需先設 budget、rate limit、資料遮罩與權限範圍。 + +## GitHub / Gitea 長期優化原則 + +### GitHub 長期主控用途 + +- 主 repo 與分支保護。 +- PR、Code Review、Codex / Codex Security 接力入口。 +- GitHub Issues / Pull Requests 作為可被 Codex cloud 與第三方工具穩定消費的協作面。 +- 低頻、短時、非部署型 workflow。 +- self-hosted runner 入口,讓 workflow 控制面在 GitHub,但實際執行可回到本地或專用 runner。 + +### GitHub 避免用途 + +- 私有 repo 高頻 `ubuntu-latest` workflow。 +- push 到 main 後自動跑完整 CI/CD。 +- 長時間 LLM / Claude Code / e2e workflow。 +- artifact/cache 大量保存。 +- 與 Gitea 同時觸發部署,造成雙主控。 + +### Gitea 長期保留用途 + +- 本地 mirror,保留 GitHub 不可用時的離線保險。 +- 內網備援與災難復原。 +- 必要時保留本地只讀查閱與 emergency branch fallback。 +- 轉移完成前,暫時保留現有 Gitea CI/CD,避免一次切換造成 release 中斷。 + +## Gitea 全量轉移到 GitHub 清單 + +這是長期切換的前置工作,必須納入本次資安網工作項目,並以「完整性」作為驗收標準。 + +| 範圍 | 轉移要求 | 驗收證據 | +|------|----------|----------| +| Repositories | 所有 Gitea repositories 都要在 GitHub 建立對應 repo 或明確標記封存 | repo 對照表、GitHub URL、owner | +| Branches | 所有仍有用途的 branches 完整同步 | branch count diff、最新 commit SHA 比對 | +| Tags | release tag、deploy tag、rollback tag 完整同步 | tag count diff、tag SHA 比對 | +| Commit history | 不得只複製 main snapshot;需要完整 Git history | `git fsck`、遠端 SHA 比對 | +| Releases / artifacts | 若 Gitea release 有檔案或說明,需要搬遷或標記替代來源 | release inventory | +| Issues / PR | 若仍有未結事項,需要匯出或轉成 GitHub issue | open item mapping | +| Secrets | 不直接搬 secret value;只建立 secret inventory 與 GitHub Actions secret 名稱 | secret 名稱清單、owner、rotation plan | +| Workflows | `.gitea/workflows` 需盤點並決定改寫、停用或保留本地 fallback | workflow migration matrix | +| Webhooks | Gitea webhook 轉 GitHub webhook 或 AwoooP event adapter | webhook endpoint mapping | +| Deploy markers | deploy-marker commits 與 GitOps revision 要能在 GitHub 追溯 | GitHub commit / tag trace | +| Permissions | GitHub teams、branch protection、required checks、CODEOWNERS | permission review 記錄 | + +轉移過程中,Gitea 不應被直接刪除或停用;至少保留一段並行 mirror 期,直到 GitHub primary、self-hosted runner、Harbor/K8s deploy、rollback 都被驗證。 + +此遷移屬於 supply chain security,必須 mirror 到 AwoooP governance。AwoooP 初期只接收 `source_control_migration_event_v1` 作為 evidence,不直接觸發主控切換或部署。 + +## 建議遷移階段 + +| 階段 | 目標 | 狀態 | +|------|------|------| +| Phase G0 | 盤點所有 Gitea repos、branches、tags、workflows、webhooks、secrets 名稱 | 未開始 | +| Phase G1 | 建立 GitHub repo / team / branch protection / CODEOWNERS | 未開始 | +| Phase G2 | 全量 mirror Git history、branches、tags,不搬 secret value | 未開始 | +| Phase G3 | 將 Code Review / Codex 接力接到 GitHub PR,但 deploy 仍走本地 runner | 未開始 | +| Phase G4 | self-hosted runner 與本地 deployment plane 驗證 Harbor/K8s/Prometheus 全鏈路 | 未開始 | +| Phase G5 | Gitea 改為 mirror / fallback,主開發入口切到 GitHub | 未開始 | +| Phase G6 | 30 天觀察與 rollback drill 後,再移除多餘 Gitea 主控 workflow | 未開始 | + +## AI Agent 分工 + +| 角色 | 建議定位 | +|------|----------| +| OpenClaw | 主控與分派,負責任務路由、權限邊界、approval gate | +| Codex | coding worker,負責修補、測試、refactor、資安 patch 草稿 | +| Codex Security | 程式碼弱點 identification / validation / remediation 候選 | +| NemoTron | 資安深度分析、工具調用、finding triage、風險摘要 | +| Hermes | 知識沉澱、SOP、LOGBOOK、長期脈絡 | +| ElephantAlpha | 反駁、誤判挑戰、修補風險審查 | +| vuln-verifier | secrets、RBAC、NetworkPolicy、K8s、資安邊界複核 | +| migration-engineer | Gitea、Webhook、CI/CD、GitOps、回滾路徑 | +| critic | 架構審查、規範稽核、回歸風險 | + +本專案不建議由單一 Agent 全包。建議採「OpenClaw 主控 + Codex coding + NemoTron 資安分析 + ElephantAlpha 挑戰 + Hermes 記錄」。 + +## NemoTron 更新評估 + +2026-05-12 官方來源校準後,NVIDIA Nemotron / NeMo 的方向確實值得列入 AWOOOI 資安網候選: + +- NVIDIA 已推出 Nemotron 3 family,主打 Nano / Super / Ultra 尺寸與 agentic AI 應用效率。 +- Nemotron 3 Super 是面向 agentic AI 的 MoE / hybrid Mamba-Transformer 模型,官方定位在多步推理與 agent workflow。 +- NVIDIA NeMo 產品線已把 agent toolkit、資料治理、模型最佳化與監控納入 enterprise agent 建置方向。 + +官方來源:[NVIDIA Nemotron 3 family](https://nvidianews.nvidia.com/news/nvidia-debuts-nemotron-3-family-of-open-models)、[Nemotron 3 Super](https://blogs.nvidia.com/blog/nemotron-3-super-agentic-ai/)、[NVIDIA NeMo](https://www.nvidia.com/en-us/ai-data-science/products/nemo/)。 + +但更新策略應為: + +1. 先做官方模型清單與授權確認。 +2. 新增候選模型設定,不直接改 production 預設。 +3. 對歷史 incident / code review / security finding 做 shadow benchmark。 +4. 比較 tool call schema 正確率、誤判率、延遲、費用、繁中品質、secret redaction。 +5. 只在 `suggest-only` 風險分析席位 canary。 +6. 7-14 天指標穩定後再決定是否升級預設模型。 + +## 風險與防護 + +| 風險 | 防護 | +|------|------| +| Codex 自動修出不安全 patch | 第一階段只允許 patch-only,必須人工批准 | +| GitHub 免費額度再次耗盡 | GitHub workflows 避免私有 repo 高頻 GitHub-hosted runner;長任務改 self-hosted | +| Gitea / GitHub 雙主控部署 | 切換前明確指定唯一 deploy controller;GitHub primary 期由 self-hosted runner / 本地 deployment plane 觸發,Gitea 只 mirror | +| Gitea 專案版本漏轉 | 先做 repo/branch/tag/release/workflow/webhook inventory;SHA 比對通過前不切主控 | +| 本地 deployment plane 仍會因重開中斷 | GitHub 只解決控制面可用性;Harbor/K8s/DB/內網 runner 必須另有 cold-start gate | +| LLM 洩漏 secrets | review report 不輸出原始敏感行;送模型前做 redaction | +| Agent 權限過大 | coding task 明列 allowed/blocked actions | +| API 成本失控 | API 化前先設 budget、rate limit、usage metrics、AOL | +| 高風險修補被自動合併 | `HIGH/CRITICAL` 強制 `vuln-verifier` + 人工批准 | + +## 第一波實作清單 + +尚未開始實作。未來若統帥批准,建議順序如下: + +1. 文件與契約:新增 `coding_task_v1` schema。 +2. Gitea/GitHub 版本盤點:列出所有 Gitea repos、branches、tags、workflows、webhooks、secrets 名稱與 GitHub 對應目標。 +3. Gitea Code Review:把 `scripts/ci_code_review.py` 報告轉成 `coding_task_v1`。 +4. 儲存層:將 coding task 寫入既有 AI governance / AOL / artifact 位置。 +5. Codex 半自動:產出可交給 Codex CLI / app 的 task prompt。 +6. 權限閘門:`HIGH/CRITICAL` 必須人工批准,不得自動修補。 +7. 測試證據:Codex patch 必須附測試指令、結果、diff summary。 +8. Draft branch:只建立修補分支或 draft PR,不自動 merge。 +9. 指標:記錄 task 數、成功率、重試率、token/credit、平均修補時間。 +10. GitHub primary ADR:新增或修訂 ADR,明確取代 ADR-039 的長期方向與 rollback plan。 + +## 不在第一波做的事 + +- 不直接串 production deploy。 +- 不自動合併 Codex patch。 +- 不啟用 Codex API 自動長任務。 +- 不讓 Claude Code 觸發 GitHub-hosted 高頻 workflow。 +- 不自動修改 secrets、RBAC、NetworkPolicy、firewall、資料庫破壞性 migration。 +- 不在完成 Gitea 全量版本轉移與 self-hosted runner 驗證前,把 GitHub 切成唯一主控。 + +## 參考來源 + +- [ADR-039: GitHub → Gitea CI/CD 遷移](/Users/ogt/awoooi/docs/adr/ADR-039-gitea-cicd-migration.md) +- [12-Agent 新遊戲規則](/Users/ogt/awoooi/docs/12-agent-game-rules.md) +- [Kali 資訊安全網藍圖](/Users/ogt/awoooi/docs/security/KALI-SECURITY-MESH-BLUEPRINT.md) +- [GitHub Actions billing](https://docs.github.com/en/billing/managing-billing-for-your-products/about-billing-for-github-actions) +- [OpenAI Codex cloud](https://platform.openai.com/docs/codex/overview) +- [Using Codex with your ChatGPT plan](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan%252525252525252525252525252525252525252525252525252525252528.pdf) +- [OpenAI API Pricing](https://openai.com/api/pricing/) +- [OpenAI Code generation / Codex models](https://platform.openai.com/docs/guides/code-generation) +- [NVIDIA Nemotron 3 family](https://nvidianews.nvidia.com/news/nvidia-debuts-nemotron-3-family-of-open-models) +- [NVIDIA Nemotron 3 Super](https://blogs.nvidia.com/blog/nemotron-3-super-agentic-ai/) +- [NVIDIA NeMo](https://www.nvidia.com/en-us/ai-data-science/products/nemo/) diff --git a/docs/schemas/approval_required_event_v1.schema.json b/docs/schemas/approval_required_event_v1.schema.json new file mode 100644 index 00000000..e73b1791 --- /dev/null +++ b/docs/schemas/approval_required_event_v1.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:approval-required-event-v1", + "title": "AWOOOI 需人工核准事件 (v1)", + "description": "高風險資安、Codex patch、GitHub/Gitea 主控切換、credentialed scan 等需要 AwoooP approval 的事件契約。", + "type": "object", + "required": [ + "schema_version", + "source_event_type", + "source_event_id", + "risk", + "requested_action", + "reason", + "required_reviewers", + "blocked_until_approved" + ], + "properties": { + "schema_version": { + "const": "approval_required_event_v1" + }, + "source_event_type": { + "type": "string", + "enum": [ + "security_finding_v1", + "coding_task_v1", + "source_control_migration_event_v1", + "gitea_repo_inventory_v1", + "github_target_decision_v1", + "manual" + ] + }, + "source_event_id": { + "type": "string", + "minLength": 1 + }, + "risk": { + "type": "string", + "enum": ["MEDIUM", "HIGH", "CRITICAL"] + }, + "requested_action": { + "type": "string", + "enum": [ + "run_credentialed_scan", + "run_active_dast", + "create_codex_patch", + "merge_patch", + "production_deploy", + "rotate_secret", + "change_rbac", + "change_network_policy", + "change_firewall", + "run_gitea_readonly_inventory", + "import_gitea_admin_export", + "create_github_repo", + "change_repo_visibility", + "sync_git_refs", + "switch_github_primary" + ] + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "required_reviewers": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "critic", + "vuln-verifier", + "migration-engineer", + "security-commander", + "human-owner" + ] + }, + "uniqueItems": true + }, + "blocked_until_approved": { + "type": "boolean", + "const": true + }, + "evidence_refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/coding_task_v1.schema.json b/docs/schemas/coding_task_v1.schema.json new file mode 100644 index 00000000..f479dd0d --- /dev/null +++ b/docs/schemas/coding_task_v1.schema.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:coding-task-v1", + "title": "AWOOOI Coding 任務事件 (v1)", + "description": "Code Review 後交給 Codex 或其他 coding worker 的標準任務契約。初期只允許 patch-only / suggest-only。", + "type": "object", + "required": [ + "schema_version", + "source", + "repo", + "branch", + "base_sha", + "head_sha", + "risk", + "summary", + "allowed_actions", + "blocked_actions", + "required_reviewers" + ], + "properties": { + "schema_version": { + "const": "coding_task_v1" + }, + "source": { + "type": "string", + "enum": [ + "github_code_review", + "gitea_code_review", + "codex_security", + "manual_review" + ] + }, + "repo": { + "type": "string", + "minLength": 1 + }, + "branch": { + "type": "string", + "minLength": 1 + }, + "base_sha": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + }, + "head_sha": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + }, + "risk": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "findings": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "severity", "file", "reason", "recommended_action"], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "severity": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "file": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "recommended_action": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "allowed_actions": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "create_patch", + "add_tests", + "open_draft_pr" + ] + }, + "uniqueItems": true + }, + "blocked_actions": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "auto_merge", + "production_deploy", + "force_push", + "secret_rotation", + "network_policy_change" + ] + }, + "uniqueItems": true + }, + "required_reviewers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "critic", + "vuln-verifier", + "migration-engineer", + "security-commander", + "human-owner" + ] + }, + "uniqueItems": true + }, + "evidence_refs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/git_remote_refs_probe_v1.schema.json b/docs/schemas/git_remote_refs_probe_v1.schema.json new file mode 100644 index 00000000..5c62c951 --- /dev/null +++ b/docs/schemas/git_remote_refs_probe_v1.schema.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:git-remote-refs-probe-v1", + "title": "AWOOOI Git Remote Refs Probe 事件 (v1)", + "description": "指定本機 Git repo 的 remote refs read-only 探測事件。此事件不授權 fetch、push、同步或主控切換。", + "type": "object", + "required": [ + "schema_version", + "group_name", + "status", + "repo_count", + "aligned_current_branch_count", + "unreachable_count", + "repos" + ], + "properties": { + "schema_version": { + "const": "git_remote_refs_probe_v1" + }, + "group_name": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": ["ok", "partial"] + }, + "repo_count": { + "type": "integer", + "minimum": 0 + }, + "aligned_current_branch_count": { + "type": "integer", + "minimum": 0 + }, + "unreachable_count": { + "type": "integer", + "minimum": 0 + }, + "repos": { + "type": "array", + "items": { + "type": "object", + "required": [ + "label", + "repo_path", + "remote", + "remote_url_redacted", + "status", + "local_branch", + "local_head", + "remote_current_branch_sha", + "head_count", + "tag_count", + "heads", + "tags", + "error_summary" + ], + "properties": { + "label": { + "type": "string" + }, + "repo_path": { + "type": "string" + }, + "remote": { + "type": "string" + }, + "remote_url_redacted": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "aligned_current_branch", + "reachable_drift_or_unknown", + "reachable_no_heads", + "unreachable" + ] + }, + "local_branch": { + "type": "string" + }, + "local_head": { + "type": "string" + }, + "remote_current_branch_sha": { + "type": "string" + }, + "head_count": { + "type": "integer", + "minimum": 0 + }, + "tag_count": { + "type": "integer", + "minimum": 0 + }, + "heads": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "sha"], + "properties": { + "name": { + "type": "string" + }, + "sha": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + } + }, + "additionalProperties": false + } + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "sha"], + "properties": { + "name": { + "type": "string" + }, + "sha": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + } + }, + "additionalProperties": false + } + }, + "error_summary": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/gitea_repo_inventory_v1.schema.json b/docs/schemas/gitea_repo_inventory_v1.schema.json new file mode 100644 index 00000000..12f4e633 --- /dev/null +++ b/docs/schemas/gitea_repo_inventory_v1.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:gitea-repo-inventory-v1", + "title": "AWOOOI Gitea Repo 全量盤點事件 (v1)", + "description": "Gitea 全量 repo 清單的 read-only 盤點事件。此事件只作為 GitHub primary 遷移前 evidence,不授權同步、刪除或主控切換。", + "type": "object", + "required": [ + "schema_version", + "base_url", + "org", + "github_owner", + "query_mode", + "query", + "visibility_scope", + "token_present", + "http_status", + "status", + "blocking_reason", + "repo_count", + "repos" + ], + "properties": { + "schema_version": { + "const": "gitea_repo_inventory_v1" + }, + "base_url": { + "type": "string", + "minLength": 1 + }, + "org": { + "type": "string", + "minLength": 1 + }, + "github_owner": { + "type": "string" + }, + "query_mode": { + "type": "string", + "enum": ["org", "user", "search", "export"] + }, + "query": { + "type": "string" + }, + "visibility_scope": { + "type": "string", + "enum": ["public_only", "authenticated", "admin_export", "unknown"], + "description": "public_only 代表未提供 token 時的公開可見範圍,不等同 server 全量。" + }, + "token_present": { + "type": "boolean", + "description": "只表示執行時是否有提供 token,不儲存 token value。" + }, + "http_status": { + "type": ["integer", "null"], + "minimum": 100, + "maximum": 599 + }, + "status": { + "type": "string", + "enum": ["ok", "partial", "blocked"] + }, + "blocking_reason": { + "type": "string" + }, + "repo_count": { + "type": "integer", + "minimum": 0 + }, + "repos": { + "type": "array", + "items": { + "type": "object", + "required": [ + "gitea_repo", + "name", + "owner", + "private", + "empty", + "archived", + "default_branch", + "clone_url_redacted", + "ssh_url_redacted", + "github_repo_candidate" + ], + "properties": { + "gitea_repo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "private": { + "type": "boolean" + }, + "empty": { + "type": "boolean" + }, + "archived": { + "type": "boolean" + }, + "default_branch": { + "type": "string" + }, + "clone_url_redacted": { + "type": "string" + }, + "ssh_url_redacted": { + "type": "string" + }, + "github_repo_candidate": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/github_target_decision_v1.schema.json b/docs/schemas/github_target_decision_v1.schema.json new file mode 100644 index 00000000..0116c1e6 --- /dev/null +++ b/docs/schemas/github_target_decision_v1.schema.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:github-target-decision-v1", + "title": "AWOOOI GitHub Target Decision 事件 (v1)", + "description": "GitHub target repo 建立、可見性、封存或待判定的 read-only 決策草案。此事件不授權建立 repo、修改 visibility、同步 refs 或主控切換。", + "type": "object", + "required": [ + "schema_version", + "status", + "decision_count", + "approval_required_count", + "decisions" + ], + "properties": { + "schema_version": { + "const": "github_target_decision_v1" + }, + "status": { + "type": "string", + "enum": ["draft"] + }, + "decision_count": { + "type": "integer", + "minimum": 0 + }, + "approval_required_count": { + "type": "integer", + "minimum": 0 + }, + "decisions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "github_repo", + "source_key", + "probe_status", + "target_state", + "recommended_action", + "risk", + "approval_required", + "blocked_until", + "evidence_refs", + "notes" + ], + "properties": { + "github_repo": { + "type": "string" + }, + "source_key": { + "type": "string" + }, + "probe_status": { + "type": "string" + }, + "target_state": { + "type": "string", + "enum": [ + "exists_refs_blocked", + "exists_aligned", + "not_found_or_private", + "external_scope" + ] + }, + "recommended_action": { + "type": "string", + "enum": [ + "hold_refs_reconcile", + "create_or_grant_access_after_approval", + "confirm_internal_remote_purpose", + "scope_review_only" + ] + }, + "risk": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "approval_required": { + "type": "boolean" + }, + "blocked_until": { + "type": "array", + "items": { + "type": "string" + } + }, + "evidence_refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "notes": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/github_target_probe_v1.schema.json b/docs/schemas/github_target_probe_v1.schema.json new file mode 100644 index 00000000..13d0d1af --- /dev/null +++ b/docs/schemas/github_target_probe_v1.schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:github-target-probe-v1", + "title": "AWOOOI GitHub Target Probe 事件 (v1)", + "description": "候選 GitHub repo read-only 存在性探測。此事件只表示未授權 read-only probe 的可見狀態,不授權建立、刪除或同步 repo。", + "type": "object", + "required": [ + "schema_version", + "status", + "candidate_count", + "exists_count", + "not_found_or_private_count", + "candidates" + ], + "properties": { + "schema_version": { + "const": "github_target_probe_v1" + }, + "status": { + "type": "string", + "enum": ["ok"] + }, + "candidate_count": { + "type": "integer", + "minimum": 0 + }, + "exists_count": { + "type": "integer", + "minimum": 0 + }, + "not_found_or_private_count": { + "type": "integer", + "minimum": 0 + }, + "candidates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "github_repo", + "url_redacted", + "status", + "head_count", + "heads", + "error_summary" + ], + "properties": { + "github_repo": { + "type": "string" + }, + "url_redacted": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["exists", "exists_empty_or_no_heads", "not_found_or_private", "timeout", "error"] + }, + "head_count": { + "type": "integer", + "minimum": 0 + }, + "heads": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "sha"], + "properties": { + "name": { + "type": "string" + }, + "sha": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + } + }, + "additionalProperties": false + } + }, + "error_summary": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/github_target_repo_approval_package_v1.schema.json b/docs/schemas/github_target_repo_approval_package_v1.schema.json new file mode 100644 index 00000000..42ca6c32 --- /dev/null +++ b/docs/schemas/github_target_repo_approval_package_v1.schema.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:github-target-repo-approval-package-v1", + "title": "AWOOOI GitHub Target Repo-by-repo Approval Package (v1)", + "description": "把 GitHub target decision 中需要人工批准的 repo 拆成逐 repo approval package。此 package 不授權建立 repo、修改 visibility、同步 refs 或 GitHub primary 切換。", + "type": "object", + "required": [ + "schema_version", + "status", + "source_snapshot", + "package_count", + "approval_items" + ], + "properties": { + "schema_version": { + "const": "github_target_repo_approval_package_v1" + }, + "status": { + "type": "string", + "enum": ["draft"] + }, + "source_snapshot": { + "type": "string", + "minLength": 1 + }, + "package_count": { + "type": "integer", + "minimum": 0 + }, + "approval_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "github_repo", + "source_key", + "risk", + "approval_action", + "approval_status", + "required_reviewers", + "blocked_until", + "allowed_after_approval", + "still_forbidden", + "evidence_refs", + "notes" + ], + "properties": { + "github_repo": { + "type": "string" + }, + "source_key": { + "type": "string" + }, + "risk": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "approval_action": { + "type": "string", + "enum": [ + "reconcile_refs_after_full_inventory", + "create_or_grant_access_after_canonical_approval", + "confirm_internal_remote_purpose" + ] + }, + "approval_status": { + "type": "string", + "enum": ["pending"] + }, + "required_reviewers": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "blocked_until": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowed_after_approval": { + "type": "array", + "items": { + "type": "string" + } + }, + "still_forbidden": { + "type": "array", + "items": { + "type": "string" + } + }, + "evidence_refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "notes": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/local_git_remote_inventory_v1.schema.json b/docs/schemas/local_git_remote_inventory_v1.schema.json new file mode 100644 index 00000000..0271099f --- /dev/null +++ b/docs/schemas/local_git_remote_inventory_v1.schema.json @@ -0,0 +1,192 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:local-git-remote-inventory-v1", + "title": "AWOOOI 本機 Git Remote 盤點事件 (v1)", + "description": "本機可見 Git working tree 的 read-only remote 盤點事件。此事件只作為 Gitea server 全量盤點受阻時的輔助 evidence,不授權同步、刪除或主控切換。", + "type": "object", + "required": [ + "schema_version", + "status", + "roots", + "max_depth", + "gitea_host_fragment", + "repo_count", + "gitea_linked_count", + "github_linked_count", + "mapped_count", + "gitea_only_count", + "github_only_count", + "internal_110_only_count", + "unique_gitea_repo_count", + "unique_github_repo_count", + "unique_internal_110_repo_count", + "unique_gitea_repos", + "unique_github_repos", + "unique_internal_110_repos", + "repos" + ], + "properties": { + "schema_version": { + "const": "local_git_remote_inventory_v1" + }, + "status": { + "type": "string", + "enum": ["partial", "empty"] + }, + "roots": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "max_depth": { + "type": "integer", + "minimum": 0 + }, + "gitea_host_fragment": { + "type": "string", + "minLength": 1 + }, + "repo_count": { + "type": "integer", + "minimum": 0 + }, + "gitea_linked_count": { + "type": "integer", + "minimum": 0 + }, + "github_linked_count": { + "type": "integer", + "minimum": 0 + }, + "mapped_count": { + "type": "integer", + "minimum": 0 + }, + "gitea_only_count": { + "type": "integer", + "minimum": 0 + }, + "github_only_count": { + "type": "integer", + "minimum": 0 + }, + "internal_110_only_count": { + "type": "integer", + "minimum": 0 + }, + "unique_gitea_repo_count": { + "type": "integer", + "minimum": 0 + }, + "unique_github_repo_count": { + "type": "integer", + "minimum": 0 + }, + "unique_internal_110_repo_count": { + "type": "integer", + "minimum": 0 + }, + "unique_gitea_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "unique_github_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "unique_internal_110_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "repos": { + "type": "array", + "items": { + "type": "object", + "required": [ + "repo_path", + "repo_name", + "status", + "gitea_repos", + "github_repos", + "internal_110_repos", + "remotes" + ], + "properties": { + "repo_path": { + "type": "string" + }, + "repo_name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "mapped", + "gitea_only_local", + "github_only_local", + "internal_110_only", + "other_remote" + ] + }, + "gitea_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "github_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "internal_110_repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "remotes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "kind", + "url_redacted", + "repo_slug" + ], + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["gitea", "github", "internal_git_110", "gitlab_110", "other"] + }, + "url_redacted": { + "type": "string" + }, + "repo_slug": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/local_repo_canonical_probe_v1.schema.json b/docs/schemas/local_repo_canonical_probe_v1.schema.json new file mode 100644 index 00000000..71fa04c4 --- /dev/null +++ b/docs/schemas/local_repo_canonical_probe_v1.schema.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:local-repo-canonical-probe-v1", + "title": "AWOOOI 本機 Repo Canonical Lineage Probe 事件 (v1)", + "description": "本機 Git working tree 的 read-only lineage 比對事件。此事件只作為 canonical repo 判定 evidence,不授權合併、同步、刪除或主控切換。", + "type": "object", + "required": [ + "schema_version", + "group_name", + "status", + "sample_limit", + "git_timeout_seconds", + "repo_count", + "comparison_count", + "repos", + "comparisons" + ], + "properties": { + "schema_version": { + "const": "local_repo_canonical_probe_v1" + }, + "group_name": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": ["related", "unrelated", "mixed", "partial"] + }, + "sample_limit": { + "type": "integer", + "minimum": 1 + }, + "git_timeout_seconds": { + "type": "integer", + "minimum": 1 + }, + "repo_count": { + "type": "integer", + "minimum": 0 + }, + "comparison_count": { + "type": "integer", + "minimum": 0 + }, + "repos": { + "type": "array", + "items": { + "type": "object", + "required": [ + "label", + "repo_path", + "exists", + "head_sha", + "head_short", + "branch", + "commit_sample_count", + "commits", + "remotes", + "probe_error" + ], + "properties": { + "label": { + "type": "string" + }, + "repo_path": { + "type": "string" + }, + "exists": { + "type": "boolean" + }, + "head_sha": { + "type": "string" + }, + "head_short": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "commit_sample_count": { + "type": "integer", + "minimum": 0 + }, + "commits": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + } + }, + "remotes": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "url_redacted"], + "properties": { + "name": { + "type": "string" + }, + "url_redacted": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "probe_error": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "comparisons": { + "type": "array", + "items": { + "type": "object", + "required": [ + "left_label", + "right_label", + "relation", + "left_head", + "right_head", + "common_commit_count", + "common_commit_samples" + ], + "properties": { + "left_label": { + "type": "string" + }, + "right_label": { + "type": "string" + }, + "relation": { + "type": "string", + "enum": [ + "same_head", + "left_descends_from_right", + "right_descends_from_left", + "shared_history", + "no_shared_history", + "partial_probe", + "missing_repo" + ] + }, + "left_head": { + "type": "string" + }, + "right_head": { + "type": "string" + }, + "common_commit_count": { + "type": "integer", + "minimum": 0 + }, + "common_commit_samples": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/security_finding_v1.schema.json b/docs/schemas/security_finding_v1.schema.json new file mode 100644 index 00000000..eb68a5ff --- /dev/null +++ b/docs/schemas/security_finding_v1.schema.json @@ -0,0 +1,146 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:security-finding-v1", + "title": "AWOOOI 資安發現事件 (v1)", + "description": "資安掃描與驗證結果的標準事件契約。初期只作為 observe-only / mirror-only evidence,不授權自動封鎖或自動修復。", + "type": "object", + "required": [ + "schema_version", + "finding_id", + "scan_run_id", + "scanner", + "asset_key", + "target_type", + "target", + "category", + "severity", + "confidence", + "recommended_mode", + "evidence_ref", + "summary", + "recommended_action" + ], + "properties": { + "schema_version": { + "const": "security_finding_v1" + }, + "finding_id": { + "type": "string", + "minLength": 8, + "description": "穩定且可重算的 fingerprint。" + }, + "scan_run_id": { + "type": "string", + "minLength": 1 + }, + "scanner": { + "type": "string", + "enum": [ + "kali", + "trivy", + "zap", + "semgrep", + "detect-secrets", + "kube-bench", + "manual", + "other" + ] + }, + "scanner_version": { + "type": "string" + }, + "asset_key": { + "type": "string", + "minLength": 1 + }, + "target_type": { + "type": "string", + "enum": [ + "host", + "website", + "api_endpoint", + "container", + "package", + "repo", + "k8s_resource", + "tool" + ] + }, + "target": { + "type": "string", + "minLength": 1, + "description": "已脫敏 target identifier,不可含 raw secret、cookie 或 exploit payload。" + }, + "category": { + "type": "string", + "enum": [ + "exposure", + "cve", + "secret", + "misconfig", + "auth", + "tls", + "web", + "code", + "supply_chain", + "network" + ] + }, + "severity": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "confidence": { + "type": "string", + "enum": ["LOW", "MEDIUM", "HIGH"] + }, + "status": { + "type": "string", + "enum": [ + "new", + "confirmed", + "false_positive", + "accepted_risk", + "fixed", + "expired" + ], + "default": "new" + }, + "recommended_mode": { + "type": "string", + "enum": [ + "observe", + "warn", + "approve_required", + "block_candidate" + ] + }, + "evidence_ref": { + "type": "string", + "minLength": 1, + "description": "指向已脫敏 evidence 的 reference。" + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "recommended_action": { + "type": "string", + "minLength": 1 + }, + "owner_team": { + "type": "string" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/security_rollout_policy_v1.schema.json b/docs/schemas/security_rollout_policy_v1.schema.json new file mode 100644 index 00000000..27269acb --- /dev/null +++ b/docs/schemas/security_rollout_policy_v1.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:security-rollout-policy-v1", + "title": "AWOOOI 低摩擦資安 Rollout Policy (v1)", + "description": "定義 Security Supply Chain 初期如何以 observe / warn / approve_required / block_candidate 分階段收斂,避免一開始把資安限制拉太高。", + "type": "object", + "required": [ + "schema_version", + "status", + "default_mode", + "enforcement_level", + "policy_items" + ], + "properties": { + "schema_version": { + "const": "security_rollout_policy_v1" + }, + "status": { + "type": "string", + "enum": ["draft"] + }, + "default_mode": { + "type": "string", + "enum": ["observe"] + }, + "enforcement_level": { + "type": "string", + "enum": ["mirror_only", "read_only_policy", "approval_gate", "enforced"] + }, + "policy_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "condition", + "mode", + "allowed", + "forbidden", + "reason" + ], + "properties": { + "condition": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["observe", "warn", "approve_required", "block_candidate"] + }, + "allowed": { + "type": "array", + "items": { + "type": "string" + } + }, + "forbidden": { + "type": "array", + "items": { + "type": "string" + } + }, + "reason": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/security_supply_chain_contract_manifest_v1.schema.json b/docs/schemas/security_supply_chain_contract_manifest_v1.schema.json new file mode 100644 index 00000000..ae813af9 --- /dev/null +++ b/docs/schemas/security_supply_chain_contract_manifest_v1.schema.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:security-supply-chain-contract-manifest-v1", + "title": "AWOOOI Security Supply Chain Contract Manifest (v1)", + "description": "集中列出 Security Supply Chain 初期可供 AwoooP mirror/read-only 消費的 schema、snapshot 與人讀文件。", + "type": "object", + "required": [ + "schema_version", + "status", + "default_enforcement_level", + "contract_count", + "contracts" + ], + "properties": { + "schema_version": { + "const": "security_supply_chain_contract_manifest_v1" + }, + "status": { + "type": "string", + "enum": ["draft"] + }, + "default_enforcement_level": { + "type": "string", + "enum": ["mirror_only"] + }, + "contract_count": { + "type": "integer", + "minimum": 0 + }, + "contracts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "contract", + "schema_path", + "snapshot_paths", + "human_docs", + "consumer", + "consumption_mode", + "allowed_actions", + "forbidden_actions", + "notes" + ], + "properties": { + "contract": { + "type": "string" + }, + "schema_path": { + "type": "string" + }, + "snapshot_paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "human_docs": { + "type": "array", + "items": { + "type": "string" + } + }, + "consumer": { + "type": "string" + }, + "consumption_mode": { + "type": "string", + "enum": ["mirror_only", "read_only_policy", "suggest_only", "approval_only"] + }, + "allowed_actions": { + "type": "array", + "items": { + "type": "string" + } + }, + "forbidden_actions": { + "type": "array", + "items": { + "type": "string" + } + }, + "notes": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/docs/schemas/source_control_migration_event_v1.schema.json b/docs/schemas/source_control_migration_event_v1.schema.json new file mode 100644 index 00000000..0bae656a --- /dev/null +++ b/docs/schemas/source_control_migration_event_v1.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:source-control-migration-event-v1", + "title": "AWOOOI Source Control 遷移事件 (v1)", + "description": "Gitea 全量版本轉移到 GitHub 的供應鏈安全證據契約。此事件只表示盤點、同步與驗證狀態,不授權主控切換。", + "type": "object", + "required": [ + "schema_version", + "gitea_repo", + "github_repo", + "branch_count_gitea", + "branch_count_github", + "tag_count_gitea", + "tag_count_github", + "latest_sha_gitea", + "latest_sha_github", + "workflows_mapped", + "webhooks_mapped", + "secrets_inventory_only", + "status" + ], + "properties": { + "schema_version": { + "const": "source_control_migration_event_v1" + }, + "gitea_repo": { + "type": "string", + "minLength": 1 + }, + "github_repo": { + "type": "string", + "minLength": 1 + }, + "branch_count_gitea": { + "type": "integer", + "minimum": 0 + }, + "branch_count_github": { + "type": "integer", + "minimum": 0 + }, + "tag_count_gitea": { + "type": "integer", + "minimum": 0 + }, + "tag_count_github": { + "type": "integer", + "minimum": 0 + }, + "latest_sha_gitea": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + }, + "latest_sha_github": { + "type": "string", + "pattern": "^[0-9a-fA-F]{7,40}$" + }, + "workflows_mapped": { + "type": "boolean" + }, + "webhooks_mapped": { + "type": "boolean" + }, + "secrets_inventory_only": { + "type": "boolean", + "const": true, + "description": "Secrets 只盤點名稱與 owner,不搬 secret value。" + }, + "status": { + "type": "string", + "enum": [ + "inventory", + "mirrored", + "verified", + "blocked" + ] + }, + "blocking_reason": { + "type": "string" + }, + "evidence_refs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md new file mode 100644 index 00000000..13d97b03 --- /dev/null +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -0,0 +1,137 @@ +# AwoooP 只讀鏡像消費清單 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 初版,供 AwoooP Session 只讀消費 | +| 範圍 | Kali / Code Review / Codex / Gitea / GitHub 資安供應鏈事件 | +| 低摩擦 policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | +| Contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` | +| 原則 | 低摩擦分階段;AwoooP 初期只 mirror、只讀 policy、只建立必要的 approval candidate,不直接執行 | + +## 0. 核心結論 + +AwoooP 可以消費 Security Supply Chain Session 產出的事件,但初期只能做三件事: + +1. mirror 成 Runtime State / Channel Event / Audit evidence。 +2. 計算 read-only policy 建議,例如 `observe`、`warn`、`approve_required`。 +3. 產生 approval candidate,等待人工核准。 + +AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得切換 GitHub primary、不得修改 production runtime。 + +初期也不得把每個 observation 都變成阻擋條件。LOW / MEDIUM 且不涉及不可逆變更的項目,先以 `observe` / `warn` 累積 evidence;只有 repo creation、visibility change、refs sync、secret、RBAC、NetworkPolicy、firewall、deploy、primary switch 等高風險動作才進 approval。 + +## 1. 允許消費的事件 + +| 事件 | 來源 | AwoooP 目標 | 初期狀態 | 必要防護 | +|------|------|-------------|----------|----------| +| `security_finding_v1` | Kali / Trivy / ZAP / Semgrep / detect-secrets / kube posture | Runtime State、Channel Event、Audit | mirror-only | 不保存 raw secret、cookie、token、exploit payload | +| `coding_task_v1` | Code Review / Codex Security / manual review | Approval candidate、Channel Event、Audit | suggest-only | 不自動開 patch runner、不自動 merge | +| `source_control_migration_event_v1` | Gitea/GitHub branch/tag/SHA diff | Supply-chain evidence、Approval candidate | mirror-only | 不觸發 deploy、不切換 primary | +| `gitea_repo_inventory_v1` | Gitea org/user repo list 或管理匯出 | Supply-chain evidence、migration matrix | mirror-only | 不保存 token value、不刪除或停用 Gitea repo | +| `local_git_remote_inventory_v1` | 本機可見 Git working tree remote | Source-control coverage evidence、migration matrix | mirror-only | 不視為 Gitea server 全量、不修改 remote | +| `github_target_probe_v1` | 候選 GitHub repo read-only probe | Migration target evidence | mirror-only | `not_found_or_private` 不等同確認不存在 | +| `github_target_decision_v1` | GitHub target 建立與可見性決策草案 | Approval candidate、Migration target evidence | mirror-only | approval 前不得建立 repo、修改 visibility、同步 refs | +| `github_target_repo_approval_package_v1` | GitHub target 逐 repo approval package | Approval queue、Migration target evidence | mirror-only | 低摩擦,只 gate 高風險執行 | +| `local_repo_canonical_probe_v1` | 本機 working tree lineage 比對 | Canonical decision evidence | mirror-only | 不自動合併、不自動建 repo、不刪除 | +| `git_remote_refs_probe_v1` | 指定 repo remote refs read-only probe | Source readiness evidence | mirror-only | 不 fetch、不 push、不自動 mirror | +| `approval_required_event_v1` | 上述事件的高風險 gate | Approval queue、Audit | approval-only | `blocked_until_approved=true` | +| `security_rollout_policy_v1` | 低摩擦資安 rollout policy | Read-only policy、Operator Console | mirror-only | 初期 observe-first,不做 runtime enforcement | +| `security_supply_chain_contract_manifest_v1` | Security Supply Chain 契約索引 | Contract registry、Operator Console | mirror-only | 只作路由索引,不作 execution router | + +## 2. AwoooP 可以做的處理 + +| 處理 | 允許 | 說明 | +|------|------|------| +| Runtime State mirror | 是 | 儲存脫敏後摘要、狀態、風險等級、evidence ref | +| Channel Event | 是 | 發送資安 posture、遷移阻塞、approval pending 等通知 | +| Read-only policy | 是 | 計算建議,不改 firewall、RBAC、NetworkPolicy、secret、deploy | +| Approval candidate | 是 | 讓人審核是否批准下一步 | +| Audit evidence | 是 | 保存可追溯事件,不保存敏感原文 | +| Operator Console 顯示 | 是 | 初期只顯示,不提供高風險 action button | + +## 3. 初期禁止動作 + +| 禁止動作 | 原因 | +|----------|------| +| 直接啟動 Kali scan | 掃描強度與範圍需人工批准,避免誤傷內網服務 | +| 直接啟動 active DAST / credentialed scan | 會碰驗證狀態與服務負載,需 approval | +| 直接呼叫 Codex patch runner | coding 仍需 patch-only / human review gate | +| 自動 merge / auto deploy | 供應鏈與 production 風險太高 | +| 修改 secrets / RBAC / NetworkPolicy / firewall | 高風險不可逆或半不可逆變更 | +| 切換 GitHub primary / Gitea mirror 主控 | 目前 Gitea/GitHub branch、tag、main SHA 尚未對齊 | +| 刪除、停用、歸檔 Gitea repo | 需要完整 repo inventory 與人工確認 | +| 保存 raw secret / token / cookie / exploit payload | 違反 evidence 脫敏原則 | + +## 4. 事件到處理模式對照 + +| 條件 | AwoooP recommended mode | 後續 | +|------|-------------------------|------| +| `security_finding_v1.severity=LOW|MEDIUM` 且 `confidence=LOW|MEDIUM` | `observe` | mirror + weekly review | +| `security_finding_v1.severity=HIGH|CRITICAL` | `approve_required` | 產生 `approval_required_event_v1` | +| `coding_task_v1.risk=LOW|MEDIUM` | `warn` | 可排入 Codex patch-only backlog | +| `coding_task_v1.risk=HIGH|CRITICAL` | `approve_required` | 必須指定 `critic`、`vuln-verifier` | +| `source_control_migration_event_v1.status=blocked` | `observe` | 顯示 blocking reason,不允許切 primary | +| `source_control_migration_event_v1.status=verified` | `approve_required` | 仍需人工批准主控切換 | +| `gitea_repo_inventory_v1.status=blocked` | `observe` | 補只讀 token 或管理匯出,不做同步 | +| `gitea_repo_inventory_v1.status=partial` | `observe` | 視為 public-only evidence,不做同步 | +| `gitea_repo_inventory_v1.status=ok` | `warn` | 進入 repo mapping / branch tag diff | +| `approval_required_event_v1.requested_action=run_gitea_readonly_inventory` | `approve_required` | 只允許 read-only token 或 redacted admin export,不保存 token value | +| `local_git_remote_inventory_v1.status=partial` | `observe` | 補 server-side inventory,不做主控切換 | +| `github_target_probe_v1.status=ok` 且有 `not_found_or_private` | `observe` | 補 GitHub target 決策,不自動建立 repo | +| `github_target_decision_v1.approval_required_count>0` | `approve_required` | 產生 approval candidate,但不執行 repo 建立或 visibility 修改 | +| `github_target_repo_approval_package_v1.status=draft` | `observe` | 建立 approval queue draft,不阻擋 read-only evidence | +| `local_repo_canonical_probe_v1.status=unrelated` | `approve_required` | 禁止自動合併,需人工 canonical 判定 | +| `git_remote_refs_probe_v1.status=ok` | `observe` | 可作 source evidence,但仍需 GitHub target 與 approval | +| `security_rollout_policy_v1.enforcement_level=mirror_only` | `observe` | 只顯示 policy,不阻擋既有流程 | +| `security_supply_chain_contract_manifest_v1.default_enforcement_level=mirror_only` | `observe` | 只載入契約索引,不新增執行入口 | + +## 5. Evidence 脫敏要求 + +| 資料 | 保存方式 | +|------|----------| +| repo URL | 移除 username、password、token | +| API token | 只保存 `token_present=true|false` | +| secret 名稱 | 可保存名稱與 owner | +| secret value | 禁止保存 | +| exploit payload | 禁止保存 raw payload,只保存 redacted evidence ref | +| scan result | 保存 finding 摘要、severity、confidence、asset key | +| host IP | 內網資產可保存,但外部分享需改 asset key | + +## 6. 目前可立即 mirror 的檔案 + +| 類型 | 檔案 | +|------|------| +| source control event snapshot | `docs/security/gitea-github-awoooi-inventory.snapshot.json` | +| source control event 人讀版 | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | +| clawbot-v5 source control snapshot | `docs/security/source-control-clawbot-v5.snapshot.json` / `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` | +| wooo-aiops source control snapshot | `docs/security/source-control-wooo-aiops.snapshot.json` / `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` | +| Gitea repo inventory snapshot | `docs/security/gitea-repo-inventory.snapshot.json` | +| Gitea repo inventory 人讀版 | `docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` | +| Gitea org endpoint blocked snapshot | `docs/security/gitea-org-repo-inventory-blocked.snapshot.json` / `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md` | +| Gitea server-side inventory runbook | `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md` | +| Gitea read-only inventory approval package | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` | +| Gitea read-only inventory approval snapshot | `docs/security/gitea-readonly-inventory-approval.snapshot.json` | +| Gitea admin export redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` | +| Gitea public repo search snapshot | `docs/security/gitea-public-repo-search.snapshot.json` / `docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` | +| 本機 Git remote inventory snapshot | `docs/security/local-git-remote-inventory.snapshot.json` | +| 本機 Git remote inventory 人讀版 | `docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md` | +| Source Control 遷移矩陣 | `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | +| Source Control Canonical Repo 判定表 | `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md` | +| GitHub target probe snapshot | `docs/security/github-target-probe.snapshot.json` / `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` | +| GitHub target 決策 snapshot | `docs/security/github-target-decision.snapshot.json` / `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | +| GitHub target repo approval package | `docs/security/github-target-repo-approval-package.snapshot.json` / `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | +| 本機 repo canonical lineage snapshot | `docs/security/local-repo-canonical-ewoooc-momo.snapshot.json` / `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | +| Internal 110 refs snapshot | `docs/security/git-remote-refs-bitan-tsenyang.snapshot.json` / `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | +| wooo-infra-config refs snapshot | `docs/security/git-remote-refs-wooo-infra-config.snapshot.json` / `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | +| Gitea/GitHub migration inventory | `docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md` | +| AwoooP handoff | `docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md` | +| 低摩擦資安 rollout policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` / `docs/security/security-rollout-policy.snapshot.json` | +| Security Supply Chain contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` / `docs/security/security-supply-chain-contract-manifest.snapshot.json` | + +## 7. 下一步 + +1. AwoooP 主線先把本清單視為契約消費檢查清單。 +2. Security Supply Chain Session 補齊 Gitea 全量 repo inventory 的只讀 token 或管理匯出來源。 +3. AwoooP 只建立 mirror/read-only policy 入口,不新增 execution action。 +4. 任一方要把事件升級成實際執行,都必須先產出 `approval_required_event_v1`。 diff --git a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md new file mode 100644 index 00000000..e64b772b --- /dev/null +++ b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md @@ -0,0 +1,629 @@ +# AwoooP x Security Supply Chain 整合 Handoff + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-06 | +| 狀態 | 已同步給 AwoooP 主線,僅文件與契約整合,尚未做 runtime 變更;2026-05-12 開始第一波 read-only 工具化 | +| 目的 | 讓 AwoooP Session 與 Kali/GitHub/Codex/Gitea 資安供應鏈 Session 在同一條治理主線上推進 | +| 低摩擦 policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | +| Contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` | +| 原則 | 低摩擦分階段、雙 Session 推進、共享契約、AwoooP 做治理控制面,資安供應鏈做 evidence 與執行來源 | + +## 0. 給 AwoooP Session 的立即同步訊息 + +統帥已批准本支線開始推進,並要求以下工作納入同一個資安工作項目: + +1. Kali `192.168.0.112` 建置完整資訊安全網。 +2. 開發主機 `192.168.0.111`、`192.168.0.168` 納入 observe-only 資安資產。 +3. Code Review 後需要 coding 的工作串接 Codex。 +4. 長期改為 GitHub primary,Gitea 降級為本地 mirror / fallback。 +5. Gitea 目前所有專案版本都必須完整盤點並轉移/同步到 GitHub。 +6. 上述事項必須與 AwoooP 治理控制面整合,不可形成第二套資安流程。 + +本文件是給 AwoooP Session 的同步入口。請先把這份 handoff 視為共享契約,不要直接把 Kali 掃描、Codex patch 或 GitHub 遷移邏輯塞進 AwoooP runtime。 + +統帥補充原則:初期不要把資安等級一次拉太高。AwoooP 初期應先支援 mirror / observe / warn / approval candidate,而不是把所有 findings、repo 狀態或流程差異都變成阻擋條件。 + +## 1. Session 分工 + +### AwoooP 主線 Session + +負責: + +- Policy / EffectivePolicy。 +- Approval gate。 +- Channel Event。 +- Operator Console。 +- MCP Gateway。 +- Run State / Audit Sink。 +- Contract packages / validators。 +- Governance event 與 exception policy。 + +不得直接接手: + +- Kali scan 執行。 +- exploit verification。 +- production deploy。 +- secret rotation。 +- firewall / RBAC / NetworkPolicy 修改。 +- GitHub/Gitea 主控切換。 + +### Security Supply Chain Session + +負責: + +- Kali findings 與 `security_finding_v1`。 +- Code Review findings 與 `coding_task_v1`。 +- GitHub primary / Gitea mirror 遷移盤點。 +- Gitea 全量版本轉移 inventory。 +- Source control / CI/CD supply chain evidence。 +- Codex patch-only / suggest-only 接力設計。 +- 112/111/168 asset seed 與 observe-only scope。 + +不得直接接手: + +- AwoooP DB migration。 +- EffectivePolicy runtime enforcement。 +- MCP Gateway runtime gate。 +- Channel Hub runtime 實作。 +- Operator Console 主線 UI。 + +## 2. 共享目標架構 + +```text +Kali / Code Review / GitHub / Gitea / Codex + -> security_supply_chain_contract_manifest_v1 + -> security_finding_v1 / coding_task_v1 / source_control_migration_event_v1 / gitea_repo_inventory_v1 / local_git_remote_inventory_v1 / github_target_probe_v1 / github_target_decision_v1 / github_target_repo_approval_package_v1 / security_rollout_policy_v1 + -> AWOOOI ingestion / asset_inventory / AIOps KPI / AOL + -> mirror 到 AwoooP Runtime State / Channel Event / Audit + -> AwoooP Policy / Approval / Exception / Operator Console + -> 人工批准後才進本地 deployment plane +``` + +初期只允許 mirror / read-only / suggest-only,不允許 AwoooP 直接觸發高風險執行。 + +## 3. 必須共享的事件契約 + +### `security_finding_v1` + +用途:承接 Kali、Trivy、ZAP、Semgrep、detect-secrets、kube posture 等 findings。 + +Schema:`docs/schemas/security_finding_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "security_finding_v1", + "finding_id": "stable fingerprint", + "scan_run_id": "string", + "scanner": "kali|trivy|zap|semgrep|detect-secrets|kube-bench", + "asset_key": "string", + "target_type": "host|website|api_endpoint|container|package|repo|k8s_resource|tool", + "target": "redacted identifier", + "category": "exposure|cve|secret|misconfig|auth|tls|web|code|supply_chain|network", + "severity": "LOW|MEDIUM|HIGH|CRITICAL", + "confidence": "LOW|MEDIUM|HIGH", + "recommended_mode": "observe|warn|approve_required|block_candidate", + "evidence_ref": "redacted evidence pointer", + "summary": "繁體中文摘要", + "recommended_action": "繁體中文建議" +} +``` + +AwoooP 初期處理方式:mirror 成 Runtime State / Channel Event,不 enforcement。 + +### `security_rollout_policy_v1` + +用途:定義 Security Supply Chain 初期的低摩擦 rollout policy,避免把 observation 全部變成 blocking controls。 + +Schema:`docs/schemas/security_rollout_policy_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "security_rollout_policy_v1", + "status": "draft", + "default_mode": "observe", + "enforcement_level": "mirror_only" +} +``` + +AwoooP 初期處理方式:只作為 read-only policy 與 Operator Console 顯示,不做 runtime enforcement。 + +### `security_supply_chain_contract_manifest_v1` + +用途:集中列出 Security Supply Chain 初期可供 AwoooP 消費的 schema、snapshot、人讀文件、允許動作與禁止動作。 + +Schema:`docs/schemas/security_supply_chain_contract_manifest_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "security_supply_chain_contract_manifest_v1", + "status": "draft", + "default_enforcement_level": "mirror_only", + "contract_count": 12 +} +``` + +AwoooP 初期處理方式:作為 contract registry 與 Operator Console 入口,只路由 mirror/read-only/approval queue,不作 execution router。 + +### `coding_task_v1` + +用途:承接 Code Review 後需要 Codex coding 的項目。 + +Schema:`docs/schemas/coding_task_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "coding_task_v1", + "source": "github_code_review|gitea_code_review|codex_security|manual_review", + "repo": "string", + "branch": "string", + "base_sha": "string", + "head_sha": "string", + "risk": "LOW|MEDIUM|HIGH|CRITICAL", + "summary": "繁體中文摘要", + "allowed_actions": ["create_patch", "add_tests", "open_draft_pr"], + "blocked_actions": ["auto_merge", "production_deploy", "force_push", "secret_rotation", "network_policy_change"], + "required_reviewers": ["critic", "vuln-verifier"] +} +``` + +AwoooP 初期處理方式:作為 approval-ready work item,不自動執行。 + +### `source_control_migration_event_v1` + +用途:追蹤 Gitea 全量版本轉移到 GitHub 的供應鏈安全證據。 + +Schema:`docs/schemas/source_control_migration_event_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "source_control_migration_event_v1", + "gitea_repo": "string", + "github_repo": "string", + "branch_count_gitea": 0, + "branch_count_github": 0, + "tag_count_gitea": 0, + "tag_count_github": 0, + "latest_sha_gitea": "string", + "latest_sha_github": "string", + "workflows_mapped": true, + "webhooks_mapped": true, + "secrets_inventory_only": true, + "status": "inventory|mirrored|verified|blocked", + "blocking_reason": "繁體中文說明" +} +``` + +AwoooP 初期處理方式:作為 supply chain governance evidence,不觸發 deploy。 + +### `gitea_repo_inventory_v1` + +用途:追蹤 Gitea org/user repo list 的全量盤點,作為「所有 Gitea 專案版本轉移到 GitHub」的前置 evidence。 + +Schema:`docs/schemas/gitea_repo_inventory_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "gitea_repo_inventory_v1", + "base_url": "http://192.168.0.110:3001", + "org": "wooo", + "query_mode": "user", + "visibility_scope": "public_only", + "github_owner": "owenhytsai", + "token_present": false, + "http_status": 200, + "status": "partial", + "repo_count": 2, + "repos": [ + { + "gitea_repo": "wooo/awoooi", + "name": "awoooi", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/awoooi.git", + "github_repo_candidate": "owenhytsai/awoooi" + }, + { + "gitea_repo": "wooo/ewoooc", + "name": "ewoooc", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/ewoooc.git", + "github_repo_candidate": "owenhytsai/ewoooc" + } + ] +} +``` + +AwoooP 初期處理方式:作為 migration matrix 的 read-only evidence;`partial` 只代表 public-only 可見範圍,不得觸發 repo 建立、刪除、封存或 GitHub primary 切換。 + +### `local_git_remote_inventory_v1` + +用途:在 Gitea API 受阻時,盤點本機可見 Git working tree 的 remote URL,找出仍指向 Gitea、GitHub、110 內部 Git 或 GitLab 類 remote 的專案。 + +Schema:`docs/schemas/local_git_remote_inventory_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "local_git_remote_inventory_v1", + "status": "partial", + "repo_count": 13, + "gitea_linked_count": 6, + "github_linked_count": 6, + "internal_110_only_count": 3 +} +``` + +AwoooP 初期處理方式:作為 migration matrix 的輔助 evidence;不得把它視為 Gitea server 全量清單。 + +### `local_repo_canonical_probe_v1` + +用途:在多個本機 working tree 名稱相近但 remote/HEAD 不一致時,提供 read-only lineage evidence,避免自動把不同歷史誤合併。 + +Schema:`docs/schemas/local_repo_canonical_probe_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "local_repo_canonical_probe_v1", + "group_name": "ewoooc-momo-pro-system", + "status": "unrelated", + "repo_count": 3, + "comparison_count": 3 +} +``` + +AwoooP 初期處理方式:只作為 canonical decision evidence;不得觸發 merge、repo creation、repo deletion 或 mirror。 + +### `git_remote_refs_probe_v1` + +用途:對指定本機 repo 的 remote refs 做 read-only 探測,確認本機 HEAD 是否與 remote branch 對齊。 + +Schema:`docs/schemas/git_remote_refs_probe_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "git_remote_refs_probe_v1", + "group_name": "internal-110-bitan-tsenyang", + "status": "ok", + "repo_count": 2, + "aligned_current_branch_count": 2 +} +``` + +AwoooP 初期處理方式:只作為 source-control readiness evidence;不得觸發 fetch、push、mirror、repo creation 或 primary switch。 + +### `github_target_probe_v1` + +用途:對候選 GitHub repo 做 read-only 可見性與 refs probe,區分已存在、不可見、或外部 scope。 + +Schema:`docs/schemas/github_target_probe_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "github_target_probe_v1", + "status": "ok", + "candidate_count": 8, + "exists_count": 5, + "not_found_or_private_count": 3 +} +``` + +AwoooP 初期處理方式:只作為 migration target evidence;`not_found_or_private` 不等同確認不存在,不得自動建立 repo。 + +### `github_target_decision_v1` + +用途:追蹤候選 GitHub repo 的建立、可見性、封存或待判定決策,讓 target ownership 與 approval 狀態可被 AwoooP mirror。 + +Schema:`docs/schemas/github_target_decision_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "github_target_decision_v1", + "status": "draft", + "decision_count": 8, + "approval_required_count": 7 +} +``` + +AwoooP 初期處理方式:作為 approval candidate 與 migration target evidence;不得直接建立 GitHub repo、修改 visibility、同步 refs 或切 GitHub primary。 + +### `github_target_repo_approval_package_v1` + +用途:把 `github_target_decision_v1` 中需要人工批准的 target 拆成逐 repo approval package,降低一次性審批的複雜度。 + +Schema:`docs/schemas/github_target_repo_approval_package_v1.schema.json` + +最小欄位: + +```json +{ + "schema_version": "github_target_repo_approval_package_v1", + "status": "draft", + "package_count": 7, + "approval_items": [] +} +``` + +AwoooP 初期處理方式:mirror 成 approval queue draft。低摩擦原則下,read-only evidence 不被阻擋;只有 repo creation、visibility change、refs sync、primary switch 等高風險執行才需要 approval。 + +### `approval_required_event_v1` + +用途:把高風險資安修補、Codex patch、source control 主控切換送進 AwoooP approval。 + +Schema:`docs/schemas/approval_required_event_v1.schema.json` + +必須進 approval 的情況: + +- `severity=HIGH|CRITICAL`。 +- 會改 secrets、RBAC、NetworkPolicy、firewall、CORS。 +- 會改 production deploy controller。 +- 會切換 GitHub primary / Gitea mirror 主控面。 +- 會啟用 credentialed scan 或 active DAST。 +- 會使用 Gitea read-only token 或匯入管理匯出補 private/internal repo list。 +- 會讓 Codex patch 自動合併或自動部署。 + +## 4. AwoooP 整合順序 + +### Phase S0:文件與契約同步 + +狀態:本文件建立後即完成第一步同步。 + +交付: + +- 本 handoff。 +- LOGBOOK 條目。 +- `security_supply_chain_contract_manifest_v1`、`security_finding_v1`、`coding_task_v1`、`source_control_migration_event_v1`、`gitea_repo_inventory_v1`、`local_git_remote_inventory_v1`、`github_target_probe_v1`、`github_target_decision_v1`、`github_target_repo_approval_package_v1`、`security_rollout_policy_v1`、`local_repo_canonical_probe_v1`、`git_remote_refs_probe_v1`、`approval_required_event_v1` JSON Schema 草案。 + +### Phase S1:Mirror-only + +目標:讓 AwoooP 看見事件,但不改變行為。 + +允許: + +- 將 security findings mirror 成 Runtime State。 +- 將 coding tasks mirror 成 approval candidate。 +- 將 source control migration inventory mirror 成 supply chain evidence。 +- 將 Gitea repo inventory mirror 成 migration matrix evidence。 +- 將本機 Git remote inventory mirror 成 source-control coverage evidence。 +- 將 GitHub target 決策 mirror 成 approval candidate。 +- 將 GitHub target repo-by-repo package mirror 成 approval queue draft。 +- 將低摩擦 rollout policy mirror 成 read-only policy。 +- 將 contract manifest mirror 成 contract registry。 + +禁止: + +- 直接 enforcement。 +- 直接啟動 scan。 +- 直接呼叫 Codex patch runner。 +- 直接切 GitHub/Gitea 主控。 +- 直接刪除、封存或建立 repo。 +- 直接修改 GitHub repo visibility。 + +### Phase S2:Read-only Policy + +目標:AwoooP 只計算 policy,不執行。 + +範例: + +- `security_finding_v1` 進來後,AwoooP 回傳 `observe|warn|approve_required` 建議。 +- `coding_task_v1` 進來後,AwoooP 回傳是否需要 `critic` / `vuln-verifier`。 +- `source_control_migration_event_v1` 進來後,AwoooP 回傳是否缺 branch/tag/workflow/webhook/permission evidence。 +- `local_git_remote_inventory_v1` 進來後,AwoooP 回傳是否仍有 local-only / internal-110-only source-control risk。 +- `github_target_decision_v1` 進來後,AwoooP 回傳是否需要 owner / visibility / canonical approval。 +- `github_target_repo_approval_package_v1` 進來後,AwoooP 回傳逐 repo approval queue draft,不阻擋 read-only evidence。 +- `security_rollout_policy_v1` 進來後,AwoooP 回傳 observe / warn / approve_required 建議,不做 enforcement。 +- `security_supply_chain_contract_manifest_v1` 進來後,AwoooP 回傳可消費 contract 清單,不新增 execution router。 + +### Phase S3:Approval Gate + +目標:高風險行為必須經 AwoooP approval。 + +先納入: + +- Codex patch 合併前。 +- HIGH/CRITICAL findings 修補前。 +- GitHub primary 切換前。 +- credentialed scan 前。 + +### Phase S4:Operator Console + +目標:讓操作者在 AwoooP Console 看見: + +- 資安 posture。 +- open findings。 +- coding task 狀態。 +- GitHub/Gitea migration matrix。 +- approval queue。 +- accepted risk / false positive / exception。 + +Console 初期不提供高風險 action button。 + +## 5. 兩邊 Session 的衝突邊界 + +| 區域 | AwoooP Session | Security Supply Chain Session | 衝突避免 | +|------|----------------|-------------------------------|----------| +| Contract schema | 主責 | 提供草案與需求 | AwoooP 決定 canonical package | +| DB migration | 主責 | 不碰 | findings 先文件化,不建表 | +| Runtime enforcement | 主責 | 不碰 | 先 mirror/read-only | +| Kali scan | 不直接執行 | 主責規劃 | AwoooP 只管 approval/policy | +| Codex patch | 不直接執行 | 主責規劃 | AwoooP 只管 approval/audit | +| GitHub/Gitea 遷移 | 治理 gate | inventory 與遷移設計 | 主控切換需雙方 handoff | +| MCP Gateway | 主責 | 只提出 access needs | 不新增 direct tool bypass | +| Channel Event | 主責 | 提供事件需求 | Channel adapter 不做 policy | + +## 6. AwoooP Session 需要消費的工作項 + +請 AwoooP 主線在後續規劃時預留: + +1. Runtime State 可容納 `security_finding_v1` mirror。 +2. Approval model 可容納 `coding_task_v1` 與 `source_control_migration_event_v1`。 +3. EffectivePolicy 可判斷 `observe|warn|approve_required|block_candidate`。 +4. Channel Event 可承載資安 findings 與供應鏈遷移事件。 +5. Audit Sink 必須脫敏,不可儲存 raw secret、token、cookie、exploit payload。 +6. Operator Console 需要 supply chain / security posture 視圖,但初期不要高風險 action。 +7. MCP Gateway 後續要能限制 Codex/Kali/GitHub 工具權限。 +8. Migration matrix 可容納 `gitea_repo_inventory_v1`,但初期只讀。 +9. Migration matrix 可容納 `local_git_remote_inventory_v1`,但不得視為 server 全量。 +10. Approval queue 可容納 `github_target_decision_v1` 與 `github_target_repo_approval_package_v1`,但不得直接建立 repo 或改 visibility。 +11. Read-only policy 可容納 `security_rollout_policy_v1`,但初期不得把它變成 runtime blocking rule。 +12. Contract registry 可容納 `security_supply_chain_contract_manifest_v1`,但初期不得把它變成 direct tool router。 + +## 7. Security Supply Chain Session 下一步 + +已批准開始推進後,本支線第一波只做: + +1. Gitea/GitHub 全量版本盤點設計。 +2. `coding_task_v1` schema 文件化。 +3. `security_finding_v1` schema 文件化。 +4. `source_control_migration_event_v1` schema 文件化。 +5. `gitea_repo_inventory_v1` schema 文件化。 +6. `local_git_remote_inventory_v1` schema 文件化。 +7. `github_target_probe_v1` schema 文件化。 +8. `local_repo_canonical_probe_v1` schema 文件化。 +9. `git_remote_refs_probe_v1` schema 文件化。 +10. `github_target_decision_v1` schema 文件化。 +11. `approval_required_event_v1` schema 文件化。 +12. 112/111/168 observe-only asset mapping。 +13. Codex patch-only handoff prompt。 +14. AwoooP mirror-only integration notes。 + +第一版 inventory 已建立於 `docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md`。目前只完成 `awoooi` repo read-only 初步盤點,已確認 GitHub / Gitea 存在 branches、tags 與 `main` SHA 差異,因此主控切換必須保持 blocked,直到全量同步驗證完成。 + +第一版 observe-only host mapping 已建立於 `docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md`。第一版 Codex patch-only handoff prompt 已建立於 `docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md`。 + +2026-05-12 更新:已新增 `scripts/security/source-control-migration-inventory.py`,並產出 `docs/security/gitea-github-awoooi-inventory.snapshot.json`。目前 `source_control_migration_event_v1.status=blocked`,因 Gitea heads 115、GitHub heads 2、Gitea tags 2、GitHub tags 0,且 `main` SHA 不一致。AwoooP Session 應將此視為 supply-chain evidence,不得視為可切 GitHub primary。 + +2026-05-12 追加:已新增 `scripts/security/gitea-repo-inventory.py`、`docs/schemas/gitea_repo_inventory_v1.schema.json`,並產出 `docs/security/gitea-repo-inventory.snapshot.json`。目前 `gitea_repo_inventory_v1.status=partial`,因未提供 token 時只能取得 public-only `wooo/awoooi` 與 `wooo/ewoooc`;完整全量仍需只讀 token 或管理匯出。 + +2026-05-12 Gitea endpoint 判定追加:已新增 `docs/security/gitea-org-repo-inventory-blocked.snapshot.json` 與 `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md`,保留 `orgs/wooo/repos` 未認證 404 evidence;後續 server-side inventory 以 `users/wooo/repos`、只讀 token 或管理匯出為主。 + +2026-05-12 server-side inventory runbook 追加:已新增 `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md`,定義 public-only、只讀 token、管理匯出 JSON 三種路徑。AwoooP 可 mirror runbook 狀態,但不得要求 Security Supply Chain Session 提供 token value。 + +2026-05-12 Gitea approval package 追加:已新增 `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` 與 `docs/security/gitea-readonly-inventory-approval.snapshot.json`,用 `approval_required_event_v1` 描述 `run_gitea_readonly_inventory` 的人工 gate。AwoooP 可建立 approval candidate,但不得保存 token value 或觸發任何 repo 建立、visibility 修改、refs sync。 + +2026-05-12 再追加:已新增 `scripts/security/local-git-remote-inventory.py`、`docs/schemas/local_git_remote_inventory_v1.schema.json`,並產出 `docs/security/local-git-remote-inventory.snapshot.json`。目前 `local_git_remote_inventory_v1.status=partial`,找到本機可見 working trees 13 個,其中 Gitea linked 6、GitHub linked 6、110 internal-only 3;此結果只作為 Gitea API 受阻時的輔助 evidence。 + +2026-05-12 矩陣追加:已新增 `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md`,將本機可見 source-control targets 拆成 P0/P1/P2。AwoooP 可 mirror 此矩陣作為 migration planning evidence,但不得依此自動建立、刪除、同步 repo 或切換 primary。 + +2026-05-12 refs diff 追加:已新增 `docs/security/source-control-clawbot-v5.snapshot.json`、`docs/security/source-control-wooo-aiops.snapshot.json`。`wooo/clawbot-v5` 與 `wooo/wooo-aiops` 目前都為 `source_control_migration_event_v1.status=blocked`,不得視為 GitHub primary ready。 + +2026-05-12 public search / canonical 追加:Gitea public search 在未提供 token 時可見 `wooo/awoooi`、`wooo/ewoooc`。已新增 `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md`,其中 `wooo/ewoooc`、`root/momo-pro-system`、`momo-pro-system`、`momo_pro_system` 仍需人工判定 canonical 關係,不得自動合併。 + +2026-05-12 GitHub target probe 追加:已新增 `scripts/security/github-target-probe.py`、`docs/schemas/github_target_probe_v1.schema.json` 與 `docs/security/github-target-probe.snapshot.json`。8 個候選中 5 個可讀,`owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website` 為 `not_found_or_private`。 + +2026-05-12 canonical lineage 追加:已新增 `scripts/security/local-repo-canonical-probe.py`、`docs/schemas/local_repo_canonical_probe_v1.schema.json` 與 `docs/security/local-repo-canonical-ewoooc-momo.snapshot.json`。`ewoooc-momo-pro-system` 群組目前為 `unrelated`,三個本機 working tree 在 sample 內沒有共同 commit,不得自動合併。 + +2026-05-12 internal refs 追加:已新增 `scripts/security/git-remote-refs-probe.py`、`docs/schemas/git_remote_refs_probe_v1.schema.json` 與 `docs/security/git-remote-refs-bitan-tsenyang.snapshot.json`。`bitan-pharmacy`、`tsenyang-website` 的本機 `main` 與 110 remote `main` 對齊,但 GitHub target 仍未確認。另新增 `docs/security/git-remote-refs-wooo-infra-config.snapshot.json`;`wooo-infra-config` 的 GitHub remote 與本機 `main` 對齊,110 internal remote 目前不可讀。 + +2026-05-12 GitHub target 決策追加:已新增 `docs/schemas/github_target_decision_v1.schema.json`、`docs/security/github-target-decision.snapshot.json` 與 `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md`。8 個 target 候選中 7 個需人工批准;AwoooP 只能 mirror 為 approval candidate,不得自動建立 repo、修改 visibility、同步 refs 或切 GitHub primary。 + +2026-05-12 GitHub target repo-by-repo approval package 追加:已新增 `docs/schemas/github_target_repo_approval_package_v1.schema.json`、`docs/security/github-target-repo-approval-package.snapshot.json` 與 `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md`。7 個 approval-required targets 已拆成逐 repo pending package;依統帥提醒採低摩擦分階段,不把 read-only evidence 變成阻擋條件。 + +2026-05-12 低摩擦 rollout policy 追加:已新增 `docs/schemas/security_rollout_policy_v1.schema.json`、`docs/security/security-rollout-policy.snapshot.json` 與 `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md`。AwoooP 初期應採 observe-first / mirror-only,不把 LOW / MEDIUM observation 變成 blocking controls。 + +2026-05-12 contract manifest 追加:已新增 `docs/schemas/security_supply_chain_contract_manifest_v1.schema.json`、`docs/security/security-supply-chain-contract-manifest.snapshot.json` 與 `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md`。AwoooP 應先讀 manifest 作為 mirror-only contract registry,不把 manifest 當 execution router。 + +本波仍不做: + +- runtime DB migration。 +- API endpoint。 +- K8s / NetworkPolicy / RBAC / firewall。 +- production deploy。 +- GitHub 主控切換。 +- Gitea 刪除或停用。 +- Codex API 自動長任務。 + +## 8. 參考文件 + +- [Kali 資訊安全網藍圖](/Users/ogt/awoooi/docs/security/KALI-SECURITY-MESH-BLUEPRINT.md) +- [Kali 資訊安全網開工準備](/Users/ogt/awoooi/docs/security/KALI-SECURITY-MESH-EXECUTION-READINESS.md) +- [Code Review 接 Codex 與 Gitea 推版優化藍圖](/Users/ogt/awoooi/docs/operations/CODE-REVIEW-CODEX-GITEA-OPTIMIZATION.md) +- [Gitea 到 GitHub 全量版本轉移 Inventory](/Users/ogt/awoooi/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md) +- [Gitea / GitHub migration snapshot](/Users/ogt/awoooi/docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md) +- [source_control_migration_event_v1 snapshot](/Users/ogt/awoooi/docs/security/gitea-github-awoooi-inventory.snapshot.json) +- [clawbot-v5 source_control_migration_event_v1 snapshot](/Users/ogt/awoooi/docs/security/source-control-clawbot-v5.snapshot.json) +- [wooo-aiops source_control_migration_event_v1 snapshot](/Users/ogt/awoooi/docs/security/source-control-wooo-aiops.snapshot.json) +- [source-control inventory script](/Users/ogt/awoooi/scripts/security/source-control-migration-inventory.py) +- [Gitea repo inventory snapshot](/Users/ogt/awoooi/docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md) +- [gitea_repo_inventory_v1 snapshot](/Users/ogt/awoooi/docs/security/gitea-repo-inventory.snapshot.json) +- [Gitea org endpoint blocked snapshot](/Users/ogt/awoooi/docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md) +- [Gitea org endpoint blocked JSON](/Users/ogt/awoooi/docs/security/gitea-org-repo-inventory-blocked.snapshot.json) +- [Gitea server-side inventory runbook](/Users/ogt/awoooi/docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md) +- [Gitea read-only inventory approval package](/Users/ogt/awoooi/docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md) +- [Gitea read-only inventory approval snapshot](/Users/ogt/awoooi/docs/security/gitea-readonly-inventory-approval.snapshot.json) +- [Gitea admin export redaction checklist](/Users/ogt/awoooi/docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md) +- [Gitea public repo search snapshot](/Users/ogt/awoooi/docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md) +- [gitea public repo search JSON](/Users/ogt/awoooi/docs/security/gitea-public-repo-search.snapshot.json) +- [gitea repo inventory script](/Users/ogt/awoooi/scripts/security/gitea-repo-inventory.py) +- [本機 Git remote inventory snapshot](/Users/ogt/awoooi/docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md) +- [local_git_remote_inventory_v1 snapshot](/Users/ogt/awoooi/docs/security/local-git-remote-inventory.snapshot.json) +- [本機 Git remote inventory script](/Users/ogt/awoooi/scripts/security/local-git-remote-inventory.py) +- [GitHub target probe snapshot](/Users/ogt/awoooi/docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md) +- [github_target_probe_v1 snapshot](/Users/ogt/awoooi/docs/security/github-target-probe.snapshot.json) +- [GitHub target probe script](/Users/ogt/awoooi/scripts/security/github-target-probe.py) +- [GitHub target 決策表](/Users/ogt/awoooi/docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md) +- [github_target_decision_v1 snapshot](/Users/ogt/awoooi/docs/security/github-target-decision.snapshot.json) +- [GitHub target repo-by-repo approval package](/Users/ogt/awoooi/docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md) +- [github_target_repo_approval_package_v1 snapshot](/Users/ogt/awoooi/docs/security/github-target-repo-approval-package.snapshot.json) +- [低摩擦資安 rollout policy](/Users/ogt/awoooi/docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md) +- [security_rollout_policy_v1 snapshot](/Users/ogt/awoooi/docs/security/security-rollout-policy.snapshot.json) +- [Security Supply Chain contract manifest](/Users/ogt/awoooi/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md) +- [security_supply_chain_contract_manifest_v1 snapshot](/Users/ogt/awoooi/docs/security/security-supply-chain-contract-manifest.snapshot.json) +- [本機 repo canonical lineage snapshot](/Users/ogt/awoooi/docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md) +- [local_repo_canonical_probe_v1 snapshot](/Users/ogt/awoooi/docs/security/local-repo-canonical-ewoooc-momo.snapshot.json) +- [本機 repo canonical lineage script](/Users/ogt/awoooi/scripts/security/local-repo-canonical-probe.py) +- [Internal 110 refs snapshot](/Users/ogt/awoooi/docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md) +- [git_remote_refs_probe_v1 snapshot](/Users/ogt/awoooi/docs/security/git-remote-refs-bitan-tsenyang.snapshot.json) +- [wooo-infra-config refs snapshot](/Users/ogt/awoooi/docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md) +- [wooo-infra-config git_remote_refs_probe_v1 snapshot](/Users/ogt/awoooi/docs/security/git-remote-refs-wooo-infra-config.snapshot.json) +- [Git remote refs probe script](/Users/ogt/awoooi/scripts/security/git-remote-refs-probe.py) +- [Source Control 遷移矩陣](/Users/ogt/awoooi/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md) +- [Source Control Canonical Repo 判定表](/Users/ogt/awoooi/docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md) +- [AwoooP Mirror-only 消費清單](/Users/ogt/awoooi/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md) +- [112 / 111 / 168 Observe-only 資產 Mapping](/Users/ogt/awoooi/docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md) +- [Codex Patch-only Handoff Prompt](/Users/ogt/awoooi/docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md) +- [AwoooP x Monitoring / Alerting Convergence Map](/Users/ogt/awoooi/docs/awooop/AWOOOP-MONITORING-ALERTING-CONVERGENCE.md) +- [AwoooP Master Workplan](/Users/ogt/awoooi/docs/awooop/MASTER-WORKPLAN.md) +- [security_finding_v1 schema](/Users/ogt/awoooi/docs/schemas/security_finding_v1.schema.json) +- [coding_task_v1 schema](/Users/ogt/awoooi/docs/schemas/coding_task_v1.schema.json) +- [source_control_migration_event_v1 schema](/Users/ogt/awoooi/docs/schemas/source_control_migration_event_v1.schema.json) +- [gitea_repo_inventory_v1 schema](/Users/ogt/awoooi/docs/schemas/gitea_repo_inventory_v1.schema.json) +- [local_git_remote_inventory_v1 schema](/Users/ogt/awoooi/docs/schemas/local_git_remote_inventory_v1.schema.json) +- [github_target_probe_v1 schema](/Users/ogt/awoooi/docs/schemas/github_target_probe_v1.schema.json) +- [github_target_decision_v1 schema](/Users/ogt/awoooi/docs/schemas/github_target_decision_v1.schema.json) +- [github_target_repo_approval_package_v1 schema](/Users/ogt/awoooi/docs/schemas/github_target_repo_approval_package_v1.schema.json) +- [security_rollout_policy_v1 schema](/Users/ogt/awoooi/docs/schemas/security_rollout_policy_v1.schema.json) +- [security_supply_chain_contract_manifest_v1 schema](/Users/ogt/awoooi/docs/schemas/security_supply_chain_contract_manifest_v1.schema.json) +- [local_repo_canonical_probe_v1 schema](/Users/ogt/awoooi/docs/schemas/local_repo_canonical_probe_v1.schema.json) +- [git_remote_refs_probe_v1 schema](/Users/ogt/awoooi/docs/schemas/git_remote_refs_probe_v1.schema.json) +- [approval_required_event_v1 schema](/Users/ogt/awoooi/docs/schemas/approval_required_event_v1.schema.json) diff --git a/docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md b/docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md new file mode 100644 index 00000000..da111c98 --- /dev/null +++ b/docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md @@ -0,0 +1,83 @@ +# Codex Patch-only Handoff Prompt + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-06 | +| 狀態 | 第一版 prompt,供 Code Review → Codex 半自動接力使用 | +| 上游契約 | `docs/schemas/coding_task_v1.schema.json` | +| 原則 | 只產生 patch,不自動 merge、不自動 deploy | + +## 0. 使用時機 + +本 prompt 用於 Code Review 產生 `coding_task_v1` 後,交給 Codex 或 coding worker 產生修補分支 / patch。 + +允許: + +- 修 bug。 +- 補測試。 +- 修格式。 +- 補型別。 +- 補防禦性檢查。 +- 產生 draft PR 或 patch summary。 + +不允許: + +- 自動 merge。 +- production deploy。 +- force push。 +- secret rotation。 +- RBAC / NetworkPolicy / firewall 修改。 +- credentialed scan。 +- destructive migration。 + +## 1. Prompt Template + +```text +你是 AWOOOI 的 Codex coding worker。請依照下列 coding_task_v1 處理工作。 + +重要規則: +1. 全部回覆、註解、文件與摘要都使用繁體中文。 +2. 僅允許產生 patch 或 draft branch 建議,不得自動 merge。 +3. 不得執行 production deploy。 +4. 不得修改 secrets、RBAC、NetworkPolicy、firewall、CORS 或 credentialed scan 設定。 +5. 如需高風險行為,停止並產生 approval_required_event_v1。 +6. 必須保留測試證據:執行過的命令、結果、未執行原因。 +7. 不得輸出 raw secret、token、cookie 或 exploit payload。 + +輸入 coding_task_v1: +<貼上 JSON> + +請輸出: +1. 修補摘要。 +2. 修改檔案清單。 +3. 測試命令與結果。 +4. 風險與 rollback note。 +5. 是否需要 critic / vuln-verifier / human-owner review。 +6. 若 blocked,請輸出 approval_required_event_v1 草案。 +``` + +## 2. Reviewer Gate + +| 條件 | 必要 reviewer | +|------|---------------| +| `risk=LOW` | `critic` 可選 | +| `risk=MEDIUM` | `critic` | +| `risk=HIGH` | `critic`、`vuln-verifier`、`human-owner` | +| `risk=CRITICAL` | `critic`、`vuln-verifier`、`human-owner`、`security-commander` | +| 涉及 CI/CD / GitHub / Gitea / deploy | `migration-engineer` | +| 涉及 secrets / RBAC / NetworkPolicy / firewall | `vuln-verifier` | + +## 3. 完成條件 + +Codex patch-only 任務完成時必須提供: + +- patch summary。 +- changed files。 +- tests run。 +- tests not run reason。 +- risk notes。 +- rollback note。 +- required reviewers。 +- blocked action list。 + +若任一 blocked action 被需要,任務不得繼續實作,必須改產 `approval_required_event_v1`。 diff --git a/docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md b/docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md new file mode 100644 index 00000000..7225a49e --- /dev/null +++ b/docs/security/DEV-HOSTS-112-111-168-OBSERVE-ONLY-MAPPING.md @@ -0,0 +1,83 @@ +# 112 / 111 / 168 Observe-only 資產 Mapping + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-06 | +| 狀態 | 第一版 mapping,尚未寫入 DB / inventory seed | +| 範圍 | Kali 資安主機與兩台開發主機 | +| 上游 | `docs/security/KALI-SECURITY-MESH-BLUEPRINT.md` | + +## 0. 原則 + +112 / 111 / 168 初期全部採 `observe_only=true`。 + +這代表: + +- 可以盤點。 +- 可以建立資產 mapping。 +- 可以產生 warning-only findings。 +- 可以 mirror evidence 到 AwoooP。 +- 不可以 blocking release。 +- 不可以自動封鎖。 +- 不可以自動修復。 +- 不可以執行 credentialed scan,除非另行批准。 + +## 1. Host Mapping + +| Host | 暫定 asset_key | 角色 | 初期模式 | Owner | +|------|----------------|------|----------|-------| +| `192.168.0.112` | `host:kali-112` | Kali 資安感測主機 | `observe_only` | Security Supply Chain | +| `192.168.0.111` | `host:dev-ai-111` | Local AI / Ollama fallback / 開發輔助主機 | `observe_only` | Dev Host Steward | +| `192.168.0.168` | `host:dev-workstation-168` | 開發工作站 / local development origin | `observe_only` | Dev Host Steward | + +## 2. 初期允許盤點項目 + +| Host | 允許項目 | 不允許項目 | +|------|----------|------------| +| `112` | scanner health、工具版本、scan job metadata、結果匯出格式 | 未批准的 exploit scan、credentialed host scan、production active DAST | +| `111` | Ollama API exposure、模型 inventory、host reachability、SSH policy posture | 自動停止模型、自動改 firewall、自動調整 fallback route | +| `168` | local service exposure、repo secrets hygiene、dev-only CORS 來源確認 | 掃描個人資料、讀取未授權目錄、credentialed scan | + +## 3. Finding 對應 + +| 類型 | 對應 category | recommended_mode | +|------|---------------|------------------| +| 112 scanner down | `tool` / `exposure` | `warn` | +| 112 scanner result schema drift | `supply_chain` | `observe` | +| 111 Ollama API 暴露過寬 | `exposure` | `warn` | +| 111 模型 fallback 不可用 | `misconfig` | `observe` | +| 168 dev CORS 來源漂移 | `misconfig` | `warn` | +| 168 repo 疑似 secret | `secret` | `approve_required` | + +## 4. AwoooP Mirror + +初期 mirror 成: + +- `security_finding_v1` +- `approval_required_event_v1`,僅在 `secret`、`HIGH`、`CRITICAL` 或任何需要權限變更時使用 + +AwoooP 初期只做: + +- policy 建議。 +- approval queue。 +- audit / evidence 顯示。 +- exception / accepted risk 記錄。 + +AwoooP 初期不做: + +- 直接執行 scan。 +- 直接執行 host command。 +- 直接改 firewall / RBAC / NetworkPolicy。 +- 直接 rotate secrets。 + +## 5. 未來 DB Seed 前置條件 + +要寫入 `asset_inventory` 前,必須先確認: + +1. `asset_key` 命名被接受。 +2. owner mapping 被接受。 +3. observe-only scope 被接受。 +4. redaction policy 被接受。 +5. rollback / disable flag 被命名。 + +目前尚未開始 DB 實作。 diff --git a/docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md b/docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md new file mode 100644 index 00000000..6945b3fd --- /dev/null +++ b/docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md @@ -0,0 +1,18 @@ +# Git Remote Refs Probe 快照 + +| 項目 | 值 | +|------|----| +| 群組 | `internal-110-bitan-tsenyang` | +| 狀態 | `ok` | +| repo 數 | `2` | +| aligned current branch | `2` | +| unreachable | `0` | + +## Repo refs + +| Label | Remote URL | Status | Local branch | Local HEAD | Remote branch SHA | Heads | Tags | Error | +|-------|------------|--------|--------------|------------|-------------------|-------|------|-------| +| `bitan-pharmacy` | `192.168.0.110:bitan-pharmacy.git` | `aligned_current_branch` | `main` | `7423a08` | `7423a08` | `1` | `0` | 無 | +| `tsenyang-website` | `192.168.0.110:tsenyang-website.git` | `aligned_current_branch` | `main` | `b103112` | `b103112` | `1` | `0` | 無 | + +> 注意:本檔只使用 `git ls-remote` 做 read-only refs 探測;未 fetch、未 clone、未 push。 diff --git a/docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md b/docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md new file mode 100644 index 00000000..6dc1e3f6 --- /dev/null +++ b/docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md @@ -0,0 +1,18 @@ +# Git Remote Refs Probe 快照 + +| 項目 | 值 | +|------|----| +| 群組 | `wooo-infra-config-remotes` | +| 狀態 | `partial` | +| repo 數 | `2` | +| aligned current branch | `1` | +| unreachable | `1` | + +## Repo refs + +| Label | Remote URL | Status | Local branch | Local HEAD | Remote branch SHA | Heads | Tags | Error | +|-------|------------|--------|--------------|------------|-------------------|-------|------|-------| +| `wooo-infra-config-gitea` | `192.168.0.110:wooo/wooo-infra-config.git` | `unreachable` | `main` | `866b5aa` | `` | `0` | `0` | SSH 權限不足或 remote 不可讀 | +| `wooo-infra-config-github` | `github.com:owenhytsai/wooo-infra-config.git` | `aligned_current_branch` | `main` | `866b5aa` | `866b5aa` | `1` | `0` | 無 | + +> 注意:本檔只使用 `git ls-remote` 做 read-only refs 探測;未 fetch、未 clone、未 push。 diff --git a/docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md b/docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md new file mode 100644 index 00000000..3623ea85 --- /dev/null +++ b/docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md @@ -0,0 +1,84 @@ +# Gitea 管理匯出 Redaction Checklist + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 第一版,給 `gitea_repo_inventory_v1` 管理匯入使用 | +| 搭配文件 | `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md` | +| Approval | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` | + +## 0. 允許保留的欄位 + +| 欄位 | 用途 | +|------|------| +| `full_name` | Gitea repo identity,例如 `wooo/awoooi` | +| `name` | repo short name | +| `owner.login` | owner / org / user 名稱 | +| `private` | 判斷 visibility migration | +| `archived` | 判斷封存策略 | +| `empty` | 判斷是否需要搬遷 refs | +| `default_branch` | 後續 branch/tag diff 起點 | +| `clone_url` | 只允許無帳密 URL,工具會再次 redacted | +| `ssh_url` | 只允許 public repo path,不含 private key | + +## 1. 絕對不能出現在匯出 JSON + +1. API token value。 +2. Personal access token。 +3. Webhook secret value。 +4. Repository secret value。 +5. Deploy key private key。 +6. SSH private key。 +7. Cookie、session、CSRF token。 +8. 帶 username/password/token 的 remote URL。 +9. CI/CD runner registration token。 + +## 2. 最小 JSON 模板 + +```json +[ + { + "full_name": "wooo/example", + "name": "example", + "owner": { + "login": "wooo" + }, + "private": true, + "archived": false, + "empty": false, + "default_branch": "main", + "clone_url": "http://192.168.0.110:3001/wooo/example.git", + "ssh_url": "git@192.168.0.110:wooo/example.git" + } +] +``` + +## 3. 匯入前人工檢查 + +| Gate | 檢查方式 | 必須結果 | +|------|----------|----------| +| 無 token | 搜尋 token / authorization / provider token prefix 等字樣 | 不得出現 value | +| 無帳密 URL | 搜尋 URL 中是否含帳號、密碼、token 與 at-sign 組合 | 不得出現 | +| 無 private key | 搜尋 `BEGIN .* PRIVATE KEY` | 不得出現 | +| repo 欄位完整 | 檢查 `full_name` 或 `owner.login + name` | 每個 repo 可識別 | +| visibility 可判斷 | 檢查 `private` | 每個 repo 有布林值 | + +## 4. 匯入指令 + +```bash +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --github-owner owenhytsai \ + --input-json /path/to/redacted-gitea-repos.json \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +## 5. 匯入後驗收 + +1. `gitea_repo_inventory_v1.status=ok`。 +2. `visibility_scope=admin_export`。 +3. `repo_count` 大於或等於 public-only repo count。 +4. `repos[].clone_url_redacted` 與 `repos[].ssh_url_redacted` 不含帳密。 +5. 下一步仍只更新 migration matrix,不同步 refs、不建 repo、不切 primary。 diff --git a/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md new file mode 100644 index 00000000..f736ffd1 --- /dev/null +++ b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md @@ -0,0 +1,219 @@ +# Gitea 到 GitHub 全量版本轉移 Inventory + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 第二版 read-only inventory,尚未開始同步或主控切換 | +| 範圍 | Source control / CI/CD supply chain security | +| 上游 handoff | `docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md` | +| branch/tag/SHA 盤點工具 | `scripts/security/source-control-migration-inventory.py` | +| repo list 盤點工具 | `scripts/security/gitea-repo-inventory.py` | +| 本機 remote 盤點工具 | `scripts/security/local-git-remote-inventory.py` | +| 最新 branch/tag/SHA snapshot | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` / `docs/security/gitea-github-awoooi-inventory.snapshot.json` | +| 最新 repo list snapshot | `docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` / `docs/security/gitea-repo-inventory.snapshot.json` | +| Gitea org endpoint blocked snapshot | `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md` / `docs/security/gitea-org-repo-inventory-blocked.snapshot.json` | +| Gitea public search snapshot | `docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` / `docs/security/gitea-public-repo-search.snapshot.json` | +| Gitea server-side inventory runbook | `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md` | +| Gitea read-only inventory approval package | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` / `docs/security/gitea-readonly-inventory-approval.snapshot.json` | +| Gitea admin export redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` | +| 最新本機 remote snapshot | `docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md` / `docs/security/local-git-remote-inventory.snapshot.json` | +| GitHub target probe | `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` / `docs/security/github-target-probe.snapshot.json` | +| 本機 canonical lineage probe | `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` / `docs/security/local-repo-canonical-ewoooc-momo.snapshot.json` | +| Internal 110 refs probe | `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` / `docs/security/git-remote-refs-bitan-tsenyang.snapshot.json` | +| wooo-infra-config refs probe | `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` / `docs/security/git-remote-refs-wooo-infra-config.snapshot.json` | +| GitHub target 決策表 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` / `docs/security/github-target-decision.snapshot.json` | +| GitHub target repo-by-repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` / `docs/security/github-target-repo-approval-package.snapshot.json` | +| Source Control 遷移矩陣 | `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | +| Canonical repo 判定表 | `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md` | + +## 0. 重要結論 + +目前不能直接把 GitHub 切成 primary。 + +第二輪只讀盤點顯示,至少目前工作中的 `awoooi` repo 存在以下差異: + +- GitHub `origin` 與 Gitea `gitea` 的 `main` SHA 不一致。 +- Gitea 有大量 `drift/adopt-*` 分支,GitHub 沒有;截至 2026-05-12,Gitea heads 為 115 條,GitHub heads 為 2 條。 +- Gitea 有 release tags,GitHub 目前查不到 tags。 +- 本機 `gitea` remote URL 內嵌憑證,這是 credential hygiene 風險;不得寫入文件、不得複製到 GitHub,後續需移除並輪替。 +- Gitea `wooo` user endpoint 在未提供 token 時可見 `wooo/awoooi` 與 `wooo/ewoooc`,目前 `gitea_repo_inventory_v1.status=partial`。 +- Gitea org endpoint 未認證查詢仍回 404;未提供 token 的結果只代表 public-only 可見範圍,不代表 private/internal repos 已完整盤到。 +- Gitea read-only inventory approval package 已建立;取得只讀 token 或管理匯出前,必須先經人工批准,且不得保存 token value。 +- GitHub target probe 顯示 8 個候選中 5 個可讀、3 個為 `not_found_or_private`:`owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website`。 +- `ewoooc-momo-pro-system` 本機 lineage probe 顯示三個 working tree 近期 sample 內無共同 commit,因此不得自動視為複本或同一 repo 分支。 +- `bitan-pharmacy` 與 `tsenyang-website` 的 110 remote refs probe 顯示本機 `main` 與 remote `main` 對齊,各 1 head / 0 tags;但 GitHub target 仍未確認。 +- `wooo-infra-config` 的 GitHub remote 與本機 `main` 對齊;110 internal remote 目前 read-only probe 不可讀,需判斷是否為舊 remote、mirror 或權限問題。 +- GitHub target 決策表已建立,8 個候選中 7 個需人工批准;其中 `ewoooc`、`bitan-pharmacy`、`tsenyang-website` 在 target visibility / owner 決策前不得自動建立或同步。 +- GitHub target repo-by-repo approval package 已建立,7 個 approval-required targets 拆成 refs reconcile、target 建立 / 授權、internal remote 用途確認三條路徑;此 package 採低摩擦原則,只 gate 高風險執行,不阻擋 read-only evidence。 +- 本機可見 Git working tree 輔助盤點已找到 13 個 repo,其中去重後 Gitea repo 4 個、GitHub repo 5 個、110 內部 repo 4 個;此結果可用來補遷移矩陣,但不能取代 Gitea server 全量清單。 + +因此後續必須先完成「repo/branch/tag/workflow/webhook/permission/secrets 名稱」全量 inventory,再逐步 mirror 與驗證。 + +## 1. 本輪 read-only 探測 + +| 檢查 | 結果 | +|------|------| +| `git remote -v` | 已確認 `origin` 指向 GitHub,`gitea` 指向本地 Gitea;未在文件中保存憑證 | +| GitHub heads | 2 條 | +| Gitea heads | 115 條 | +| GitHub tags | 0 條 | +| Gitea tag refs | 4 條 raw refs,實際 tag 為 `v7.2.0`、`v7.3.0` | +| Gitea org API | 未認證查詢 `http://192.168.0.110:3001/api/v1/orgs/wooo/repos` 回 404,保留為 endpoint 判定 evidence | +| Gitea user API | 未認證查詢 `http://192.168.0.110:3001/api/v1/users/wooo/repos` 回 200,取得 public repos 2 個 | +| Gitea public search API | 未認證查詢 `/api/v1/repos/search` 回 200,取得 `wooo/awoooi`、`wooo/ewoooc` | +| 可重跑工具 | `python3 scripts/security/source-control-migration-inventory.py --repo . --gitea-remote gitea --github-remote origin --output-json docs/security/gitea-github-awoooi-inventory.snapshot.json --output-md docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | +| Gitea repo list 工具 | `python3 scripts/security/gitea-repo-inventory.py --base-url http://192.168.0.110:3001 --org wooo --scope user --github-owner owenhytsai --output-json docs/security/gitea-repo-inventory.snapshot.json --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` | +| Gitea read-only inventory approval | `docs/security/gitea-readonly-inventory-approval.snapshot.json` | +| Gitea public search 工具 | `python3 scripts/security/gitea-repo-inventory.py --base-url http://192.168.0.110:3001 --org public-search --github-owner owenhytsai --scope search --limit 100 --output-json docs/security/gitea-public-repo-search.snapshot.json --output-md docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` | +| 本機 remote 盤點工具 | `python3 scripts/security/local-git-remote-inventory.py --root /Users/ogt --root "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs" --max-depth 4 --output-json docs/security/local-git-remote-inventory.snapshot.json --output-md docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md` | +| GitHub target probe 工具 | `python3 scripts/security/github-target-probe.py --candidate owenhytsai/awoooi --candidate owenhytsai/clawbot-v5 --candidate owenhytsai/wooo-aiops --candidate owenhytsai/wooo-infra-config --candidate owenhytsai/ewoooc --candidate owenhytsai/bitan-pharmacy --candidate owenhytsai/tsenyang-website --candidate nexu-io/open-design --output-json docs/security/github-target-probe.snapshot.json --output-md docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` | +| 本機 canonical lineage 工具 | `python3 scripts/security/local-repo-canonical-probe.py --group-name ewoooc-momo-pro-system --repo local-momo-gitea=/Users/ogt/momo-pro-system --repo icloud-momo-gitea="/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system" --repo local-momo-gitlab=/Users/ogt/momo_pro_system --sample-limit 100 --git-timeout 8 --output-json docs/security/local-repo-canonical-ewoooc-momo.snapshot.json --output-md docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | +| Internal 110 refs 工具 | `python3 scripts/security/git-remote-refs-probe.py --group-name internal-110-bitan-tsenyang --repo bitan-pharmacy=/Users/ogt/bitan-pharmacy=origin --repo tsenyang-website=/Users/ogt/tsenyang-website=origin --output-json docs/security/git-remote-refs-bitan-tsenyang.snapshot.json --output-md docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | +| wooo-infra-config refs 工具 | `python3 scripts/security/git-remote-refs-probe.py --group-name wooo-infra-config-remotes --repo wooo-infra-config-gitea=/Users/ogt/wooo-infra-config=gitea --repo wooo-infra-config-github=/Users/ogt/wooo-infra-config=origin --output-json docs/security/git-remote-refs-wooo-infra-config.snapshot.json --output-md docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | +| GitHub target 決策 snapshot | `docs/security/github-target-decision.snapshot.json`,依前述 read-only evidence 人工彙整,非執行工具,不授權 repo 建立或 visibility 修改 | +| GitHub target repo-by-repo approval snapshot | `docs/security/github-target-repo-approval-package.snapshot.json`,逐 repo 拆分 approval path,不授權執行 | + +## 1.1 Gitea repo list snapshot + +| 欄位 | 值 | +|------|----| +| Schema | `gitea_repo_inventory_v1` | +| Status | `partial` | +| Query mode | `user` | +| Visibility scope | `public_only` | +| HTTP status | `200` | +| Repo count | `2` | +| Token present | `false` | +| Snapshot | `docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` / `docs/security/gitea-repo-inventory.snapshot.json` | +| Org endpoint blocked snapshot | `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md` / `docs/security/gitea-org-repo-inventory-blocked.snapshot.json` | +| 阻塞原因 | 未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出 | + +此結果代表目前已完成 public-only server-side repo list,但尚未完成「Gitea 所有專案」盤點。不得開始批量同步、刪除、封存或 GitHub primary 切換。 + +## 1.1.1 Gitea public search snapshot + +| 欄位 | 值 | +|------|----| +| Schema | `gitea_repo_inventory_v1` | +| Status | `partial` | +| HTTP status | `200` | +| Repo count | `2` | +| Token present | `false` | +| 可見 repos | `wooo/awoooi`、`wooo/ewoooc` | +| Snapshot | `docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` / `docs/security/gitea-public-repo-search.snapshot.json` | + +此結果代表 Gitea 有公開 repo 可被 read-only search 看到,但 private/internal repos 仍可能缺席。因此它只能補強 evidence,不能取代只讀 token 或管理匯出。完整操作方式見 `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md`。 + +## 1.2 本機 Git remote snapshot + +| 欄位 | 值 | +|------|----| +| Schema | `local_git_remote_inventory_v1` | +| Status | `partial` | +| 掃描 roots | `/Users/ogt`、`/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs` | +| Working tree count | `13` | +| Gitea linked working trees | `6` | +| GitHub linked working trees | `6` | +| Mapped working trees | `4` | +| Gitea-only working trees | `2` | +| GitHub-only working trees | `2` | +| 110 internal-only working trees | `3` | +| 去重後 Gitea repos | `wooo/awoooi`、`wooo/clawbot-v5`、`wooo/ewoooc`、`wooo/wooo-aiops` | +| 去重後 GitHub repos | `nexu-io/open-design`、`owenhytsai/awoooi`、`owenhytsai/clawbot-v5`、`owenhytsai/wooo-aiops`、`owenhytsai/wooo-infra-config` | +| 去重後 110 internal repos | `bitan-pharmacy`、`root/momo-pro-system`、`tsenyang-website`、`wooo/wooo-infra-config` | +| Snapshot | `docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md` / `docs/security/local-git-remote-inventory.snapshot.json` | + +此 snapshot 只能代表本機可見 working tree。它揭露了 Gitea API 之外的 source control 風險:仍有專案只連到 110 內部 remote 或 GitLab 類 remote,GitHub primary 切換前也要納入遷移矩陣。 + +第一版派工矩陣已建立於 `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md`。該矩陣將 `awoooi`、`ewoooc`、`clawbot-v5`、`wooo-aiops`、`bitan-pharmacy`、`tsenyang-website`、`wooo-infra-config` 等 source-control target 拆成 P0/P1/P2,不授權任何自動同步或刪除。 + +2026-05-12 追加 refs diff:已對 `wooo/clawbot-v5` 與 `wooo/wooo-aiops` 產生 read-only refs snapshot,兩者皆為 `blocked`。因此目前已驗證的 mapped repos 中,`awoooi`、`clawbot-v5`、`wooo-aiops` 都不是 GitHub primary ready。 + +Canonical repo 判定表已建立於 `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md`。`wooo/ewoooc`、`root/momo-pro-system`、`momo-pro-system`、`momo_pro_system` 目前列為待人工判定,不可自動合併。 + +GitHub target probe 已建立於 `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md`。`owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website` 目前未授權 read-only probe 看不到,因此不能視為已完成 GitHub target。 + +GitHub target 建立與可見性決策表已建立於 `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md`。目前 `github_target_decision_v1.status=draft`,8 個 target 候選中 7 個 `approval_required=true`。此表只能作為下一階段 approval evidence,不能自動建立 repo、修改 visibility、同步 refs 或切 GitHub primary。 + +GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md`。目前 `github_target_repo_approval_package_v1.status=draft`,7 個 approval items 全部 `pending`。此 package 用於分段批准與 owner / visibility / canonical 判定,不得被解讀為已批准推版或同步。 + +## 2. 目前 repo 對照 + +| 欄位 | Gitea | GitHub | 狀態 | +|------|-------|--------|------| +| Repo | `wooo/awoooi` | `owenhytsai/awoooi` | 已有對應 | +| `main` | `a18e2f9c3f403050d0fb7476bf6fdb860225731a` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | 不一致,阻塞主控切換 | +| `release/v1.0` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | 一致 | +| `dev` | `25889d4b8edcb83b6ec707c5eef3c21ae5d432b0` | 無 | GitHub 缺分支 | +| `drift/adopt-*` | 多條 | 無 | GitHub 缺分支 | +| `v7.2.0` | 有 | 無 | GitHub 缺 tag | +| `v7.3.0` | 有 | 無 | GitHub 缺 tag | + +## 3. Gitea-only 分支類型 + +| 類型 | 說明 | 建議處理 | +|------|------|----------| +| `dev` | Gitea 上存在,GitHub 不存在 | 判斷是否仍使用;若使用,需同步 | +| `drift/adopt-*` | GitOps / drift adoption 類分支 | 先保留並同步或封存,不可直接刪除 | +| `main` | Gitea 與 GitHub SHA 不一致 | 需確認哪一端是部署真相 | +| `release/v1.0` | 兩端 SHA 一致 | 可標為已對齊 | + +## 4. Credential Hygiene + +本輪發現本機 git remote 內嵌 Gitea 憑證。後續處理原則: + +1. 不把憑證值寫入任何文件、LOGBOOK、issue、PR 或 chat。 +2. 切換前移除 local remote URL 中的 embedded credential。 +3. 改用 credential helper、只讀 token、或部署專用 secret store。 +4. 對既有 token 做 rotation。 +5. 在 AwoooP / AWOOOI audit 中記錄「已輪替」的 evidence,不記錄 token value。 + +## 5. 全量專案盤點待辦 + +目前只完成本工作目錄的 `awoooi` repo 初步盤點。要滿足「Gitea 目前所有專案版本都轉移到 GitHub」,仍需完成: + +| 項目 | 狀態 | 備註 | +|------|------|------| +| Gitea org/repo list | 部分完成 | public-only user endpoint 已確認 2 個 repo;private/internal 仍需要只讀 token 或管理匯出 | +| 本機可見 Git remotes | 部分完成 | 只能當輔助 evidence,不等同 server 全量 | +| 每個 repo 的 GitHub 對應目標 | 部分完成 | 已有 8 個 target 候選與決策草案;仍需 owner / visibility / server-side refs 決策 | +| branches 全量 diff | 待盤點 | 每 repo 執行 heads 比對 | +| tags 全量 diff | 待盤點 | 每 repo 執行 tags 比對 | +| releases / artifacts | 待盤點 | Gitea API 或 UI 匯出 | +| issues / PR | 待盤點 | 需決定搬遷或封存 | +| workflows | 待盤點 | `.gitea/workflows` 改寫或保留 fallback | +| webhooks | 待盤點 | 對接 GitHub webhook 或 AwoooP event adapter | +| secrets 名稱 | 待盤點 | 只盤名稱與 owner,不搬 value | +| branch protection / CODEOWNERS | 待設計 | GitHub primary 前必備 | + +## 6. `source_control_migration_event_v1` 範例 + +```json +{ + "schema_version": "source_control_migration_event_v1", + "gitea_repo": "wooo/awoooi", + "github_repo": "owenhytsai/awoooi", + "branch_count_gitea": 115, + "branch_count_github": 2, + "tag_count_gitea": 2, + "tag_count_github": 0, + "latest_sha_gitea": "a18e2f9c3f403050d0fb7476bf6fdb860225731a", + "latest_sha_github": "202071f7a8724d5e8c29de441c3f380575a0ea94", + "workflows_mapped": false, + "webhooks_mapped": false, + "secrets_inventory_only": true, + "status": "blocked", + "blocking_reason": "Gitea 與 GitHub main SHA 不一致,且 GitHub 缺 Gitea-only branches/tags。" +} +``` + +此範例已由 `docs/security/gitea-github-awoooi-inventory.snapshot.json` 產生,並通過 `source_control_migration_event_v1` 必填欄位與 additional-properties 檢查。 + +## 7. 下一步 + +1. 依 `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` 取得 Gitea 只讀 repo inventory 批准,不使用寫入 token。 +2. 依 `github_target_decision_v1` 對需要人工批准的 target 做 owner / visibility / canonical 決策。 +3. 產生每個 repo 的 heads/tags SHA diff。 +4. 標記「可 mirror」、「需人工判斷」、「需封存」、「不可搬」。 +5. 產出 GitHub primary ADR,定義切換 gate 與 rollback。 +6. 將 `source_control_migration_event_v1`、`gitea_repo_inventory_v1`、`local_git_remote_inventory_v1` mirror 到 AwoooP,初期只作為 evidence。 diff --git a/docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md b/docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md new file mode 100644 index 00000000..6591fb06 --- /dev/null +++ b/docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md @@ -0,0 +1,34 @@ +# Source Control 遷移盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `blocked` | +| Gitea remote | `gitea` | +| GitHub remote | `origin` | +| Gitea repo | `wooo/awoooi` | +| GitHub repo | `owenhytsai/awoooi` | +| Gitea URL | `http://192.168.0.110:3001/wooo/awoooi.git` | +| GitHub URL | `https://github.com/owenhytsai/awoooi.git` | +| Gitea 分支數 | `117` | +| GitHub 分支數 | `2` | +| Gitea tags | `2` | +| GitHub tags | `0` | +| Gitea main | `0bc187877884f6fa6fe87a03dab99e9c6622fd42` | +| GitHub main | `202071f7a8724d5e8c29de441c3f380575a0ea94` | +| 阻塞原因 | branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 | + +## 分支差異 + +- 只在 Gitea:`115` +- 只在 GitHub:`0` +- SHA 不一致:`1` +- SHA 一致:`1` + +## Tag 差異 + +- 只在 Gitea:`2` +- 只在 GitHub:`0` +- SHA 不一致:`0` +- SHA 一致:`0` + +> 注意:本檔由 read-only inventory 工具產生,不包含 remote URL 內的帳密。 diff --git a/docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md b/docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md new file mode 100644 index 00000000..c643dba0 --- /dev/null +++ b/docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md @@ -0,0 +1,22 @@ +# Gitea Repo 全量盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `blocked` | +| Gitea base URL | `http://192.168.0.110:3001` | +| Org/User | `wooo` | +| GitHub owner 候選 | `owenhytsai` | +| 查詢模式 | `org` | +| 查詢字串 | `` | +| 可見性範圍 | `public_only` | +| 是否提供 token | `False` | +| HTTP status | `404` | +| Repo 數量 | `0` | +| 阻塞原因 | Gitea API 查無 org/user repos,需確認 org 名稱或使用管理匯出 | + +## Repo 清單 + +| Gitea repo | GitHub 候選 | default branch | private | archived | +|------------|------------------|----------------|---------|----------| + +> 注意:本檔由 read-only Gitea inventory 工具產生,不包含 API token 或 remote URL 帳密。 diff --git a/docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md b/docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md new file mode 100644 index 00000000..831b3940 --- /dev/null +++ b/docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md @@ -0,0 +1,24 @@ +# Gitea Repo 全量盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `partial` | +| Gitea base URL | `http://192.168.0.110:3001` | +| Org/User | `public-search` | +| GitHub owner 候選 | `owenhytsai` | +| 查詢模式 | `search` | +| 查詢字串 | `` | +| 可見性範圍 | `public_only` | +| 是否提供 token | `False` | +| HTTP status | `200` | +| Repo 數量 | `2` | +| 阻塞原因 | 未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出 | + +## Repo 清單 + +| Gitea repo | GitHub 候選 | default branch | private | archived | +|------------|------------------|----------------|---------|----------| +| `wooo/awoooi` | `owenhytsai/awoooi` | `main` | `False` | `False` | +| `wooo/ewoooc` | `owenhytsai/ewoooc` | `main` | `False` | `False` | + +> 注意:本檔由 read-only Gitea inventory 工具產生,不包含 API token 或 remote URL 帳密。 diff --git a/docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md b/docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md new file mode 100644 index 00000000..6ef06914 --- /dev/null +++ b/docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md @@ -0,0 +1,100 @@ +# Gitea Read-only 全量 Inventory Approval Package + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 草案,等待人工批准 | +| 來源事件 | `gitea_repo_inventory_v1` | +| Approval event | `docs/security/gitea-readonly-inventory-approval.snapshot.json` | +| Redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` | +| 目的 | 補齊 Gitea private/internal server-side repo list | +| 原則 | 低摩擦、只讀、只盤 metadata、不保存 token value、不做同步或主控切換 | + +## 0. 為什麼需要 approval + +目前已取得 public-only Gitea repo evidence: + +1. `users/wooo/repos` 可見 `wooo/awoooi`、`wooo/ewoooc`。 +2. `orgs/wooo/repos` 未認證查詢回 404,已保留 blocked evidence。 +3. 本機 remote inventory 顯示還有 `clawbot-v5`、`wooo-aiops`、110 internal / GitLab 類 remote 等來源。 + +因此 public-only evidence 不足以宣告「Gitea 所有專案版本」已盤完。下一步需要統帥或 repo owner 批准其中一條路徑: + +1. 提供 Gitea read-only token,僅用環境變數執行一次 inventory。 +2. 提供 Gitea 管理介面匯出的 redacted repo list JSON。 + +此 approval package 的目的不是提高日常開發門檻,而是只保護「會接觸 token 或管理匯出」這一個邊界。取得全量清單後,後續仍先進入 evidence / matrix / draft plan,不直接進入同步或阻擋。 + +## 1. 申請批准的動作 + +| 動作 | 風險 | 批准後允許 | 仍然禁止 | +|------|------|------------|----------| +| `run_gitea_readonly_inventory` | MEDIUM | 使用 read-only token 呼叫 Gitea API 盤 repo metadata | 寫入 Gitea、保存 token、同步 refs、建 repo | +| `import_gitea_admin_export` | MEDIUM | 匯入 redacted JSON,產生 `gitea_repo_inventory_v1.status=ok` evidence | 匯入 secret value、webhook secret、deploy key private key | + +## 2. Token 使用邊界 + +1. Token 只允許放在 `GITEA_READONLY_TOKEN` 環境變數。 +2. Token 不得寫入 `.env`、shell history、文件、LOGBOOK、issue、PR、snapshot。 +3. 工具輸出只保存 `token_present=true|false`。 +4. Token 權限必須只讀,不能有 repo delete、push、admin、secret read/write 權限。 +5. 執行後建議撤銷或輪替 token。 + +## 3. 批准後指令 + +```bash +GITEA_READONLY_TOKEN='' \ +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --scope user \ + --github-owner owenhytsai \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +替代路徑: + +```bash +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --github-owner owenhytsai \ + --input-json /path/to/redacted-gitea-repos.json \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +## 4. 驗收標準 + +| Gate | 必須結果 | +|------|----------| +| JSON 可解析 | `docs/security/gitea-repo-inventory.snapshot.json` 通過 parse | +| Schema 欄位 | `schema_version=gitea_repo_inventory_v1` | +| 全量狀態 | `status=ok` | +| 可見範圍 | `visibility_scope=authenticated` 或 `admin_export` | +| 敏感資訊 | 不含 token、password、secret value、private key | +| Redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` 已通過 | +| 後續動作 | 只更新 matrix / decision table,不同步 refs | + +## 5. 批准前不得做 + +1. 不要求使用者在對話中貼 token value。 +2. 不把 token 寫入任何檔案。 +3. 不用有寫入權限的 token。 +4. 不建立 GitHub repo。 +5. 不修改 GitHub repo visibility。 +6. 不 push、fetch mirror、sync refs。 +7. 不切 GitHub primary。 + +## 6. AwoooP 消費方式 + +AwoooP 可以把本 package mirror 成 approval candidate,但只能顯示: + +1. requested action。 +2. risk。 +3. required reviewers。 +4. evidence refs。 +5. blocked_until_approved。 + +AwoooP 不得直接要求、保存或傳遞 token value。 diff --git a/docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md b/docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md new file mode 100644 index 00000000..a50613f1 --- /dev/null +++ b/docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md @@ -0,0 +1,24 @@ +# Gitea Repo 全量盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `partial` | +| Gitea base URL | `http://192.168.0.110:3001` | +| Org/User | `wooo` | +| GitHub owner 候選 | `owenhytsai` | +| 查詢模式 | `user` | +| 查詢字串 | `` | +| 可見性範圍 | `public_only` | +| 是否提供 token | `False` | +| HTTP status | `200` | +| Repo 數量 | `2` | +| 阻塞原因 | 未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出 | + +## Repo 清單 + +| Gitea repo | GitHub 候選 | default branch | private | archived | +|------------|------------------|----------------|---------|----------| +| `wooo/awoooi` | `owenhytsai/awoooi` | `main` | `False` | `False` | +| `wooo/ewoooc` | `owenhytsai/ewoooc` | `main` | `False` | `False` | + +> 注意:本檔由 read-only Gitea inventory 工具產生,不包含 API token 或 remote URL 帳密。 diff --git a/docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md b/docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md new file mode 100644 index 00000000..6c541e1a --- /dev/null +++ b/docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md @@ -0,0 +1,137 @@ +# Gitea Server-side 全量 Repo Inventory Runbook + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 第一版,read-only / export-only | +| 工具 | `scripts/security/gitea-repo-inventory.py` | +| 事件 | `gitea_repo_inventory_v1` | +| Approval package | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` | +| Redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` | +| 原則 | 不寫入 Gitea、不搬 secret value、不建立或刪除 repo | + +## 0. 核心結論 + +目前已確認 `wooo` 在 Gitea 上以 user endpoint 可讀到 2 個 public repos: + +1. `wooo/awoooi` +2. `wooo/ewoooc` + +但未提供只讀 token 時,這只能代表 public-only 可見範圍,不等同完整 server-side repo list。要滿足「Gitea 目前所有專案版本都轉移到 GitHub」,仍需要下列其中一種來源: + +1. Gitea 只讀 token。 +2. Gitea 管理介面匯出的 repo list JSON。 +3. 由管理者在 Gitea 主機上產出的只含 repo metadata 的脫敏 JSON。 + +補充:`orgs/wooo/repos` 未認證查詢目前回 404,已保留於 `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md`。後續若確認 `wooo` 是 org 而非 user,應改用只讀 token 或管理匯出重新驗證。 + +上述兩條非 public-only 路徑都必須先走 `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md`,不得在對話、文件或 LOGBOOK 中保存 token value。 + +## 1. Public-only 快照指令 + +```bash +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --scope user \ + --github-owner owenhytsai \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +預期狀態:`partial`。 + +## 2. 只讀 token 快照指令 + +執行前必須先取得 `approval_required_event_v1` 批准。 + +```bash +GITEA_READONLY_TOKEN='' \ +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --scope user \ + --github-owner owenhytsai \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +注意: + +1. Token 只放在環境變數,不寫入文件。 +2. 輸出只保存 `token_present=true`,不保存 token value。 +3. 若 token 有權限讀 private/internal repos,狀態才可視為 `ok`。 + +## 3. 管理匯出 JSON 格式 + +工具接受以下三種結構: + +```json +[ + { + "full_name": "wooo/example", + "name": "example", + "owner": { + "login": "wooo" + }, + "private": true, + "archived": false, + "empty": false, + "default_branch": "main", + "clone_url": "http://192.168.0.110:3001/wooo/example.git", + "ssh_url": "git@192.168.0.110:wooo/example.git" + } +] +``` + +也接受: + +```json +{ + "repos": [] +} +``` + +或: + +```json +{ + "repositories": [] +} +``` + +## 4. 管理匯入指令 + +匯入前必須先確認 JSON 已 redacted,不含 token、password、webhook secret、deploy key private key 或 repository secret value。 +詳細檢查表見 `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md`。 + +```bash +python3 scripts/security/gitea-repo-inventory.py \ + --base-url http://192.168.0.110:3001 \ + --org wooo \ + --github-owner owenhytsai \ + --input-json /path/to/redacted-gitea-repos.json \ + --output-json docs/security/gitea-repo-inventory.snapshot.json \ + --output-md docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md +``` + +預期狀態:`ok`,且 `visibility_scope=admin_export`。 + +## 5. 不可包含的資料 + +1. API token value。 +2. Git remote URL 內嵌 username/password/token。 +3. Webhook secret value。 +4. Deploy key private key。 +5. Repository secret value。 +6. Cookie、session、personal access token。 + +## 6. 進入 GitHub migration 前的 gate + +只有在 `gitea_repo_inventory_v1.status=ok` 後,才可以進入下一階段: + +1. 每個 repo 建立 GitHub target 決策。 +2. 每個 repo 產生 branch/tag SHA diff。 +3. 每個 repo 盤點 workflow / webhook / runner / secret 名稱。 +4. 每個 repo 建立 mirror / archive / keep-local 判定。 +5. 產出 GitHub primary ADR 與 rollback plan。 diff --git a/docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md b/docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md new file mode 100644 index 00000000..ed050587 --- /dev/null +++ b/docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md @@ -0,0 +1,23 @@ +# GitHub Target Probe 快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `ok` | +| 候選 repo 數 | `8` | +| exists | `5` | +| not found or private | `3` | + +## 候選 Repo + +| GitHub repo | status | heads | error | +|-------------|--------|-------|-------| +| `owenhytsai/awoooi` | `exists` | `2` | 無 | +| `owenhytsai/clawbot-v5` | `exists` | `1` | 無 | +| `owenhytsai/wooo-aiops` | `exists` | `3` | 無 | +| `owenhytsai/wooo-infra-config` | `exists` | `1` | 無 | +| `owenhytsai/ewoooc` | `not_found_or_private` | `0` | GitHub 回應 repository not found;可能未建立或為 private 且未授權 | +| `owenhytsai/bitan-pharmacy` | `not_found_or_private` | `0` | GitHub 回應 repository not found;可能未建立或為 private 且未授權 | +| `owenhytsai/tsenyang-website` | `not_found_or_private` | `0` | GitHub 回應 repository not found;可能未建立或為 private 且未授權 | +| `nexu-io/open-design` | `exists` | `186` | 無 | + +> 注意:`not_found_or_private` 只代表未授權 read-only probe 看不到,不等同確認不存在。 diff --git a/docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md b/docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md new file mode 100644 index 00000000..13846d33 --- /dev/null +++ b/docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md @@ -0,0 +1,78 @@ +# GitHub Target Repo-by-repo Approval Package + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 草案,等待人工批准 | +| 上游決策 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | +| JSON snapshot | `docs/security/github-target-repo-approval-package.snapshot.json` | +| Schema | `docs/schemas/github_target_repo_approval_package_v1.schema.json` | +| 低摩擦 policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | +| 原則 | 低摩擦、逐 repo 決策,不自動建 repo、不改 visibility、不同步 refs、不切 primary | + +## 0. 核心結論 + +7 個 approval-required GitHub targets 已拆成三條批准路徑: + +1. Refs reconcile:`awoooi`、`clawbot-v5`、`wooo-aiops`。 +2. GitHub target 建立 / 授權:`ewoooc`、`bitan-pharmacy`、`tsenyang-website`。 +3. Internal remote 用途確認:`wooo-infra-config`。 + +這份 package 只讓 AwoooP / 統帥看到每個 repo 的批准條件與禁止動作,不代表已批准 push、mirror、repo creation、visibility 修改或 GitHub primary。 + +## 0.1 低摩擦分階段原則 + +初期資安網的目標是先建立框架與 evidence,不是把整個產品、架構、流程一次拉到最高限制。為避免造成開發、部署與營運流程過度複雜,本 package 採用以下原則: + +1. Read-only inventory、文件化、evidence mirror 預設允許繼續推進。 +2. 一般 repo 分類、owner 註記、風險標籤先用 `observe` / `warn`,不直接阻擋流程。 +3. 只有會建立 repo、修改 visibility、同步 refs、搬 secret、刪除 / 封存 repo、切 primary 的動作才需要 approval。 +4. Approval package 只做「下一步是否可執行」的邊界,不把每個低風險調查動作都變成審批。 +5. 每階段只收斂一小段控制面,避免一次導入過多工具、規則與流程。 + +## 1. Repo-by-repo 決策表 + +| GitHub target | Action | Risk | Required reviewers | Blocked until | +|---------------|--------|------|--------------------|---------------| +| `owenhytsai/awoooi` | refs reconcile plan | HIGH | migration-engineer、security-commander、human-owner | server-side inventory、refs/workflows/webhooks/secrets 名稱、primary ADR | +| `owenhytsai/clawbot-v5` | refs reconcile plan | MEDIUM | migration-engineer、human-owner | main SHA / tag 處理方式確認 | +| `owenhytsai/wooo-aiops` | refs reconcile plan | MEDIUM | migration-engineer、human-owner | main SHA、GitHub-only branch/tags 來源釐清 | +| `owenhytsai/wooo-infra-config` | confirm internal remote purpose | MEDIUM | migration-engineer、security-commander、human-owner | 110 remote 用途、infra secret 名稱 inventory | +| `owenhytsai/ewoooc` | create or grant access after canonical approval | HIGH | migration-engineer、security-commander、human-owner | momo/ewoooc canonical、server-side refs diff、owner / visibility | +| `owenhytsai/bitan-pharmacy` | create or grant access after canonical approval | MEDIUM | migration-engineer、human-owner | active 狀態、owner / visibility | +| `owenhytsai/tsenyang-website` | create or grant access after canonical approval | MEDIUM | migration-engineer、human-owner | active 狀態、owner / visibility | + +## 2. 批准後只允許的事 + +| Action | 批准後允許 | +|--------|------------| +| refs reconcile plan | 產生 reconcile plan、draft migration PR 或 ADR、更新 matrix evidence | +| create or grant access after canonical approval | 決定建立 GitHub repo 或授權既有 private repo、產生 migration plan | +| confirm internal remote purpose | 標記 remote 為 mirror / legacy / active source,更新 canonical decision table | + +## 3. 即使批准仍禁止的事 + +1. 直接 push refs。 +2. 直接同步 mirror。 +3. 直接切 GitHub primary。 +4. 直接停用、刪除或封存 Gitea repo。 +5. 自動合併 unrelated histories。 +6. 搬移 secret value。 +7. 修改 GitHub visibility 而沒有明確 repo owner 批准。 + +## 4. AwoooP 消費方式 + +AwoooP 可以 mirror `github_target_repo_approval_package_v1` 作為 approval queue 的分組 evidence,但只能: + +1. 顯示 repo、risk、required reviewers、blocked_until。 +2. 建立 approval candidate。 +3. 回傳 read-only policy 建議。 + +AwoooP 不得直接執行 GitHub repo creation、visibility change、refs sync 或 primary switch。 + +## 5. 下一步 + +1. 等待 Gitea full inventory approval 完成,補 private/internal repo list。 +2. 逐 repo 取得 owner / visibility / canonical 決策。 +3. 對 refs blocked repos 產生 reconcile plan。 +4. 對 `ewoooc` 先完成 canonical 判定,再決定 target 建立或授權。 diff --git a/docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md b/docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md new file mode 100644 index 00000000..03d55612 --- /dev/null +++ b/docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md @@ -0,0 +1,68 @@ +# GitHub Target 建立與可見性決策表 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 草案,等待人工決策 | +| 上游 evidence | `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md`、`docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | +| JSON snapshot | `docs/security/github-target-decision.snapshot.json` | +| Repo-by-repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | +| 原則 | 不自動建立 repo、不改 visibility、不同步 refs、不切 primary | + +## 0. 核心結論 + +目前 GitHub target 分成四類: + +1. 已存在但 refs blocked:`awoooi`、`clawbot-v5`、`wooo-aiops`。 +2. 已存在且本機 GitHub remote 對齊,但 110 internal remote 用途待判定:`wooo-infra-config`。 +3. GitHub target 未授權 probe 看不到:`ewoooc`、`bitan-pharmacy`、`tsenyang-website`。 +4. 外部/設計 repo,需 scope review:`nexu-io/open-design`。 + +因此現階段不得建立自動 mirror,也不得把 GitHub primary 視為 ready。 + +## 1. 決策表 + +| GitHub target | Source key | Probe | Target state | 建議動作 | 風險 | 人工批准 | +|---------------|------------|-------|--------------|----------|------|----------| +| `owenhytsai/awoooi` | `wooo/awoooi` | `exists` | `exists_refs_blocked` | hold refs reconcile | HIGH | 是 | +| `owenhytsai/clawbot-v5` | `wooo/clawbot-v5` | `exists` | `exists_refs_blocked` | hold refs reconcile | MEDIUM | 是 | +| `owenhytsai/wooo-aiops` | `wooo/wooo-aiops` | `exists` | `exists_refs_blocked` | hold refs reconcile | MEDIUM | 是 | +| `owenhytsai/wooo-infra-config` | `wooo/wooo-infra-config` | `exists` | `exists_aligned` | confirm internal remote purpose | MEDIUM | 是 | +| `owenhytsai/ewoooc` | `wooo/ewoooc` / `root/momo-pro-system` | `not_found_or_private` | `not_found_or_private` | create or grant access after approval | HIGH | 是 | +| `owenhytsai/bitan-pharmacy` | `bitan-pharmacy` | `not_found_or_private` | `not_found_or_private` | create or grant access after approval | MEDIUM | 是 | +| `owenhytsai/tsenyang-website` | `tsenyang-website` | `not_found_or_private` | `not_found_or_private` | create or grant access after approval | MEDIUM | 是 | +| `nexu-io/open-design` | `open-design` | `exists` | `external_scope` | scope review only | LOW | 否 | + +## 2. 建立 / 授權前 gate + +| Repo | Blocked until | +|------|---------------| +| `owenhytsai/ewoooc` | `ewoooc/momo-pro-system` canonical 關係人工確認、server-side refs diff、visibility/owner 決策 | +| `owenhytsai/bitan-pharmacy` | 確認仍 active、visibility/owner 決策 | +| `owenhytsai/tsenyang-website` | 確認仍 active、visibility/owner 決策 | +| `owenhytsai/wooo-infra-config` | 110 internal remote 用途確認、若為舊主控則降級或移除 | + +## 3. AwoooP 消費方式 + +AwoooP 可以 mirror `github_target_decision_v1` 作為 migration planning evidence,但只能做: + +- Runtime State / Channel Event 顯示。 +- read-only policy 建議。 +- approval candidate 建立。 + +AwoooP 不得直接做: + +- 建立 GitHub repo。 +- 修改 repo visibility。 +- 新增 secret。 +- 同步 refs。 +- 切 GitHub primary。 + +## 4. 下一步 + +1. 統帥或 repo owner 決定 `ewoooc`、`bitan-pharmacy`、`tsenyang-website` 的 GitHub target visibility。 +2. 針對 `ewoooc/momo-pro-system` 完成 server-side refs diff 與 canonical 判定。 +3. 確認 `bitan-pharmacy`、`tsenyang-website` 是否仍 active。 +4. 確認 `wooo-infra-config` 的 110 internal remote 是否應移除或保留為 mirror。 +5. 任何 repo 建立、visibility 修改或 mirror 行為,都必須先走 approval。 +6. Approval 只套用高風險執行動作;read-only inventory 與 evidence mirror 不應被過度阻擋。 diff --git a/docs/security/KALI-SECURITY-MESH-BLUEPRINT.md b/docs/security/KALI-SECURITY-MESH-BLUEPRINT.md new file mode 100644 index 00000000..70ec7cc2 --- /dev/null +++ b/docs/security/KALI-SECURITY-MESH-BLUEPRINT.md @@ -0,0 +1,233 @@ +# Kali 資訊安全網藍圖 + +> 日期:2026-05-06(台北時間) +> 範圍:`192.168.0.112` Kali 資安主機、所有 AWOOOI/AwoooP 資產、開發主機 `192.168.0.168` 與 `192.168.0.111` +> 姿態:先建框架、預設只觀測、後續分階段收攏 + +## 0. 核心決策 + +將 `192.168.0.112` 建置為整個架構的資安感測與驗證節點,但初期不採用高強度封鎖。 + +初期模式如下: + +| 層級 | 初期姿態 | 後續姿態 | +|---|---|---| +| 資產發現 | 唯讀盤點與可達性確認 | 持續資產漂移偵測 | +| 掃描 | 低噪音排程掃描 | 依風險與批准執行深度掃描 | +| 發現項 | 寫入證據、評分、路由 | SLA、責任歸屬、政策閘門 | +| 修復 | 僅提出建議 | dry-run、批准後受控執行 | +| 封鎖 | 除確認的機密外,不讓 CI/CD 強制失敗 | 逐步推進 P0/P1 閘門 | + +這遵守 MASTER 自主化飛輪規則:不建立靜態黑白名單決策引擎,不硬編修復動作,不在缺少 dry-run 與批准證據時執行破壞性操作。 + +## 1. 目前盤點結論 + +### 已經存在 + +| 面向 | 目前證據 | 缺口 | +|---|---|---| +| Kali 主機 | `192.168.0.112:8080` 已出現在 `docs/reference/SERVICE-ENDPOINTS.md`、`KALI_SCANNER_URL`、NetworkPolicy egress、blackbox probe、`KaliScannerDown` 告警 | 目前只監控存活,尚未形成完整掃描結果治理閉環 | +| 資產資料庫地基 | ADR-090 migration 已定義 `asset_inventory`、`asset_discovery_run`、`asset_coverage_snapshot`、`asset_compliance_snapshot` | 目前 scanner 以 K8s 為主;Docker、Gitea repos、網站、主機套件、Kali findings 還不完整 | +| AIOps KPI | `/api/v1/aiops/kpi` 會彙整資產、覆蓋率、規則品質、容量與自動化流量 | 資安姿態尚未成為 KPI 的一級區塊 | +| 合規掃描 | 已寫入 7 個 compliance 維度,SSL 與 Secret 年齡有部分檢查 | `cve_scan`、`audit_log_enabled`、`access_reviewed`、`encryption_at_rest` 多數仍是 `unknown` | +| 資安 Agent | 已有 `SecurityAgent`、`vuln-verifier`、`security_interceptor`、AwoooP security ADR | 尚未接成以 Kali 為核心的資安網 | +| 監控 | 已有 Prometheus/Alertmanager、SignOz、Sentry、blackbox、node exporter、Docker textfile exporter | 資安 finding 尚未正規化為 Prometheus metrics 與 incident taxonomy | +| 網路護欄 | `k8s/awoooi-prod/02-network-policy.yaml` 已 default-deny,並明確允許到 112/110/188/120/121/111 的必要 egress | 168 不是架構資產;111 主要被視為 Ollama fallback,尚未成為完整資安資產 | + +### 明顯缺口 + +| 缺口 | 影響 | 第一個修正方向 | +|---|---|---| +| `192.168.0.168` 沒進 inventory | 開發工作站對資安覆蓋不可見 | 加入資安資產登錄,初期只觀測 | +| `192.168.0.111` 主要被當成 Ollama fallback | Local AI/dev host 可能有模型、SSH、套件風險,但尚未以主機姿態納管 | 加入 host posture、service、model、package、access-review 檢查 | +| Kali findings 沒有標準 schema | 結果會變成一次性報告,不能成為 AI 飛輪證據 | 正規化為 `asset_compliance_snapshot` 與 security event envelope | +| 程式碼資安與 runtime 資安分離 | repo 漏洞、secrets、containers、live services 無法關聯 | 將 SAST、secret、SBOM、container scan 結果接到 `asset_inventory` | +| 缺少階段性 enforcement 表 | 初期容易過度收緊 | 使用 observe -> warn -> approve -> block 的推進閘門 | + +## 2. 資安資產地圖 + +| 類別 | 資產 | 需要的資安覆蓋 | +|---|---|---| +| 資安感測 | `192.168.0.112` Kali Scanner API | scanner health、scan job state、工具版本、結果匯出、保存週期 | +| DevOps 金庫 | `192.168.0.110` Harbor、Gitea、runners、Sentry、Langfuse、Grafana、Prometheus、Ollama proxy | registry image scan、runner hardening、暴露 port、TLS、secrets、CI workflow risk | +| K3s 控制面 | `192.168.0.120`、`192.168.0.121`、VIP `192.168.0.125` | kube-bench 類姿態、RBAC、NetworkPolicy、Pod Security、API exposure、node health | +| AI/Web/Data | `192.168.0.188` PostgreSQL、Redis、OpenClaw、SignOz、MinIO、momo services | DB exposure、backup restore proof、Redis ACL、web/API DAST、storage access、model endpoint exposure | +| Local AI/開發 | `192.168.0.111` Ollama local fallback 與開發主機職責 | host packages、Ollama API exposure、SSH policy、model inventory、fallback readiness | +| 開發工作站 | `192.168.0.168` local development origin | dev-only CORS origin、repo secrets hygiene、local service exposure、least-privilege access | +| 公開網站 | `awoooi.wooo.work`、`aiops.wooo.work`、`mo.wooo.work`、`stock.wooo.work`、`bitan.wooo.work` 與其他 active product domains | TLS、headers、auth、公開攻擊面、endpoint inventory、uptime/security correlation | +| 程式碼/專案 | AWOOOI、AwoooP、MOMO、Tsenyang、Stock、Bitan、future tenants | SAST、dependency/SBOM、secret scan、IaC scan、container scan、contract/policy drift | +| Source Control / CI/CD | Gitea 現有 repos、GitHub primary 候選、branches、tags、workflows、webhooks、runner secrets 名稱 | 供應鏈盤點、版本完整性比對、權限審查、GitHub primary / Gitea mirror 遷移 | +| 可觀測工具 | Prometheus、Alertmanager、SignOz、Sentry、Grafana、Langfuse | admin exposure、token hygiene、alert route integrity、audit export | +| 自動化工具 | MCP Gateway、SSH provider、K8s provider、GitOps、repair bots | tool grants、approval boundaries、command safety、audit and replay | + +## 3. 目標資料流 + +```text +Kali 112 掃描 / 驗證 + -> 資安結果 envelope + -> AWOOOI API ingestion + -> asset_inventory / asset_compliance_snapshot / asset_change_event + -> Prometheus metrics + Alertmanager secops taxonomy + -> incidents / timeline_events / automation_operation_log + -> SecurityAgent + Critic + Reviewer triage + -> Telegram / AwoooP Channel Event + -> 僅建議修復 + -> 後續 dry-run + approval + 受控執行 +``` + +第一個生產級契約應該是 `security_finding` envelope: + +| 欄位 | 意義 | +|---|---| +| `finding_id` | 由 scanner、target、rule id、evidence 產生的穩定 hash | +| `scan_run_id` | Kali run id,可在可能時連到 `asset_discovery_run` | +| `asset_key` | 既有 ADR-090 asset key | +| `target` | Host、URL、repo、image、K8s object、package 或 tool | +| `category` | `exposure`、`cve`、`secret`、`misconfig`、`auth`、`tls`、`web`、`code`、`supply_chain`、`network` | +| `severity` | AI review 前的 scanner severity | +| `confidence` | AI review 前的 scanner confidence | +| `evidence_ref` | 已脫敏證據指標,不存 raw secret 或完整 exploit output | +| `recommended_mode` | `observe`、`warn`、`approve_required`、`block_candidate` | +| `human_required` | 破壞性或權限變更類 action 必須為 true | + +## 4. 分階段推進 + +### Phase 0 - 基線與責任歸屬 + +目標:讓所有資產可見,但不執行 enforcement。 + +| 動作 | 主責 Agent | 完成證據 | +|---|---|---| +| 將 112/111/168 加進資安資產登錄 | Asset Cartographer | `asset_inventory` 有 host rows 與 tags | +| 依資產類型定義安全 scan scope | Security Orchestrator | scope file 含 `observe_only=true` | +| 保持 Kali 可達性告警為 health-only | SRE Sentinel | `KaliScannerDown` 仍是 warning,不 auto repair | +| 盤點公開網站與內部 endpoint | Web Perimeter Agent | website/api assets 有 TLS metadata | +| 盤點 Gitea 全量版本與 GitHub 對應目標 | Source Control Migration Steward | repo/branch/tag/workflow/webhook 對照表 | + +閘門:不封鎖、不做破壞性掃描、不在未批准時做 credentialed scan。 + +### Phase 1 - 被動與低噪音掃描 + +目標:不干擾服務地收集 findings。 + +| 面向 | 初期檢查 | +|---|---| +| 主機 | port inventory、若 SSH-approved 則 OS/package inventory、SSH banner/config posture | +| 網站 | TLS expiry、headers、基本 auth/session 檢查、安全 DAST crawl | +| K8s | RBAC、Pod Security labels、NetworkPolicy coverage、exposed Services | +| 容器 | image tag inventory、SBOM、已知 CVE scan、registry metadata | +| 程式碼 | secret scan、dependency scan、IaC scan、SAST warning inventory | +| 可觀測工具 | admin exposure、unauthenticated endpoints、token/config drift | + +閘門:findings 只進 `observe` 或 `warn`;只有確認的 hardcoded secret 可立即升為 P0。 + +### Phase 2 - 資安分類與告警 + +目標:把重複性 findings 轉成有用告警,而不是製造噪音。 + +新增告警分類: + +| 分類 | 範例 | 初期路由 | +|---|---|---| +| `secops_exposure` | 非預期 open port、公開 admin UI | Telegram security summary | +| `secops_cve` | critical vulnerable image/package | incident + owner | +| `secops_secret` | committed token、暴露 env、弱 secret handling | P0 human review | +| `secops_web` | TLS/header/auth weakness | warning ticket | +| `secops_k8s` | privileged pod、弱 RBAC、缺 NetworkPolicy | SRE + Security review | +| `secops_supply_chain` | unsigned image、unpinned dependency、runner risk | DevOps review | + +閘門:alert rule 先 warning-only;除既有 hard rules 外,不自動封鎖。 + +### Phase 3 - 建議與批准模式 + +目標:AI 提出修復,人類批准高風險變更。 + +| 動作 | 模式 | +|---|---| +| 建立 issue / Telegram card / AwoooP work item | automatic | +| 針對 headers、dependency bumps、NetworkPolicy additions 草擬 PR | automatic draft | +| rotate secret | approval required | +| 關閉 public port | approval required | +| 修改 RBAC / firewall / CORS | approval required | +| 執行更深層 active scan | approval required | + +閘門:每個建議都必須有 evidence、blast radius、rollback 與 owner。 + +### Phase 4 - 逐步 enforcement + +目標:將低噪音且已驗證的控制項提升為 gate。 + +推進順序: + +| Gate | 推進條件 | +|---|---| +| Secret scan gate | 14 天低 false positives,且 confirmed sanitizer | +| Container critical CVE gate | owner mapping 與 override workflow 已存在 | +| IaC dangerous pattern gate | dry-run 可抓問題且不阻擋有效部署 | +| Public exposure gate | inventory 完整且 approved exceptions 已存在 | +| K8s security gate | Pod Security/NetworkPolicy evidence 穩定 | + +除非 false-positive rate、owner routing、override、rollback path 都可量測,否則不得提升為 blocking gate。 + +## 5. 12 Agent 作業模型 + +| # | Agent | 責任 | 第一個 ownership | +|---|---|---|---| +| 1 | Security Commander | 資安網優先順序與 phase gates | 本藍圖、promotion criteria | +| 2 | Asset Cartographer | 主機、服務、repos、網站、網路、工具 | 112/111/168 的 ADR-090 asset keys | +| 3 | Kali Orchestrator | 掃描 scope、排程、工具 output normalization | `KALI_SCANNER_URL`、security finding envelope | +| 4 | Network Mapper | Port/service/topology drift | 110/112/120/121/188/111/168 + public domains | +| 5 | Web Perimeter Agent | DAST-safe website 與 API posture | public sites、AWOOOI web/API | +| 6 | Code Guardian | SAST、secrets、dependency、SBOM | repo 與 package scan evidence | +| 6A | Source Control Migration Steward | Gitea 全量版本盤點、GitHub primary 遷移、安全權限 | Gitea/GitHub repos、branches、tags、workflows、webhooks | +| 7 | Container Supply Agent | Harbor images、SBOM、image CVEs | registry 與 runtime image mapping | +| 8 | K8s Guard | RBAC、Pod Security、NetworkPolicy、API exposure | `k8s/awoooi-prod`、`k8s/pod-security` | +| 9 | Dev Host Steward | 168 與 111 developer host posture | observe-only host inventory | +| 10 | Observability Sentinel | Prometheus/SignOz/Sentry/Grafana security signals | metrics、alert rules、dashboards | +| 11 | Policy Gatekeeper | AwoooP policy、approval、exception handling | suggest/approve/block promotion | +| 12 | Evidence Archivist | 脫敏證據、audit、KM、timeline | `automation_operation_log`、KM、LOGBOOK | + +這是責任分工,不代表一定要同時啟動 12 個 coding session;只有在寫入範圍互不衝突時才並行。 + +## 6. 需要建置的整合點 + +| 整合 | 目前 anchor | 需要工作 | +|---|---|---| +| Kali API health | `KALI_SCANNER_URL`、`KaliScannerDown` | 新增 scan run/result endpoint 或 adapter | +| 資產盤點 | `asset_scanner_job.py` | 從 K8s 擴展到 hosts、Docker、Gitea/GitHub、websites、dev hosts | +| 合規 | `compliance_scanner_job.py` | 補上 `cve_scan`、`audit_log_enabled`、`access_reviewed`、`encryption_at_rest` | +| KPI | `AiopsKpiService` | 新增 `security_posture` 區塊 | +| 告警 | `alert_rules.yaml`、`alerts-unified.yml` | result schema 穩定後新增 secops taxonomy | +| AwoooP | ADR-106/107 contracts | enforcement 前先 mirror findings 到 Channel/Runtime events | +| 前端 | `/security-compliance` | 顯示 API 產生的真實 security posture,不用靜態 widgets | +| 知識 | KM/RAG + playbooks | 資安修復 outcomes 成為可檢索 evidence | + +## 7. 護欄 + +1. 預設不做破壞性掃描。 +2. 未經明確批准與維護窗口,不對 production 執行 exploit。 +3. 未定義 credential source、scope、audit trail 前,不做 credentialed scanning。 +4. Telegram、logs、KM 不得存 raw secret、token、cookie、key、exploit payload。 +5. Phase 0-2 不自動修改 firewall、RBAC、CORS、NetworkPolicy、route 或 secret rotation。 +6. 開發主機 `192.168.0.168` 與 `192.168.0.111` 初期為 `observe-only`,不是 compliance blocker。 +7. Security findings 必須用 stable fingerprint 去重後才能告警。 +8. 每個 finding 必須有 asset owner、severity、confidence、evidence pointer、next action。 +9. Blocking gate 必須有可量測 false-positive rate 與 override workflow。 +10. 長期授權由 AwoooP policy 負責;scanner 只提供 evidence。 + +## 8. 第一波實作建議 + +建議下一波程式實作: + +| 步驟 | 變更 | 風險 | +|---|---|---| +| 1 | 新增 security finding schema 與 adapter contract | 低 | +| 2 | 建立 Gitea 全量版本盤點與 GitHub primary / Gitea mirror 遷移清單,先只盤點與比對,不切主控 | 低 | +| 3 | 將 111/168/112 host records 加進 asset discovery seed | 低 | +| 4 | 新增 Kali result ingestion endpoint,只存脫敏 findings | 中 | +| 5 | 擴充 compliance scanner,消費 Kali findings 到 `cve_scan` 與 `secops_exposure` | 中 | +| 6 | 在 `/api/v1/aiops/kpi` 新增 `security_posture` | 低 | +| 7 | 從 normalized finding counts 輸出 warning-only Prometheus metrics | 中 | +| 8 | 新增前端 `/security-compliance` 真實資料 panel | 低 | + +本波不得部署自動封鎖或自動修復。 diff --git a/docs/security/KALI-SECURITY-MESH-EXECUTION-READINESS.md b/docs/security/KALI-SECURITY-MESH-EXECUTION-READINESS.md new file mode 100644 index 00000000..3881e872 --- /dev/null +++ b/docs/security/KALI-SECURITY-MESH-EXECUTION-READINESS.md @@ -0,0 +1,176 @@ +# Kali 資訊安全網開工準備 + +> 日期:2026-05-06(台北時間) +> 狀態:僅規劃,尚未開始實作 +> 上游藍圖:`docs/security/KALI-SECURITY-MESH-BLUEPRINT.md` +> AwoooP 同步:`docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md` + +## 0. 目前裁示 + +統帥已批准繼續推進 Kali 資訊安全網,但明確指示:**先不要開始實作**。 + +因此本文件只是一份開工準備地圖,用來定義工作包、責任歸屬、閘門、證據與批准邊界。本文件不授權 runtime 變更、部署、更嚴格封鎖、破壞性掃描、credentialed scanning 或自動修復。 + +2026-05-06 追加長期方向:GitHub 應評估回到雲端主控面,Gitea 降級為本地 mirror / fallback。此項納入本次資安工作包,原因是 source control、CI/CD、Code Review、Codex 接力與部署權限本身就是供應鏈安全邊界。 + +2026-05-06 追加同步:統帥批准本支線開始推進,並要求立即同步給 AwoooP 工作 Session。本支線已建立 `docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md`,作為雙 Session 共享契約與邊界文件。 + +## 1. 非實作邊界 + +目前允許: + +| 允許事項 | 用途 | +|---|---| +| 細化架構與 ownership | 降低後續 coding 前的模糊地帶 | +| 先在文件中定義 schema 與 event contract | 避免 scanner payload 各自為政 | +| 定義 observe-only scan scope | 讓初期 rollout 保持低摩擦 | +| 標出需要批准的事項 | 避免誤觸高風險掃描或封鎖 | +| 準備驗證清單 | 讓後續實作可量測 | +| 盤點 Gitea 到 GitHub 的版本轉移範圍 | 把 source control 與 CI/CD 納入供應鏈安全治理 | + +目前不允許: + +| 不允許事項 | 原因 | +|---|---| +| 新增或修改 API endpoints | 這已經是實作 | +| 修改 DB migrations 或 models | 需要 schema 決策與 review | +| 修改 NetworkPolicy、firewall、RBAC、CORS 或 secrets | 可能影響 production 連通性或安全邊界 | +| 部署 Kali tools、jobs、cron、exporters 或 alerts | 屬於 runtime 行為變更 | +| 開始 active、exploit 或 credentialed scans | 需要明確批准與 scope | +| 加入 CI/CD blocking gates | 初期資安網必須先觀測 | +| 自動 rotate secrets 或自動關閉 ports | 屬於破壞性或 access-changing 行為 | +| 直接把 GitHub 切成唯一主控 | 必須先完成 Gitea 全量版本盤點、同步、runner 驗證與 rollback plan | + +## 2. 開工準備閘門 + +| 閘門 | 問題 | 實作前必備 | +|---|---|---| +| G0 asset scope | 初期 observe-only scope 包含哪些資產? | 已批准 target list 與 owner map | +| G1 data contract | 正規化 finding envelope 長什麼樣? | 欄位清單、severity/confidence mapping、dedupe fingerprint | +| G2 storage | 每種 finding 要落到哪裡? | 對 ADR-090 tables 的 mapping 與是否需新表的決策 | +| G3 safety | 哪些 scan 是 passive、active 或 credentialed? | 每個 scope 的模式與批准規則 | +| G4 alerting | 哪些 findings 變成 warnings 或 incidents? | secops taxonomy 與 escalation threshold | +| G5 privacy | 哪些 evidence 可以儲存或顯示? | secrets、payloads、internal IPs、cookies 的 redaction rules | +| G6 ownership | 每一類 finding 由誰處理? | 12 Agent ownership 與 human owner mapping | +| G7 rollback | 如何快速停用 scanning/alerts? | kill switch 或 feature flags 已定義 | +| G8 source control | Gitea 現有專案版本如何完整轉移到 GitHub? | repo/branch/tag/workflow/webhook/secret inventory 與 SHA 比對計畫 | + +## 3. 初期 observe-only scope + +先從已知且不需要干擾式探測的資產開始。 + +| 範圍 | 目標 | 模式 | 備註 | +|---|---|---|---| +| Scanner health | `192.168.0.112:8080` | health-only | 保持現有 `KaliScannerDown` 語意 | +| Host inventory | 110、112、120、121、188、111、168 | passive / read-only | 168 與 111 必須先加入 observe-only dev assets | +| Website perimeter | 公開產品 domains 與 AWOOOI public routes | passive DAST-safe | 只做 TLS/header/basic crawl | +| K8s posture | manifests 與 live read-only K8s metadata | read-only | RBAC、Pod Security、NetworkPolicy coverage | +| Container inventory | Harbor/runtime image list | read-only | 先盤 image metadata;CVE scan 等 schema mapping 完成 | +| Codebase | repo files、manifests、dependencies | local static scan | secrets/SAST/SBOM,不 blocking | +| Source control plane | Gitea repos、GitHub repos、branches、tags、workflows、webhooks | inventory-only | 先做版本與權限盤點,不切主控 | +| Observability tools | Prometheus、Alertmanager、SignOz、Sentry、Grafana、Langfuse | passive exposure review | 不做 admin action | + +## 4. Finding contract 草案 + +第一波 coding 應以這份作為標準契約草案。 + +| 欄位 | 必填 | 說明 | +|---|---:|---| +| `finding_id` | 是 | 穩定且可重算的 fingerprint | +| `scan_run_id` | 是 | Scanner run 或 ADR-090 discovery run | +| `scanner` | 是 | `kali`、`trivy`、`zap`、`semgrep`、`detect-secrets` 等 | +| `scanner_version` | 否 | 進入 enforcement 後必填 | +| `asset_key` | 是 | 相容 ADR-090 的 asset key | +| `target_type` | 是 | `host`、`website`、`api_endpoint`、`container`、`package`、`repo`、`k8s_resource`、`tool` | +| `target` | 是 | 已脫敏 target identifier | +| `category` | 是 | `exposure`、`cve`、`secret`、`misconfig`、`auth`、`tls`、`web`、`code`、`supply_chain`、`network` | +| `severity` | 是 | scanner 原始 severity 映射到 platform severity | +| `confidence` | 是 | scanner confidence 或 verifier confidence | +| `status` | 是 | `new`、`confirmed`、`false_positive`、`accepted_risk`、`fixed`、`expired` | +| `recommended_mode` | 是 | `observe`、`warn`、`approve_required`、`block_candidate` | +| `evidence_ref` | 是 | 指向脫敏 evidence,不存 raw exploit 或 secret | +| `summary` | 是 | 人可讀摘要,不能含敏感 payload | +| `recommended_action` | 是 | 初期只作為建議 | +| `owner_team` | 否 | 升級 incident 前必填 | +| `expires_at` | 否 | 暫時 accepted risk 時可用 | + +## 5. ADR-090 落點地圖 + +| Finding 類型 | 主要落點 | 次要落點 | 升級方式 | +|---|---|---|---| +| Port/service exposure | `asset_inventory.metadata`、`asset_compliance_snapshot` | `asset_change_event` | 非預期且重複時才 alert | +| CVE/package/image | `asset_compliance_snapshot(cve_scan)` | `asset_coverage_snapshot(auto_alerting/playbook)` | 超過 threshold 才產生 `CVECritical` | +| Repo/config secret | `asset_compliance_snapshot(secret_rotated or secret_scan)` | `automation_operation_log` | confirmed 後立即 P0 human review | +| TLS/header/web issue | `asset_compliance_snapshot(ssl_cert_valid/web)` | website asset metadata | 先 warning ticket | +| K8s misconfig | `asset_compliance_snapshot(access_reviewed/encryption_at_rest)` | `asset_coverage_snapshot` | SRE/Security review | +| Monitoring blind spot | `asset_coverage_snapshot(auto_monitoring/auto_alerting)` | `alert_rule_catalog` | 僅 coverage gap alert | +| Tool permission risk | `asset_compliance_snapshot(access_reviewed)` | AwoooP policy/gateway audit | approval-required work item | +| Source control drift | `asset_inventory.metadata`、`asset_compliance_snapshot(access_reviewed)` | `automation_operation_log` | Gitea/GitHub SHA 或權限漂移時先 warning | + +## 6. 12 Agent 工作包 + +| Agent | 工作包 | 實作前第一份交付 | +|---|---|---| +| Security Commander | Phase gates 與 escalation policy | 已簽核 observe-only scope | +| Asset Cartographer | Asset taxonomy 與 seed list | 112/111/168 + core assets mapping | +| Kali Orchestrator | Scanner jobs 與 output contract | Scanner-to-finding normalization spec | +| Network Mapper | Port 與 route inventory | Expected vs unexpected exposure table | +| Web Perimeter Agent | Website/API scan policy | Safe crawl 與 TLS/header checklist | +| Code Guardian | SAST/secrets/dependency scope | Repo scan tools 與 false-positive rules | +| Container Supply Agent | Image/SBOM/CVE flow | Harbor/runtime image map | +| K8s Guard | RBAC/PodSecurity/NetworkPolicy review | K8s posture checklist | +| Dev Host Steward | 168/111 observe-only policy | Dev host safe probe boundaries | +| Observability Sentinel | Metrics 與 alert wiring design | warning-only metric names 與 labels | +| Policy Gatekeeper | AwoooP approval/exception model | approve/block promotion criteria | +| Evidence Archivist | Evidence storage 與 redaction | sanitized evidence rules | +| Source Control Migration Steward | Gitea 全量版本盤點與 GitHub primary 遷移設計 | repo/branch/tag/workflow/webhook 對照表 | + +## 7. 批准邊界 + +以下事項必須在未來實作前另外取得明確批准: + +| 邊界 | 需要批准的原因 | +|---|---| +| 對 110/111/112/120/121/168/188 做 credentialed host scans | 會使用具權限存取 | +| 對 production websites 做 active DAST | 可能產生流量、噪音或狀態變更 | +| 對 live production 做 exploit verification | 高營運風險 | +| 修改 firewall/NetworkPolicy/RBAC/CORS | 會改變連通性與安全邊界 | +| Secret rotation | 可能中斷整合 | +| CI/CD blocking gates | 可能阻擋 release | +| 超過 ticket/PR draft 的 auto-remediation | 有 side effects | +| 新增付費 scanner/SaaS/provider | 觸發費用批准規則 | +| GitHub primary 切換 | 會改變 source control 主控面、runner、secrets 與部署審批鏈 | + +## 8. 第一波程式實作開工清單 + +只有這份清單被接受後,才能開始實作。 + +| 檢查項 | 狀態 | +|---|---| +| Observe-only asset scope 已接受 | 待確認 | +| `security_finding_v1` envelope 已接受 | 待確認 | +| Storage landing map 已接受 | 待確認 | +| 112/111/168 asset classification 已接受 | 待確認 | +| Redaction policy 已接受 | 待確認 | +| No-blocking phase boundary 已接受 | 待確認 | +| Human approval boundaries 已接受 | 待確認 | +| Rollback/disable flag 已命名 | 待確認 | +| Gitea 全量轉 GitHub inventory 已接受 | 待確認 | +| GitHub primary / Gitea mirror rollback plan 已接受 | 待確認 | +| AwoooP mirror-only handoff 已同步 | 已完成 | +| 共享事件 JSON Schema 已建立 | 已完成 | + +## 9. 未來批准後的第一波實作順序 + +等未來明確批准開始實作後,建議順序如下: + +1. 新增只定義契約的 data model 或 `security_finding_v1` Pydantic schema。 +2. 產出 Gitea 全量版本盤點:repositories、branches、tags、workflows、webhooks、secret 名稱、GitHub 對應目標。 +3. 在 scanner path 加入 112/111/168 的 host asset seed。 +4. 在 feature flag 後新增 Kali result ingestion endpoint 或 adapter。 +5. 將已脫敏 findings 存入 ADR-090-compatible snapshots。 +6. 在 AIOps KPI 加入 `security_posture` read model。 +7. 依 severity/category/asset 輸出 warning-only metrics。 +8. 前端只增加 visibility,不增加 action buttons,最多 view/filter。 + +在自動封鎖、自動修復、active exploit verification、credentialed scans 之前必須停止。 diff --git a/docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md b/docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md new file mode 100644 index 00000000..2eb07a4c --- /dev/null +++ b/docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md @@ -0,0 +1,39 @@ +# 本機 Git Remote 盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `partial` | +| 掃描 root | `/Users/ogt, /Users/ogt/Library/Mobile Documents/com~apple~CloudDocs` | +| max depth | `4` | +| Gitea host fragment | `192.168.0.110:3001` | +| repo 數量 | `13` | +| Gitea linked | `6` | +| GitHub linked | `6` | +| mapped | `4` | +| Gitea-only local | `2` | +| GitHub-only local | `2` | +| Internal 110-only local | `3` | +| 去重後 Gitea repo | `4` | +| 去重後 GitHub repo | `5` | +| 去重後 110 內部 repo | `4` | + +## Repo 對照 + +| 狀態 | 本機路徑 | Gitea repo | GitHub repo | 110 內部 remote | +|------|----------|------------|-------------|----------------| +| `mapped` | `/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/awoooi` | `wooo/awoooi` | `owenhytsai/awoooi` | - | +| `gitea_only_local` | `/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system` | `wooo/ewoooc` | - | - | +| `github_only_local` | `/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/open-design` | - | `nexu-io/open-design` | - | +| `other_remote` | `/Users/ogt/aider-watch` | - | - | - | +| `mapped` | `/Users/ogt/awoooi` | `wooo/awoooi` | `owenhytsai/awoooi` | - | +| `internal_110_only` | `/Users/ogt/bitan-pharmacy` | - | - | `bitan-pharmacy` | +| `mapped` | `/Users/ogt/clawbot-v5` | `wooo/clawbot-v5` | `owenhytsai/clawbot-v5` | - | +| `gitea_only_local` | `/Users/ogt/momo-pro-system` | `wooo/ewoooc` | - | - | +| `internal_110_only` | `/Users/ogt/momo_pro_system` | - | - | `root/momo-pro-system` | +| `other_remote` | `/Users/ogt/stockPlatform` | - | - | - | +| `internal_110_only` | `/Users/ogt/tsenyang-website` | - | - | `tsenyang-website` | +| `mapped` | `/Users/ogt/wooo-aiops` | `wooo/wooo-aiops` | `owenhytsai/wooo-aiops` | - | +| `github_only_local` | `/Users/ogt/wooo-infra-config` | - | `owenhytsai/wooo-infra-config` | `wooo/wooo-infra-config` | + +> 注意:本檔只代表本機指定 roots 可見的 Git working tree,不等同 Gitea server 全量 repo 清單。 +> 輸出前已移除 remote URL 中的 username、password、token。 diff --git a/docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md b/docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md new file mode 100644 index 00000000..3fbf2394 --- /dev/null +++ b/docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md @@ -0,0 +1,28 @@ +# 本機 Repo Canonical Lineage Probe 快照 + +| 項目 | 值 | +|------|----| +| 群組 | `ewoooc-momo-pro-system` | +| 狀態 | `unrelated` | +| repo 數 | `3` | +| 比對數 | `3` | +| sample limit | `100` | +| git timeout seconds | `8` | + +## Repo HEAD + +| Label | Path | Branch | HEAD | Remotes | +|-------|------|--------|------|---------| +| `local-momo-gitea` | `/Users/ogt/momo-pro-system` | `main` | `61a9c4c` | `origin:http://192.168.0.110:3001/wooo/ewoooc.git` | +| `icloud-momo-gitea` | `/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system` | `main` | `fe6180b` | `origin:http://192.168.0.110:3001/wooo/ewoooc.git` | +| `local-momo-gitlab` | `/Users/ogt/momo_pro_system` | `main` | `5a4294e` | `gitlab:http://192.168.0.110:8929/root/momo-pro-system.git` | + +## Lineage 比對 + +| Left | Right | Relation | Common commits | +|------|-------|----------|----------------| +| `local-momo-gitea` | `icloud-momo-gitea` | `no_shared_history` | `0` | +| `local-momo-gitea` | `local-momo-gitlab` | `no_shared_history` | `0` | +| `icloud-momo-gitea` | `local-momo-gitlab` | `no_shared_history` | `0` | + +> 注意:本檔只比較本機 Git 物件,未 fetch 遠端;common commit sample 只用 SHA,不含 commit message。 diff --git a/docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md b/docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md new file mode 100644 index 00000000..736d0109 --- /dev/null +++ b/docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md @@ -0,0 +1,73 @@ +# 低摩擦資安 Rollout Policy + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 草案 | +| JSON snapshot | `docs/security/security-rollout-policy.snapshot.json` | +| Schema | `docs/schemas/security_rollout_policy_v1.schema.json` | +| 原則 | 初期先建立框架、可見性與追溯性,不把資安限制一次拉到最高 | + +## 0. 核心結論 + +本資安網初期採 `observe-first`,不是 `block-first`。 + +目標是讓 Kali、Code Review、GitHub、Gitea、Codex、AwoooP 先共享 evidence、狀態與 approval boundary。初期不應把每個 LOW / MEDIUM finding、repo 差異、文件缺口、owner 待定都變成阻擋條件。 + +## 1. 四種模式 + +| Mode | 用途 | 初期處理 | +|------|------|----------| +| `observe` | read-only inventory、evidence mirror、狀態追蹤 | 允許持續推進,不阻擋 | +| `warn` | LOW / MEDIUM observation、非不可逆差異 | 產生 follow-up,不自動卡流程 | +| `approve_required` | 會碰 token、repo 建立、visibility、refs、secret、deploy、primary switch | 建立 approval candidate,批准後才執行下一步 | +| `block_candidate` | 無 rollback 的破壞性動作、保存 raw secret、force push | 預設不執行,只能走人工 exception | + +## 2. 低摩擦矩陣 + +| 條件 | Mode | 可以做 | 不可以做 | +|------|------|--------|----------| +| Read-only inventory / evidence mirror | `observe` | 收 metadata、寫 redacted snapshot、更新文件、mirror 到 AwoooP | 寫入遠端、刪 repo、sync refs | +| LOW / MEDIUM observation,且不涉及不可逆變更 | `warn` | 標風險、建 follow-up、準備草案 | 擋 deploy、自動 patch、自動 merge | +| 使用 read-only token 或管理匯出 | `approve_required` | 人工批准後單次執行、只保存 `token_present` | 保存 token value、使用寫入 token、建立 repo | +| 建 repo / 改 visibility / sync refs | `approve_required` | 建 approval candidate、準備 migration / rollback plan | 未批准直接執行、直接 push refs、切 primary | +| secret / RBAC / NetworkPolicy / firewall / deploy / primary switch | `approve_required` | 建 approval event、準備 dry-run / rollback | auto execute、保存 secret value、跳過人工 review | +| 無 rollback 的破壞性動作或保存 raw secret | `block_candidate` | 記錄原因、要求人工 exception | force push、delete repo、保存 raw secret、關閉 audit | + +## 3. AwoooP 初期行為 + +AwoooP 初期只應: + +1. mirror Runtime State / Channel Event / Audit evidence。 +2. 計算 read-only policy 建議。 +3. 建立必要的 approval candidate。 +4. 顯示阻塞原因,但不直接執行修復。 + +AwoooP 初期不應: + +1. 直接啟動 Kali active scan。 +2. 直接呼叫 Codex patch runner。 +3. 直接建 GitHub repo 或修改 visibility。 +4. 直接同步 refs 或切 GitHub primary。 +5. 把所有 observation 都變成 blocking incident。 + +## 4. 階段性收斂 + +| 階段 | 目標 | 控制強度 | +|------|------|----------| +| S0 | 文件與契約同步 | observe | +| S1 | read-only evidence 建立 | observe / warn | +| S2 | AwoooP mirror-only | observe / warn | +| S3 | 高風險 approval gate | approve_required | +| S4 | 受控 migration / execution | approve_required + rollback | +| S5 | 規則收斂與自動化 | 只在 evidence 成熟後逐步提高 | + +## 5. 永久邊界 + +即使後續提高資安等級,以下仍不得自動化: + +1. 保存 raw secret / token value。 +2. 無 rollback 的 repo 刪除 / force push。 +3. 未經 owner 批准的 visibility 修改。 +4. 未完成 parity 的 GitHub primary switch。 +5. 未經人工批准的 production deploy。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md new file mode 100644 index 00000000..9706ede8 --- /dev/null +++ b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md @@ -0,0 +1,57 @@ +# Security Supply Chain Contract Manifest + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 草案 | +| JSON snapshot | `docs/security/security-supply-chain-contract-manifest.snapshot.json` | +| Schema | `docs/schemas/security_supply_chain_contract_manifest_v1.schema.json` | +| 預設 enforcement | `mirror_only` | +| 原則 | AwoooP 先讀 manifest,再依合約 mirror / read-only policy / approval queue 消費 | + +## 0. 核心結論 + +目前 Security Supply Chain 已有 12 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 + +初期預設仍是 `mirror_only`。Manifest 不授權 runtime enforcement、不授權 GitHub/Gitea 主控切換、不授權 repo 建立或 refs sync。 + +## 1. Contract 清單 + +| Contract | Consumption | 主要用途 | Snapshot | +|----------|-------------|----------|----------| +| `security_rollout_policy_v1` | read-only policy | 低摩擦 observe-first policy | `docs/security/security-rollout-policy.snapshot.json` | +| `security_finding_v1` | mirror-only | Kali / code / infra finding | 無正式 snapshot | +| `coding_task_v1` | suggest-only | Code Review 接 Codex patch-only | 無正式 snapshot | +| `source_control_migration_event_v1` | mirror-only | Gitea/GitHub refs 差異 | `gitea-github-awoooi`、`clawbot-v5`、`wooo-aiops` | +| `gitea_repo_inventory_v1` | mirror-only | Gitea repo inventory | public-only / blocked endpoint snapshots | +| `local_git_remote_inventory_v1` | mirror-only | 本機 remote coverage | `local-git-remote-inventory.snapshot.json` | +| `github_target_probe_v1` | mirror-only | GitHub target visibility | `github-target-probe.snapshot.json` | +| `github_target_decision_v1` | mirror-only | GitHub target 決策 | `github-target-decision.snapshot.json` | +| `github_target_repo_approval_package_v1` | approval-only | 逐 repo approval queue draft | `github-target-repo-approval-package.snapshot.json` | +| `local_repo_canonical_probe_v1` | mirror-only | momo/ewoooc lineage evidence | `local-repo-canonical-ewoooc-momo.snapshot.json` | +| `git_remote_refs_probe_v1` | mirror-only | 110 / GitHub remote refs readiness | `bitan-tsenyang`、`wooo-infra-config` | +| `approval_required_event_v1` | approval-only | 高風險 / 敏感邊界 approval | `gitea-readonly-inventory-approval.snapshot.json` | + +## 2. AwoooP 消費順序 + +1. 先讀 `security_rollout_policy_v1`,確認目前仍是 `mirror_only`。 +2. 再讀本 manifest,取得可消費 contract 與禁止動作。 +3. 將 snapshot mirror 成 Runtime State / Channel Event / Audit evidence。 +4. 只對 `approval_required_event_v1` 與 repo approval package 建 approval candidate。 +5. 不新增 execution action button,不做 runtime enforcement。 + +## 3. 永久禁止 + +1. 不保存 raw secret、token、cookie、private key。 +2. 不直接啟動 Kali active scan。 +3. 不直接呼叫 Codex patch runner。 +4. 不直接建立 GitHub repo 或修改 visibility。 +5. 不直接同步 refs。 +6. 不切 GitHub primary。 +7. 不停用、刪除、封存 Gitea repo。 + +## 4. 下一步 + +1. AwoooP 主線可把 manifest 當作 mirror-only contract index。 +2. Security Supply Chain Session 後續新增 schema / snapshot 時,必須同步更新本 manifest。 +3. 等 runtime integration 被正式批准前,本 manifest 只作文件與 evidence 路由,不作 execution router。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md new file mode 100644 index 00000000..4f48156f --- /dev/null +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -0,0 +1,73 @@ +# Security Supply Chain 整體進度 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | S0/S1 read-only evidence 建置中 | +| 本階段完成 | Security Supply Chain contract manifest | +| 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | + +## 0. 本階段完成後整體進度 + +| 階段 | 狀態 | 目前結果 | 下一個 gate | +|------|------|----------|-------------| +| S0 文件與契約同步 | 完成 | Kali / Codex / GitHub / Gitea / AwoooP 邊界已文件化,核心 schema 草案已建立 | AwoooP 只讀 mirror 消費 | +| S1 source-control read-only inventory | 進行中 | 已有 Gitea/GitHub refs、Gitea public-only user repo list、本機 remote、GitHub target probe、canonical lineage、110 refs evidence | Gitea private/internal 全量 repo list | +| S1.0 Gitea 全量 inventory approval | 完成草案 | 已建立 read-only token / admin export approval package | 統帥或 repo owner 批准 | +| S1.1 GitHub target 決策 | 完成草案 | 8 個 target 候選,7 個需人工批准;3 個 `not_found_or_private` 不得自動建立 | owner / visibility / canonical approval | +| S1.2 GitHub target 逐 repo approval | 完成草案 | 7 個 approval-required targets 已拆成逐 repo pending package | 低摩擦逐項批准 | +| S1.3 低摩擦 rollout policy | 完成草案 | observe-first / mirror-only matrix 已建立 | AwoooP read-only policy 消費 | +| S1.4 Contract manifest | 完成草案 | 12 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | +| S2 AwoooP mirror-only | 可交接 | `AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md` 已列出可消費事件與禁止動作 | AwoooP 主線建立只讀入口 | +| S3 approval gate | 未開始 | 已定義哪些動作要進 approval | 不得繞過人工批准 | +| S4 migration execution | 未開始 | GitHub primary 長期方向已確認,但 refs / tags / workflow / secret 名稱尚未全量驗證 | SHA/tag/workflow parity 與 rollback ADR | + +## 1. 已建立的主要 evidence + +| 類型 | 檔案 | +|------|------| +| AwoooP handoff | `docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md` | +| Mirror-only 清單 | `docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md` | +| Gitea/GitHub migration inventory | `docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md` | +| Gitea server-side inventory runbook | `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md` | +| Gitea read-only inventory approval package | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` | +| Gitea read-only inventory approval JSON | `docs/security/gitea-readonly-inventory-approval.snapshot.json` | +| Gitea 管理匯出 redaction checklist | `docs/security/GITEA-ADMIN-EXPORT-REDACTION-CHECKLIST.md` | +| Gitea org endpoint blocked evidence | `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md` | +| Source-control migration matrix | `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | +| Canonical repo 判定表 | `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md` | +| GitHub target 決策表 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | +| GitHub target 決策 JSON | `docs/security/github-target-decision.snapshot.json` | +| GitHub target repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | +| GitHub target repo approval JSON | `docs/security/github-target-repo-approval-package.snapshot.json` | +| 低摩擦 rollout policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | +| 低摩擦 rollout policy JSON | `docs/security/security-rollout-policy.snapshot.json` | +| Security Supply Chain contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` | +| Security Supply Chain contract manifest JSON | `docs/security/security-supply-chain-contract-manifest.snapshot.json` | + +## 2. 現在不能做的事 + +1. 不建立或刪除 GitHub / Gitea repo。 +2. 不修改 repo visibility。 +3. 不同步 refs、branch、tag。 +4. 不切 GitHub primary。 +5. 不把 Codex patch runner、Kali scan 或 deploy 接進 AwoooP runtime。 +6. 不保存 secret / token value。 + +## 2.1 初期不要過度收緊 + +1. Read-only inventory、文件化、risk label、mirror evidence 可持續推進。 +2. 初期不把 LOW / MEDIUM observation 變成阻擋條件。 +3. 初期不要求所有 repo 一次完成最高等級 controls。 +4. 只針對不可逆或高風險動作設 approval gate。 +5. 每階段完成後再逐步收斂,避免讓產品、架構與部署流程突然變複雜。 + +## 3. 下一階段建議 + +1. 等待 Gitea read-only inventory approval 被批准後,再用只讀 token 或管理匯出補 private/internal server-side 全量 repo list。 +2. 對 7 個 `approval_required=true` 的 GitHub target 做 owner / visibility / canonical 決策。 +3. 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 做 refs / tags reconcile 計畫。 +4. 對 `ewoooc` / `momo-pro-system` 完成 server-side canonical 判定。 +5. AwoooP 主線只建立 mirror-only / read-only policy 入口,不新增執行按鈕。 +6. AwoooP 主線消費 `security_rollout_policy_v1` 時,只做 read-only policy,不做 runtime blocking。 +7. AwoooP 主線先讀 `security_supply_chain_contract_manifest_v1` 作為 contract registry,不新增 execution router。 diff --git a/docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md b/docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md new file mode 100644 index 00000000..9260c472 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md @@ -0,0 +1,116 @@ +# Source Control Canonical Repo 判定表 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 第一版,read-only evidence | +| 上游 evidence | `docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md`、`docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md`、`docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md`、`docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md`、`docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | +| 原則 | 只判定風險與下一步,不合併、不同步、不刪除、不切 primary | + +## 0. 核心結論 + +目前不能假設「本機資料夾名稱相近」就代表同一個 canonical repo。 + +尤其 `momo-pro-system`、`momo_pro_system`、`wooo/ewoooc`、`root/momo-pro-system` 看起來高度相關,但來源 remote、repo host、HEAD 都不同。第一輪本機 lineage probe 顯示三個 working tree 在最近 100 個 commits sample 內沒有共同 commit,因此這一組必須先人工判定 canonical 關係,否則 GitHub 遷移時可能發生錯誤合併、漏轉、或把舊版覆蓋新版。 + +## 1. Server-side public search 結果 + +未提供 token 的 Gitea `users/wooo/repos` 與 public repo search 可見: + +| Gitea repo | default branch | GitHub candidate | 狀態 | +|------------|----------------|------------------|------| +| `wooo/awoooi` | `main` | `owenhytsai/awoooi` | 已有 GitHub,但 refs blocked | +| `wooo/ewoooc` | `main` | `owenhytsai/ewoooc` | GitHub candidate 未建立或不可見 | + +此結果只代表 public-only 可見範圍,private/internal repos 仍需只讀 token 或管理匯出。操作契約見 `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md`。 + +## 2. P0 canonical 判定 + +| 群組 | Evidence | 目前判定 | 風險 | 下一步 | +|------|----------|----------|------|--------| +| `ewoooc` / `momo-pro-system` | `wooo/ewoooc` public Gitea repo;`/Users/ogt/momo-pro-system` 與 iCloud `momo-pro-system` 都指向 Gitea;lineage probe 顯示兩份 working tree `no_shared_history` | 待人工判定,禁止自動視為複本 | 同名資料夾不同 HEAD,且近期 sample 無共同 commit;GitHub target 未確認 | 確認部署真相、完整 refs / ancestry 後再決定 GitHub target | +| `momo_pro_system` / GitLab 110 | `/Users/ogt/momo_pro_system` 指向 `root/momo-pro-system` GitLab 類 remote;lineage probe 顯示與 Gitea 兩份 working tree `no_shared_history` | 待人工判定,禁止自動合併 | 可能是同產品不同歷史、舊版重建、或完全不同 repo | 確認是否仍 active、不得自動合併 | +| `bitan-pharmacy` | 指向 110 SSH 類 remote;110 remote `main` 與本機 `main` 對齊;GitHub `owenhytsai/bitan-pharmacy` 未授權 probe 看不到 | 待建立 target | GitHub target 未確認 | 確認是否 active、建立或指定 GitHub target | +| `tsenyang-website` | 指向 110 SSH 類 remote;110 remote `main` 與本機 `main` 對齊;GitHub `owenhytsai/tsenyang-website` 未授權 probe 看不到 | 待建立 target | GitHub target 未確認 | 確認是否仍 active、建立或指定 GitHub target | +| `wooo-infra-config` | GitHub remote 存在且 `main` 與本機對齊;110 internal remote 因 SSH 權限不足或不可讀 | 待判定 110 remote 用途 | 可能是舊 remote、mirror、或權限未配置 | 確認 110 remote 用途;若為舊 remote,降級或移除 | + +## 2.1 GitHub target probe + +| GitHub repo | probe status | heads | 判定 | +|-------------|--------------|-------|------| +| `owenhytsai/awoooi` | `exists` | `2` | 可見但 refs blocked | +| `owenhytsai/clawbot-v5` | `exists` | `1` | 可見但 refs blocked | +| `owenhytsai/wooo-aiops` | `exists` | `3` | 可見但 refs blocked | +| `owenhytsai/wooo-infra-config` | `exists` | `1` | 可見,需判斷 110 internal remote | +| `owenhytsai/ewoooc` | `not_found_or_private` | `0` | 未建立或為 private 且未授權,不能當作已轉移 | +| `owenhytsai/bitan-pharmacy` | `not_found_or_private` | `0` | 未建立或為 private 且未授權,不能當作已轉移 | +| `owenhytsai/tsenyang-website` | `not_found_or_private` | `0` | 未建立或為 private 且未授權,不能當作已轉移 | +| `nexu-io/open-design` | `exists` | `186` | 外部/設計 repo,需 scope 判定 | + +## 2.1.1 GitHub target 決策 + +`docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` 顯示: + +| 類型 | Repos | 判定 | +|------|-------|------| +| refs blocked | `awoooi`、`clawbot-v5`、`wooo-aiops` | 不得切 primary,需 refs reconcile approval | +| create / grant access after approval | `ewoooc`、`bitan-pharmacy`、`tsenyang-website` | 不得自動建立或修改 visibility | +| internal remote purpose pending | `wooo-infra-config` | 需確認 110 internal remote 用途 | +| external scope | `nexu-io/open-design` | 先做 scope review | + +此決策只補足 target owner / visibility 的下一步,不取代 canonical 判定與 server-side refs diff。 + +## 2.2 Internal 110 refs probe + +`docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` 顯示: + +| Repo | Relation | Heads | Tags | 判定 | +|------|----------|-------|------|------| +| `bitan-pharmacy` | `aligned_current_branch` | `1` | `0` | 110 remote 可作為 source candidate,但 GitHub target 未確認 | +| `tsenyang-website` | `aligned_current_branch` | `1` | `0` | 110 remote 可作為 source candidate,但 GitHub target 未確認 | +| `wooo-infra-config` GitHub | `aligned_current_branch` | `1` | `0` | GitHub target 可讀且與本機 main 對齊 | +| `wooo-infra-config` 110 internal | `unreachable` | `0` | `0` | 110 internal remote 用途與權限待判定 | + +## 3. 本機 lineage evidence + +`docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` 顯示: + +| 比對 | Relation | Common commits | +|------|----------|----------------| +| `local-momo-gitea` vs `icloud-momo-gitea` | `no_shared_history` | `0` | +| `local-momo-gitea` vs `local-momo-gitlab` | `no_shared_history` | `0` | +| `icloud-momo-gitea` vs `local-momo-gitlab` | `no_shared_history` | `0` | + +此結果仍只代表本機 sample,未 fetch 遠端;但足以阻止自動把這三者當成同一個 repo 複本處理。 + +## 4. 本機 HEAD evidence + +| Working tree | Remote 類型 | 短 SHA | 判定 | +|--------------|-------------|--------|------| +| `/Users/ogt/momo-pro-system` | Gitea `wooo/ewoooc` | `61a9c4c` | 不可直接視為 iCloud 複本 | +| `/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system` | Gitea `wooo/ewoooc` | `fe6180b` | 與本機 momo-pro-system 不同 HEAD | +| `/Users/ogt/momo_pro_system` | GitLab 類 110 remote | `5a4294e` | 與 `wooo/ewoooc` 關係待判定 | +| `/Users/ogt/bitan-pharmacy` | 110 SSH 類 remote | `7423a08` | GitHub target 未確認 | +| `/Users/ogt/tsenyang-website` | 110 SSH 類 remote | `b103112` | GitHub target 未確認 | + +上述短 SHA 只用於人工辨識,不代表 server-side 最新狀態。 + +## 5. Credential hygiene + +本輪 read-only 檢查發現,部分本機 remote URL 仍含 embedded credential 或個人 token 形式。文件只記錄風險,不記錄任何值。 + +後續處理 gate: + +1. 先完成 repo inventory 與 canonical 判定。 +2. 以 credential helper、只讀 token 或 secret store 取代 embedded credential。 +3. 對既有 token 做 rotation。 +4. 在 AwoooP / AWOOOI audit 中只記錄「已輪替」與 evidence ref,不記錄 token value。 + +## 6. 下一步 + +1. 用只讀 token 或管理匯出取得 Gitea server 全量 repo list。 +2. 對 `wooo/ewoooc` 與 `root/momo-pro-system` 做 server-side refs diff。 +3. 確認三個 momo/ewoooc working tree 的產品關係與部署真相。 +4. 確認 `bitan-pharmacy`、`tsenyang-website` 是否仍 active。 +5. 依 GitHub target 決策表完成 repo / owner / visibility approval。 +6. 完成 canonical 判定前,不建立自動 mirror、不刪除任何 remote、不切 GitHub primary。 diff --git a/docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md b/docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md new file mode 100644 index 00000000..e81356b0 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md @@ -0,0 +1,34 @@ +# Source Control 遷移盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `blocked` | +| Gitea remote | `gitea` | +| GitHub remote | `origin` | +| Gitea repo | `wooo/clawbot-v5` | +| GitHub repo | `owenhytsai/clawbot-v5` | +| Gitea URL | `http://192.168.0.110:3001/wooo/clawbot-v5.git` | +| GitHub URL | `https://github.com/owenhytsai/clawbot-v5.git` | +| Gitea 分支數 | `1` | +| GitHub 分支數 | `1` | +| Gitea tags | `1` | +| GitHub tags | `0` | +| Gitea main | `22074fbe4d6ec6c11c86f76139eea55756d1d160` | +| GitHub main | `7a769de46450087f9d6a8ef0d2ac23ed15565d2c` | +| 阻塞原因 | branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 | + +## 分支差異 + +- 只在 Gitea:`0` +- 只在 GitHub:`0` +- SHA 不一致:`1` +- SHA 一致:`0` + +## Tag 差異 + +- 只在 Gitea:`1` +- 只在 GitHub:`0` +- SHA 不一致:`0` +- SHA 一致:`0` + +> 注意:本檔由 read-only inventory 工具產生,不包含 remote URL 內的帳密。 diff --git a/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md new file mode 100644 index 00000000..27082468 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md @@ -0,0 +1,150 @@ +# Source Control 遷移矩陣 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-12 | +| 狀態 | 第一版,依本機可見 remote inventory 建立 | +| 上游 snapshot | `docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md`、`docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md`、`docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md` | +| Gitea server-side inventory runbook | `docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md` | +| Gitea read-only inventory approval | `docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md` | +| GitHub target probe | `docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md` | +| GitHub target 決策 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | +| GitHub target repo approval | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | +| 原則 | 只做盤點與分類,不做同步、不刪除、不切 primary | + +## 0. 核心結論 + +GitHub primary 可以作為長期方向,但目前還不能切換。 + +原因不是只有 `awoooi` 的 Gitea/GitHub `main` SHA 不一致;本機可見 source-control 拓撲還顯示: + +- 有 Gitea-only 專案,例如 `wooo/ewoooc`。 +- 有 110 internal-only remote,例如 `bitan-pharmacy`、`tsenyang-website`。 +- 有 GitLab 類 110 remote,例如 `root/momo-pro-system`。 +- 有 GitHub + 110 internal 雙 remote,例如 `wooo-infra-config`。 +- 有 local-only Git working tree,尚未判定是否需要納入 GitHub。 + +因此遷移工作不能只處理 Gitea `wooo/awoooi`,必須先建立完整 source-control target matrix。 + +2026-05-12 追加確認:Gitea `users/wooo/repos` 未提供 token 時可見 `wooo/awoooi` 與 `wooo/ewoooc`,`gitea_repo_inventory_v1.status=partial`。這補強 server-side public evidence,但仍不能代表 private/internal 全量。 + +## 1. 優先級矩陣 + +| 優先級 | 類型 | Repo / working tree | 目前狀態 | 下一步 | +|--------|------|---------------------|----------|--------| +| P0 | Mapped but blocked | `wooo/awoooi` -> `owenhytsai/awoooi` | Gitea/GitHub `main` SHA、branches、tags 未對齊 | 確認部署真相來源,產生 full refs diff,禁止 primary switch | +| P0 | Gitea-only / canonical blocked | `wooo/ewoooc` | 本機兩份 `momo-pro-system` working tree 指向 Gitea,但 lineage probe 顯示 `no_shared_history`;`owenhytsai/ewoooc` 未授權 probe 看不到 | 決定 canonical repo 與 GitHub target 前,不得同步 | +| P0 | Internal 110-only / source aligned | `bitan-pharmacy` | 110 remote `main` 與本機 `main` 對齊,1 head / 0 tags;`owenhytsai/bitan-pharmacy` 未授權 probe 看不到 | 確認是否 active,建立或指定 GitHub target | +| P0 | Internal 110-only / source aligned | `tsenyang-website` | 110 remote `main` 與本機 `main` 對齊,1 head / 0 tags;`owenhytsai/tsenyang-website` 未授權 probe 看不到 | 確認是否仍 active,建立或指定 GitHub target | +| P0 | GitLab 110-only / canonical blocked | `root/momo-pro-system` | 指向 `192.168.0.110:8929` GitLab 類 remote;與兩份 Gitea working tree 近期 sample 無共同 commit | 與 `wooo/ewoooc` 做 server-side 判定,不得直接合併或刪除 | +| P1 | Mapped but blocked | `wooo/clawbot-v5` -> `owenhytsai/clawbot-v5` | Gitea/GitHub `main` SHA 不一致,GitHub 缺 Gitea tag | 確認哪一端是真相來源,再同步 refs | +| P1 | Mapped but blocked | `wooo/wooo-aiops` -> `owenhytsai/wooo-aiops` | Gitea/GitHub `main` SHA 不一致,GitHub tags 比 Gitea 多,GitHub 多 1 條 branch | 釐清雙端分歧來源與部署控制面 | +| P1 | GitHub aligned / internal 110 unreadable | `wooo-infra-config` | GitHub `main` 與本機 `main` 對齊;110 internal remote 因 SSH 權限不足或不可讀,mirror 方向未確認 | 確認 110 remote 用途與權限;若為舊 remote,降級或移除 | +| P2 | GitHub-only | `nexu-io/open-design` | GitHub-only,可能非 AWOOOI 主線 | 標記 owner 與 scope,決定是否納入資安網 | +| P2 | Local-only | `aider-watch`、`stockPlatform` | 本機 Git working tree 無 remote | 判定是否為草稿、封存或需建立 GitHub repo | +| P2 | Local-only / no clear remote | `momo_pro_system` | 另有 GitLab 類 remote,但名稱與 `momo-pro-system` 重疊 | 需人工釐清 canonical repo | + +## 2. 去重後 target group + +### Gitea candidates + +| Gitea repo | GitHub candidate | 目前狀態 | +|------------|------------------|----------| +| `wooo/awoooi` | `owenhytsai/awoooi` | 已 mapped,但 refs blocked | +| `wooo/clawbot-v5` | `owenhytsai/clawbot-v5` | 已 mapped,但 refs blocked | +| `wooo/ewoooc` | 待定 | Gitea-only,P0 | +| `wooo/wooo-aiops` | `owenhytsai/wooo-aiops` | 已 mapped,但 refs blocked | + +### Gitea server-side public inventory summary + +| Source | Status | Visibility | Repo count | Evidence | +|--------|--------|------------|------------|----------| +| `users/wooo/repos` | `partial` | `public_only` | `2` | `docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md` | +| `orgs/wooo/repos` | `blocked` | `public_only` | `0` | `docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md` | + +完整 private/internal list 仍需只讀 token 或管理匯出 JSON,且必須先通過 `docs/security/gitea-readonly-inventory-approval.snapshot.json`。 + +### 110 internal candidates + +| Internal repo | GitHub candidate | 目前狀態 | +|---------------|------------------|----------| +| `bitan-pharmacy` | 待定 | 110 SSH 類 remote | +| `root/momo-pro-system` | 待定 | 110 GitLab 類 remote | +| `tsenyang-website` | 待定 | 110 SSH 類 remote | +| `wooo/wooo-infra-config` | `owenhytsai/wooo-infra-config` | GitHub aligned;110 internal remote unreadable,需判斷用途 | + +### GitHub-only candidates + +| GitHub repo | 判定 | +|-------------|------| +| `nexu-io/open-design` | 可能是外部設計 repo,需 owner/scope 判定 | +| `owenhytsai/wooo-infra-config` | 已在 GitHub,但需處理 110 internal remote | + +### Canonical lineage probe summary + +| 群組 | Status | 結論 | Evidence | +|------|--------|------|----------| +| `ewoooc-momo-pro-system` | `unrelated` | 三個本機 working tree 近期 sample 內無共同 commit;不能自動視為複本或同 repo 分支 | `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | + +### Internal 110 refs probe summary + +| Repo | Status | Heads | Tags | Local HEAD | Remote main | Evidence | +|------|--------|-------|------|------------|-------------|----------| +| `bitan-pharmacy` | `aligned_current_branch` | `1` | `0` | `7423a08` | `7423a08` | `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | +| `tsenyang-website` | `aligned_current_branch` | `1` | `0` | `b103112` | `b103112` | `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | +| `wooo-infra-config` GitHub | `aligned_current_branch` | `1` | `0` | `866b5aa` | `866b5aa` | `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | +| `wooo-infra-config` 110 internal | `unreachable` | `0` | `0` | `866b5aa` | 無 | `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | + +### GitHub target probe summary + +| 類型 | 數量 | 說明 | +|------|------|------| +| 候選 GitHub repo | `8` | 以本機 remote 與候選 target 建立 | +| 可 read-only probe | `5` | `awoooi`、`clawbot-v5`、`wooo-aiops`、`wooo-infra-config`、`nexu-io/open-design` | +| not found or private | `3` | `ewoooc`、`bitan-pharmacy`、`tsenyang-website` | + +### GitHub target decision summary + +| 狀態 | 數量 | 說明 | +|------|------|------| +| target candidates | `8` | 以 read-only probe 與本機 remote inventory 彙整 | +| approval required | `7` | 除 `nexu-io/open-design` scope review 外,其餘皆需人工 gate | +| create / grant access after approval | `3` | `ewoooc`、`bitan-pharmacy`、`tsenyang-website` | +| refs reconcile blocked | `3` | `awoooi`、`clawbot-v5`、`wooo-aiops` | +| internal remote purpose pending | `1` | `wooo-infra-config` | + +此決策表仍是 draft。它只把 target visibility / repo creation 變成 approval evidence,不授權任何自動 repo 建立、visibility 修改、refs sync 或 GitHub primary 切換。 + +Repo-by-repo approval package 已建立,7 個 approval-required targets 皆為 `pending`。Approval scope 採低摩擦原則:只處理高風險執行邊界,不阻擋 read-only inventory、evidence mirror 與草案規劃。 + +## 3. 必要驗收 gate + +任何 repo 進入「已可切 GitHub primary」之前,都必須通過: + +1. Server-side repo inventory 已完成,不能只靠本機 working tree。 +2. Branch count 與 branch SHA diff 已完成。 +3. Tag count 與 tag SHA diff 已完成。 +4. Release / artifact / deploy marker inventory 已完成。 +5. Workflow / webhook / runner / secret 名稱 inventory 已完成。 +6. GitHub target repo、owner、branch protection、CODEOWNERS 已確認。 +7. Secrets 只盤名稱,不搬 value。 +8. AwoooP 只收到 evidence,不直接觸發 sync、merge、deploy 或 primary switch。 + +## 4. 已產生 refs diff 的 mapped repos + +| Repo | Status | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main | Evidence | +|------|--------|----------------|-----------------|------------|-------------|------------|-------------|----------| +| `wooo/awoooi` -> `owenhytsai/awoooi` | `blocked` | `115` | `2` | `2` | `0` | `a18e2f9c3f403050d0fb7476bf6fdb860225731a` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | +| `wooo/clawbot-v5` -> `owenhytsai/clawbot-v5` | `blocked` | `1` | `1` | `1` | `0` | `22074fbe4d6ec6c11c86f76139eea55756d1d160` | `7a769de46450087f9d6a8ef0d2ac23ed15565d2c` | `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` | +| `wooo/wooo-aiops` -> `owenhytsai/wooo-aiops` | `blocked` | `2` | `3` | `0` | `19` | `507384a2e1943f4183942bf17d7b52e223067853` | `7c7aa109d93da6d75d687d6ee5131151afee37e8` | `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` | + +這三個 mapped repos 都不能直接視為 GitHub primary ready。 + +## 5. 下一波建議 + +1. 先批准 Gitea read-only inventory package,再用只讀 token 或管理匯出補齊 Gitea server repo list。 +2. 依 GitHub target repo-by-repo approval package 處理 7 個 approval-required target。 +3. 釐清 `wooo/awoooi`、`wooo/clawbot-v5`、`wooo/wooo-aiops` 的雙端分歧來源。 +4. 釐清 `wooo/ewoooc`、`root/momo-pro-system`、`momo-pro-system`、`momo_pro_system` 的 canonical 關係。 +5. 釐清 `bitan-pharmacy`、`tsenyang-website` 是否仍 active,並決定 GitHub owner / visibility。 +6. 產出 GitHub primary ADR 前,不做主控切換。 diff --git a/docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md b/docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md new file mode 100644 index 00000000..9385da07 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md @@ -0,0 +1,34 @@ +# Source Control 遷移盤點快照 + +| 項目 | 值 | +|------|----| +| 狀態 | `blocked` | +| Gitea remote | `gitea` | +| GitHub remote | `origin` | +| Gitea repo | `wooo/wooo-aiops` | +| GitHub repo | `owenhytsai/wooo-aiops` | +| Gitea URL | `http://192.168.0.110:3001/wooo/wooo-aiops.git` | +| GitHub URL | `https://github.com/owenhytsai/wooo-aiops.git` | +| Gitea 分支數 | `2` | +| GitHub 分支數 | `3` | +| Gitea tags | `0` | +| GitHub tags | `19` | +| Gitea main | `507384a2e1943f4183942bf17d7b52e223067853` | +| GitHub main | `7c7aa109d93da6d75d687d6ee5131151afee37e8` | +| 阻塞原因 | branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 | + +## 分支差異 + +- 只在 Gitea:`0` +- 只在 GitHub:`1` +- SHA 不一致:`1` +- SHA 一致:`1` + +## Tag 差異 + +- 只在 Gitea:`0` +- 只在 GitHub:`19` +- SHA 不一致:`0` +- SHA 一致:`0` + +> 注意:本檔由 read-only inventory 工具產生,不包含 remote URL 內的帳密。 diff --git a/docs/security/git-remote-refs-bitan-tsenyang.snapshot.json b/docs/security/git-remote-refs-bitan-tsenyang.snapshot.json new file mode 100644 index 00000000..bc7596da --- /dev/null +++ b/docs/security/git-remote-refs-bitan-tsenyang.snapshot.json @@ -0,0 +1,50 @@ +{ + "schema_version": "git_remote_refs_probe_v1", + "group_name": "internal-110-bitan-tsenyang", + "status": "ok", + "repo_count": 2, + "aligned_current_branch_count": 2, + "unreachable_count": 0, + "repos": [ + { + "label": "bitan-pharmacy", + "repo_path": "/Users/ogt/bitan-pharmacy", + "remote": "origin", + "remote_url_redacted": "192.168.0.110:bitan-pharmacy.git", + "status": "aligned_current_branch", + "local_branch": "main", + "local_head": "7423a080fff0fb9fe7698857f47475725ed9ec7a", + "remote_current_branch_sha": "7423a080fff0fb9fe7698857f47475725ed9ec7a", + "head_count": 1, + "tag_count": 0, + "heads": [ + { + "name": "main", + "sha": "7423a080fff0fb9fe7698857f47475725ed9ec7a" + } + ], + "tags": [], + "error_summary": "" + }, + { + "label": "tsenyang-website", + "repo_path": "/Users/ogt/tsenyang-website", + "remote": "origin", + "remote_url_redacted": "192.168.0.110:tsenyang-website.git", + "status": "aligned_current_branch", + "local_branch": "main", + "local_head": "b1031127af71c56673decebe258d2437e98a7ae9", + "remote_current_branch_sha": "b1031127af71c56673decebe258d2437e98a7ae9", + "head_count": 1, + "tag_count": 0, + "heads": [ + { + "name": "main", + "sha": "b1031127af71c56673decebe258d2437e98a7ae9" + } + ], + "tags": [], + "error_summary": "" + } + ] +} diff --git a/docs/security/git-remote-refs-wooo-infra-config.snapshot.json b/docs/security/git-remote-refs-wooo-infra-config.snapshot.json new file mode 100644 index 00000000..dc35d152 --- /dev/null +++ b/docs/security/git-remote-refs-wooo-infra-config.snapshot.json @@ -0,0 +1,45 @@ +{ + "schema_version": "git_remote_refs_probe_v1", + "group_name": "wooo-infra-config-remotes", + "status": "partial", + "repo_count": 2, + "aligned_current_branch_count": 1, + "unreachable_count": 1, + "repos": [ + { + "label": "wooo-infra-config-gitea", + "repo_path": "/Users/ogt/wooo-infra-config", + "remote": "gitea", + "remote_url_redacted": "192.168.0.110:wooo/wooo-infra-config.git", + "status": "unreachable", + "local_branch": "main", + "local_head": "866b5aa1a3ff8df7e949614fba58f95e32254afa", + "remote_current_branch_sha": "", + "head_count": 0, + "tag_count": 0, + "heads": [], + "tags": [], + "error_summary": "SSH 權限不足或 remote 不可讀" + }, + { + "label": "wooo-infra-config-github", + "repo_path": "/Users/ogt/wooo-infra-config", + "remote": "origin", + "remote_url_redacted": "github.com:owenhytsai/wooo-infra-config.git", + "status": "aligned_current_branch", + "local_branch": "main", + "local_head": "866b5aa1a3ff8df7e949614fba58f95e32254afa", + "remote_current_branch_sha": "866b5aa1a3ff8df7e949614fba58f95e32254afa", + "head_count": 1, + "tag_count": 0, + "heads": [ + { + "name": "main", + "sha": "866b5aa1a3ff8df7e949614fba58f95e32254afa" + } + ], + "tags": [], + "error_summary": "" + } + ] +} diff --git a/docs/security/gitea-github-awoooi-inventory.snapshot.json b/docs/security/gitea-github-awoooi-inventory.snapshot.json new file mode 100644 index 00000000..2186fdce --- /dev/null +++ b/docs/security/gitea-github-awoooi-inventory.snapshot.json @@ -0,0 +1,19 @@ +{ + "schema_version": "source_control_migration_event_v1", + "gitea_repo": "wooo/awoooi", + "github_repo": "owenhytsai/awoooi", + "branch_count_gitea": 117, + "branch_count_github": 2, + "tag_count_gitea": 2, + "tag_count_github": 0, + "latest_sha_gitea": "0bc187877884f6fa6fe87a03dab99e9c6622fd42", + "latest_sha_github": "202071f7a8724d5e8c29de441c3f380575a0ea94", + "workflows_mapped": false, + "webhooks_mapped": false, + "secrets_inventory_only": true, + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致", + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md" + ] +} diff --git a/docs/security/gitea-org-repo-inventory-blocked.snapshot.json b/docs/security/gitea-org-repo-inventory-blocked.snapshot.json new file mode 100644 index 00000000..0070350c --- /dev/null +++ b/docs/security/gitea-org-repo-inventory-blocked.snapshot.json @@ -0,0 +1,15 @@ +{ + "schema_version": "gitea_repo_inventory_v1", + "base_url": "http://192.168.0.110:3001", + "org": "wooo", + "github_owner": "owenhytsai", + "query_mode": "org", + "query": "", + "visibility_scope": "public_only", + "token_present": false, + "http_status": 404, + "status": "blocked", + "blocking_reason": "Gitea API 查無 org/user repos,需確認 org 名稱或使用管理匯出", + "repo_count": 0, + "repos": [] +} diff --git a/docs/security/gitea-public-repo-search.snapshot.json b/docs/security/gitea-public-repo-search.snapshot.json new file mode 100644 index 00000000..b2aa616a --- /dev/null +++ b/docs/security/gitea-public-repo-search.snapshot.json @@ -0,0 +1,40 @@ +{ + "schema_version": "gitea_repo_inventory_v1", + "base_url": "http://192.168.0.110:3001", + "org": "public-search", + "github_owner": "owenhytsai", + "query_mode": "search", + "query": "", + "visibility_scope": "public_only", + "token_present": false, + "http_status": 200, + "status": "partial", + "blocking_reason": "未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出", + "repo_count": 2, + "repos": [ + { + "gitea_repo": "wooo/awoooi", + "name": "awoooi", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/awoooi.git", + "github_repo_candidate": "owenhytsai/awoooi" + }, + { + "gitea_repo": "wooo/ewoooc", + "name": "ewoooc", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/ewoooc.git", + "github_repo_candidate": "owenhytsai/ewoooc" + } + ] +} diff --git a/docs/security/gitea-readonly-inventory-approval.snapshot.json b/docs/security/gitea-readonly-inventory-approval.snapshot.json new file mode 100644 index 00000000..0910c7de --- /dev/null +++ b/docs/security/gitea-readonly-inventory-approval.snapshot.json @@ -0,0 +1,21 @@ +{ + "schema_version": "approval_required_event_v1", + "source_event_type": "gitea_repo_inventory_v1", + "source_event_id": "gitea-private-internal-server-side-inventory-2026-05-12", + "risk": "MEDIUM", + "requested_action": "run_gitea_readonly_inventory", + "reason": "目前 Gitea repo inventory 只有 public_only partial evidence,尚未涵蓋 private/internal repos;需人工批准後才能使用 read-only token 或管理匯出補齊 server-side 全量清單。", + "required_reviewers": [ + "migration-engineer", + "security-commander", + "human-owner" + ], + "blocked_until_approved": true, + "evidence_refs": [ + "docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md", + "docs/security/GITEA-ORG-REPO-INVENTORY-BLOCKED-SNAPSHOT.md", + "docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md", + "docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md" + ], + "expires_at": "2026-05-19T23:59:59+08:00" +} diff --git a/docs/security/gitea-repo-inventory.snapshot.json b/docs/security/gitea-repo-inventory.snapshot.json new file mode 100644 index 00000000..acab1415 --- /dev/null +++ b/docs/security/gitea-repo-inventory.snapshot.json @@ -0,0 +1,40 @@ +{ + "schema_version": "gitea_repo_inventory_v1", + "base_url": "http://192.168.0.110:3001", + "org": "wooo", + "github_owner": "owenhytsai", + "query_mode": "user", + "query": "", + "visibility_scope": "public_only", + "token_present": false, + "http_status": 200, + "status": "partial", + "blocking_reason": "未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出", + "repo_count": 2, + "repos": [ + { + "gitea_repo": "wooo/awoooi", + "name": "awoooi", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/awoooi.git", + "github_repo_candidate": "owenhytsai/awoooi" + }, + { + "gitea_repo": "wooo/ewoooc", + "name": "ewoooc", + "owner": "wooo", + "private": false, + "empty": false, + "archived": false, + "default_branch": "main", + "clone_url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git", + "ssh_url_redacted": "ssh://localhost:2222/wooo/ewoooc.git", + "github_repo_candidate": "owenhytsai/ewoooc" + } + ] +} diff --git a/docs/security/github-target-decision.snapshot.json b/docs/security/github-target-decision.snapshot.json new file mode 100644 index 00000000..2f518fef --- /dev/null +++ b/docs/security/github-target-decision.snapshot.json @@ -0,0 +1,154 @@ +{ + "schema_version": "github_target_decision_v1", + "status": "draft", + "decision_count": 8, + "approval_required_count": 7, + "decisions": [ + { + "github_repo": "owenhytsai/awoooi", + "source_key": "wooo/awoooi", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "recommended_action": "hold_refs_reconcile", + "risk": "HIGH", + "approval_required": true, + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "branches/tags/workflows/webhooks/secrets 名稱 inventory 完成", + "GitHub primary ADR 完成" + ], + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "GitHub repo 可讀,但 refs blocked,不可切 primary。" + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "source_key": "wooo/clawbot-v5", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "recommended_action": "hold_refs_reconcile", + "risk": "MEDIUM", + "approval_required": true, + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub 缺 Gitea tag 的處理方式已決定" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "GitHub repo 可讀,但 main SHA 與 tag 不一致。" + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "source_key": "wooo/wooo-aiops", + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "recommended_action": "hold_refs_reconcile", + "risk": "MEDIUM", + "approval_required": true, + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub-only branch 與 tags 的來源已釐清" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "GitHub repo 可讀,但 GitHub tags 比 Gitea 多,需釐清真相來源。" + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "source_key": "wooo/wooo-infra-config", + "probe_status": "exists", + "target_state": "exists_aligned", + "recommended_action": "confirm_internal_remote_purpose", + "risk": "MEDIUM", + "approval_required": true, + "blocked_until": [ + "110 internal remote 用途已確認", + "若 110 remote 為舊主控,已降級或移除", + "infra secrets 名稱 inventory 完成" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "GitHub 與本機 main 對齊;110 internal remote 不可讀,需判斷用途。" + }, + { + "github_repo": "owenhytsai/ewoooc", + "source_key": "wooo/ewoooc / root/momo-pro-system / momo working trees", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "recommended_action": "create_or_grant_access_after_approval", + "risk": "HIGH", + "approval_required": true, + "blocked_until": [ + "ewoooc/momo-pro-system canonical 關係人工確認", + "server-side refs diff 完成", + "GitHub repo visibility 與 owner 決策完成" + ], + "evidence_refs": [ + "docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md", + "docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "GitHub target 未授權 probe 看不到,且 momo/ewoooc lineage unrelated,不可自動建立 mirror。" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "source_key": "bitan-pharmacy", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "recommended_action": "create_or_grant_access_after_approval", + "risk": "MEDIUM", + "approval_required": true, + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo visibility 與 owner 決策完成" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "110 remote 與本機 main 對齊,可作 source candidate;GitHub target 未確認。" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "source_key": "tsenyang-website", + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "recommended_action": "create_or_grant_access_after_approval", + "risk": "MEDIUM", + "approval_required": true, + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo visibility 與 owner 決策完成" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "110 remote 與本機 main 對齊,可作 source candidate;GitHub target 未確認。" + }, + { + "github_repo": "nexu-io/open-design", + "source_key": "open-design", + "probe_status": "exists", + "target_state": "external_scope", + "recommended_action": "scope_review_only", + "risk": "LOW", + "approval_required": false, + "blocked_until": [ + "確認是否屬於 AWOOOI 資安網範圍" + ], + "evidence_refs": [ + "docs/security/github-target-probe.snapshot.json" + ], + "notes": "外部/設計 repo,先做 scope review,不納入主控切換。" + } + ] +} diff --git a/docs/security/github-target-probe.snapshot.json b/docs/security/github-target-probe.snapshot.json new file mode 100644 index 00000000..3b1fb812 --- /dev/null +++ b/docs/security/github-target-probe.snapshot.json @@ -0,0 +1,850 @@ +{ + "schema_version": "github_target_probe_v1", + "status": "ok", + "candidate_count": 8, + "exists_count": 5, + "not_found_or_private_count": 3, + "candidates": [ + { + "github_repo": "owenhytsai/awoooi", + "url_redacted": "https://github.com/owenhytsai/awoooi.git", + "status": "exists", + "head_count": 2, + "heads": [ + { + "name": "main", + "sha": "202071f7a8724d5e8c29de441c3f380575a0ea94" + }, + { + "name": "release/v1.0", + "sha": "d15fb7d9f4bac86873d5c16b9c17c527b8f38bef" + } + ], + "error_summary": "" + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "url_redacted": "https://github.com/owenhytsai/clawbot-v5.git", + "status": "exists", + "head_count": 1, + "heads": [ + { + "name": "main", + "sha": "7a769de46450087f9d6a8ef0d2ac23ed15565d2c" + } + ], + "error_summary": "" + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "url_redacted": "https://github.com/owenhytsai/wooo-aiops.git", + "status": "exists", + "head_count": 3, + "heads": [ + { + "name": "main", + "sha": "7c7aa109d93da6d75d687d6ee5131151afee37e8" + }, + { + "name": "refactor/phase-9.3", + "sha": "7261f94b86346fb706eca729c5db844e65bd902c" + }, + { + "name": "uat", + "sha": "1ec245fd4d90ed6db479b3fb0487545dfd632411" + } + ], + "error_summary": "" + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "url_redacted": "https://github.com/owenhytsai/wooo-infra-config.git", + "status": "exists", + "head_count": 1, + "heads": [ + { + "name": "main", + "sha": "866b5aa1a3ff8df7e949614fba58f95e32254afa" + } + ], + "error_summary": "" + }, + { + "github_repo": "owenhytsai/ewoooc", + "url_redacted": "https://github.com/owenhytsai/ewoooc.git", + "status": "not_found_or_private", + "head_count": 0, + "heads": [], + "error_summary": "GitHub 回應 repository not found;可能未建立或為 private 且未授權" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "url_redacted": "https://github.com/owenhytsai/bitan-pharmacy.git", + "status": "not_found_or_private", + "head_count": 0, + "heads": [], + "error_summary": "GitHub 回應 repository not found;可能未建立或為 private 且未授權" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "url_redacted": "https://github.com/owenhytsai/tsenyang-website.git", + "status": "not_found_or_private", + "head_count": 0, + "heads": [], + "error_summary": "GitHub 回應 repository not found;可能未建立或為 private 且未授權" + }, + { + "github_repo": "nexu-io/open-design", + "url_redacted": "https://github.com/nexu-io/open-design.git", + "status": "exists", + "head_count": 186, + "heads": [ + { + "name": "add-twitter-badge", + "sha": "883bb50660feb04de19279a1f74c9faae3f6924b" + }, + { + "name": "agents-ts", + "sha": "12fcd256fa2d29e84f27bfc76d8c5639d8b676c8" + }, + { + "name": "assets/maintainer-posters", + "sha": "73f77838b2a08050dbb6302bc7544dc5153963ea" + }, + { + "name": "astonishing-reindeer", + "sha": "4287d6e373bed3f96e2d4929a59b10e834e6d3a9" + }, + { + "name": "automation/refresh-contributors-wall", + "sha": "902ea006ee84ba3ac07ccecc5dfe9f0530eb241c" + }, + { + "name": "beta/langfuse-report-hook-smoke", + "sha": "fbf91220cc194a6e14f4690d5d3daf7383280fb9" + }, + { + "name": "bot-author", + "sha": "3f5fba963b0513842b0b83125966e9dd501147ac" + }, + { + "name": "bot-cards", + "sha": "7f695bfec57e8388a010a604bfc31c9661eb28a1" + }, + { + "name": "bot/fix-issue-646-acp-timeout", + "sha": "3830fbc9a1a97fa56e1fa45332a7de98c38fcfc2" + }, + { + "name": "brassy-hip", + "sha": "46aeadb9ef5dad3ee7400ec893b61778e9cd0fb3" + }, + { + "name": "chore/add-flowai-live-dashboard-template", + "sha": "81538ea99297718327d0e0f56972e93bc717542b" + }, + { + "name": "chore/codex-exec-path-fix", + "sha": "103c4ab6885686864139d9f18afe004d844ac4dc" + }, + { + "name": "chore/discord-issue-forum-webhook", + "sha": "9c64ef1b2bb2cffafb0ad6167d6a3831bdc0ba11" + }, + { + "name": "chore/docs-environment-refresh", + "sha": "ef86ec99e68970f2c65348341e575a807d392919" + }, + { + "name": "chore/e2e-smoke-hardening", + "sha": "96f667a9520aba323fda53dfc3a60e43177417d0" + }, + { + "name": "chore/e2e-test-gating", + "sha": "da890dc2d3c344712841c43b903a0cdac4e2017c" + }, + { + "name": "chore/entry-test-coverage", + "sha": "affec6f13e14d4130f933dbdde1303193edb95bd" + }, + { + "name": "chore/extended-playwright-stabilization", + "sha": "e0fc33a2b1e41644ff961df4e7502868ff4395a2" + }, + { + "name": "chore/mac-signing-perf-audit", + "sha": "81e4208946756b702bbf7a9036077680859c2d9f" + }, + { + "name": "chore/main-directory-align-refresh", + "sha": "b1f7b1082ad31ace41bfb147009a19140924879e" + }, + { + "name": "chore/nightly-ui-e2e-2026-05-10", + "sha": "d11134e22ae7f64760e5cbf0df1bf277dd06c917" + }, + { + "name": "chore/nightly-ui-e2e-2026-05-10-pr", + "sha": "26313d61da52bf37d19a39334396c0c1444c5f0a" + }, + { + "name": "chore/pr-fixes-link-discipline", + "sha": "3601e050e4be6c75e6a58847477c894d99479212" + }, + { + "name": "chore/prebundle-artifact-size-research", + "sha": "56855543169bddcc5a4dc964169594ecf6a9bebe" + }, + { + "name": "chore/release-beta-report-mac-signing", + "sha": "20e77f039b451012b37a72d4bddf974cc5ef58b2" + }, + { + "name": "chore/release-beta-spec-reports", + "sha": "324b0c52a86ae61f739690dcfcc6dd6a54201c07" + }, + { + "name": "chore/release-beta-windows-signing", + "sha": "ba5971219385e1035655de619753509fbbe6328c" + }, + { + "name": "chore/release-stable-nightly-gate", + "sha": "5813f3e5a3b8548fbc2ce3d94974f721d8880034" + }, + { + "name": "chore/tools-dev-usability-temp", + "sha": "3d384bbb87470c2f8f94727e7dfc31c9d2a09f9b" + }, + { + "name": "chore/ui-e2e-automation-suite", + "sha": "661ac35dfd0402b38a569e443a0f785d2c37307d" + }, + { + "name": "chore/windows-package-optimization", + "sha": "5a75290b4d6acd0b53604f1075e1e17c040e91d7" + }, + { + "name": "chore/windows-prebundle-optimization", + "sha": "4ce852743ffbac7ce7ae5ba7d64490c0220f974f" + }, + { + "name": "chore/zh-cn-readme-trim-byok-fallback", + "sha": "73c7c1cd204a03c2cefbc7498449d57cf124a4f4" + }, + { + "name": "codex-wrapper", + "sha": "fc556baf9303163b6344bb5c2221e2fccc168750" + }, + { + "name": "codex/agent-browser-cdp-startup", + "sha": "0565111ff27dd3f87158ae0f9b5ca8e7e8527113" + }, + { + "name": "codex/desktop-auto-update", + "sha": "6add7eea031a3d8251e2c26a7f97e40a6789c840" + }, + { + "name": "codex/disable-codex-plugins-env", + "sha": "449e48bda38112043a8714fb6c91c4dfd6814704" + }, + { + "name": "codex/draw", + "sha": "ec39701602c341348297cb5b8d896977a98c3426" + }, + { + "name": "codex/draw-worktree", + "sha": "fb8d6ef0b61fb6941f5698a1b6e2f73ac15a2190" + }, + { + "name": "codex/fix-assistant-agent-label", + "sha": "acd0c988de371065cd0bd943683cd4590e06d098" + }, + { + "name": "codex/fix-composio-oauth-launch-cancel", + "sha": "16ded885e7ad7509897565b75fd9541cde53a854" + }, + { + "name": "codex/fix-opencode-todowrite-footer", + "sha": "ec58ff7a465334227e2f7d44633fa3ae5d66ba4b" + }, + { + "name": "codex/fix-uk-duplicate-prompt-template-keys", + "sha": "1ce34f9e9a3bcb1e608c8c37e1cc4f43733ef98d" + }, + { + "name": "codex/fix-uk-prompt-template-keys", + "sha": "3eff4013a6a8d8de2502e044cf19a1af3e5cf93e" + }, + { + "name": "codex/issue-1288-chat-pane-feedback", + "sha": "eb6dc749fefb2fd1db3abe29ae2623ad5b26eedf" + }, + { + "name": "codex/issue-564-claude-diagnostics", + "sha": "ad7353d43273576e973b1dcbae55d363e3871348" + }, + { + "name": "codex/manual-edit-mode", + "sha": "db7af9e974ebf48bc1b6a364c70735b5ab8d243a" + }, + { + "name": "codex/readme-comment-partial", + "sha": "472feb2f7ca6865f8b3f8f30f47f007e1460ffe9" + }, + { + "name": "codex/soft-empty-api-response", + "sha": "82cdd50ece4418ce45fd9c2db3c39841c5a024f6" + }, + { + "name": "codex/sse-keepalive-nginx-docs", + "sha": "88465d744c1063866248d8cead71b708c7637655" + }, + { + "name": "common-sphere", + "sha": "31c0bd159fb92c4cf40f7745a410c491d43ffb36" + }, + { + "name": "cursor/289994c1", + "sha": "9185f856150b94aeebccd852dc0fa38f7e864f19" + }, + { + "name": "cursor/376f147b", + "sha": "5c5eb1eccee0ca2dc4e1bb94fc12bbdc53e8f5de" + }, + { + "name": "cursor/47ca13ab", + "sha": "c2f83ec124ae7b6453db1af5b9d6c5becb876787" + }, + { + "name": "cursor/e740089e", + "sha": "89d92d8ae07f748c1719f64773fb034ad3b69ebe" + }, + { + "name": "deeply-confidence", + "sha": "65f5f90fb59744ae2be359dce818f2e37dbeb9f6" + }, + { + "name": "demo/launchpad-ia", + "sha": "c28f9d642f6232a7e7af34f22f8d68eae41af432" + }, + { + "name": "dev-0508", + "sha": "1190e4bd966e3b1303ee3a53d3bd5d14d27fcb00" + }, + { + "name": "docs/arabic-readme", + "sha": "5f3293d760fb9664b0cf25fc386957599c5edb46" + }, + { + "name": "docs/desktop-shipped", + "sha": "c9884739a7db02f204cb8a6e23b166e3e92397fd" + }, + { + "name": "docs/social-preview-assets", + "sha": "50d1fd10f8fa327faa18c561224956985a1fbc23" + }, + { + "name": "explore/daemon-token-channel-injection", + "sha": "ca598a7f640da965d0cbe46f92bcbc24105f5210" + }, + { + "name": "feat/anime-fighting-game-screenshot", + "sha": "e4b76077be9edfbb548c314b40751a0ce2e64327" + }, + { + "name": "feat/deployment-watermark", + "sha": "21ae3bacc4718a18ff791ac462f559978fc07852" + }, + { + "name": "feat/designs-tab-cards", + "sha": "ceeb210e4687112c2d8e0c86814569104dc285f9" + }, + { + "name": "feat/desktop-integration-refresh", + "sha": "455330f5c72fca0b5060bf9227c8f33f02074602" + }, + { + "name": "feat/desktop-integraton", + "sha": "8d7645a1d3ffb5d85130f73fa9f60ef2a4b27d9f" + }, + { + "name": "feat/diagnostics-export", + "sha": "8aa0b19dd80cb80badc5b774da3973a1eb32675c" + }, + { + "name": "feat/discord-community-link", + "sha": "8e9f2b0d96adcf44666ee8d8eb41d14ec22c1656" + }, + { + "name": "feat/document-upload", + "sha": "b859c7a777e2751ab75fa85c5e05c0c8af9ae0a4" + }, + { + "name": "feat/ds-gallery-template-wiring", + "sha": "fbb22f5f3ed276fdce5f1ad65572322d1d1ee9ca" + }, + { + "name": "feat/github-dashboard-skill", + "sha": "19a6814e1be3f1394fd76739d453bf034b903c30" + }, + { + "name": "feat/grok-imagine", + "sha": "02bf3a7c245ec1b357080ae2c02a84e29568f2d0" + }, + { + "name": "feat/hermes", + "sha": "aa98f24cffba1ff35adbd2b713f06071d954047c" + }, + { + "name": "feat/import-claude-design-zip", + "sha": "cfab3745f9e1e0c73b34ad4ab657baf79375ced1" + }, + { + "name": "feat/in-context-comments", + "sha": "b2a1e1e5063f42ff040e685b5bc75e7ad7beb975" + }, + { + "name": "feat/langfuse-telemetry", + "sha": "bd5c1449e08b5e757762cc0f8993f4b346365f13" + }, + { + "name": "feat/langfuse-telemetry-relay", + "sha": "7f9a0d2663451a3c6873a89019d6468a35422a41" + }, + { + "name": "feat/multi-model-endpoints", + "sha": "a930178fd882fc98d0114f04f25ba605493a4330" + }, + { + "name": "feat/new-project-media-redesign", + "sha": "37d7bc74e54c16839a6248928d570c282c5db188" + }, + { + "name": "feat/optimize-naming", + "sha": "19b5272f3827958dbb83fe816c144f79297a0f74" + }, + { + "name": "feat/otaku-dance-choreography-breakdown", + "sha": "2283d1365378c6fb76cbe5cdc305d5edecc95675" + }, + { + "name": "feat/social-media-dashboard-skill", + "sha": "64e2e95daa9701607f154a2f602a1dc0814b506f" + }, + { + "name": "feat/support-star-us", + "sha": "5a63d09f2fecde8a6375538f0a605f6552e2537c" + }, + { + "name": "feat/three-kingdoms-screenshots", + "sha": "14f1b8ffd6e5a37305d58695ce2c1dbcb096f32d" + }, + { + "name": "feat/three-kingdoms-seedance-videos", + "sha": "87c65e3f3622cd2b38a4958c24a72c70d2ce1188" + }, + { + "name": "feat/tools-pr", + "sha": "8dae916ba12745a51bbaf64a5010fa46d3598090" + }, + { + "name": "feat/topbar-redesign", + "sha": "7d8944860ffc8647ff22af84d45fc4145f91ac70" + }, + { + "name": "feat/tweaks-palette-popover", + "sha": "8c9258651e3f464a71a94e2b112947773232577f" + }, + { + "name": "feat/ui-chore", + "sha": "d077371fc30664d324e7e78caa24d3c030cecc76" + }, + { + "name": "feat/vaunt-contributor-recognition", + "sha": "1ee568547e467f5b03516b2c0fae73d581e96c93" + }, + { + "name": "feat/vercel-multifile-deploy", + "sha": "372fb416a07da56c2b7b3a0ced2771728c09fc38" + }, + { + "name": "feat/windows-beta-packaging", + "sha": "807860f55881bde2d98068d6ee2273c457c616d6" + }, + { + "name": "feature/live-artifacts-spec", + "sha": "10e255228e9452abc35828298818d8736181210c" + }, + { + "name": "fix-138", + "sha": "02eb1580a3f0a0b3a4afedf82a5f5cf943450edf" + }, + { + "name": "fix-145", + "sha": "7b24fbefd1f0ade0e7dc5b919d6bc0b880112ff1" + }, + { + "name": "fix-coauthor", + "sha": "cb68699871c3ab284262a0455d92b2be350fa04e" + }, + { + "name": "fix-contributor-wall", + "sha": "72d7a2e308b101940026e80aee616e89bbdfcee1" + }, + { + "name": "fix-image-timeout", + "sha": "d5ee6b35875f450f11c0d5cccd2229d7a0afbd08" + }, + { + "name": "fix-main-new-project-panel-default", + "sha": "7f6afc4b9abaf3f41a12d31a7710d510f9615db8" + }, + { + "name": "fix-translations-drift", + "sha": "8d0d8ca2f7995e775cb3d1f587f2922e5f0aa55f" + }, + { + "name": "fix/52-claude-stdin-windows-enametoolong", + "sha": "67e33c12b6f434044b8ba288deb2bd798598d9bb" + }, + { + "name": "fix/body-hydration-warning", + "sha": "afce416f7123b19b79d1bec3581fffaeb67541a0" + }, + { + "name": "fix/codex-cli-path-fallback-ux", + "sha": "255770035d1bf94b971c4deea8d756f0485c16b5" + }, + { + "name": "fix/conversation-run-duration", + "sha": "59fb6323c135329e24e1d0f259c8db78b8ee07e9" + }, + { + "name": "fix/daemon-typecheck-core", + "sha": "4198e95d54bb0e61f6e9fd7d23476fa3655d59b8" + }, + { + "name": "fix/daemon-typecheck-leaf", + "sha": "a2b3287484df65bbf9d523cf97bcd83198b658d9" + }, + { + "name": "fix/desktop-entry-chrome-consistency", + "sha": "e06d8702a1911ea86e8c7437f1819c78b66d8d18" + }, + { + "name": "fix/desktop-stable-web-origin-511", + "sha": "6d3c22f080fe1269e13fbfc90d4c89271ff4e8d7" + }, + { + "name": "fix/entry-tab-layout-and-ds-showcase", + "sha": "f6d44b709ad59c469b05b41f769b6d7f432f4d77" + }, + { + "name": "fix/i18n-content-dup-keys", + "sha": "2020d70250bfeef4569005b247491b41ea2ff9ef" + }, + { + "name": "fix/i18n-designfiles-showmore-coverage", + "sha": "aea1ae4b598c5143dbab866abafe691926a2fad5" + }, + { + "name": "fix/issue-1118-windows-installer-process-close", + "sha": "7648a97b1e5741dfe511756ad27a690721bd198f" + }, + { + "name": "fix/issue-1215", + "sha": "425de3b6ddf9b946cea2f1454734237e113aeccd" + }, + { + "name": "fix/issue-164-media-output-validation", + "sha": "565152358c3587f76d75e988ee8faeaf20432189" + }, + { + "name": "fix/issue-37-unfinished-todos", + "sha": "0e7fddf6840ec7e5f4c3253057e2303df2d9afd2" + }, + { + "name": "fix/issue-891-run-shutdown-cleanup", + "sha": "adfbf50f9a36fde29082695b749c050f58aef1fb" + }, + { + "name": "fix/langfuse-report-hook", + "sha": "1ce8d766255b823ab413a5093f3cb978720a646a" + }, + { + "name": "fix/media-provider-key-visibility", + "sha": "331be764b62eebfa4cee2bf864b5800b07dbeff3" + }, + { + "name": "fix/orbit-template-migration", + "sha": "988e7279277fd693b47a07e82d214cfb1f9412e3" + }, + { + "name": "fix/pnpm-install-toolchain", + "sha": "e3d6ba60d32772fd89553f549ed9a62b0dd241b3" + }, + { + "name": "fix/prompt-template-desktop-close-hitbox", + "sha": "206fb2e30027acc5c546c566ff77980be0215080" + }, + { + "name": "fix/question-form-max-selections", + "sha": "d1b26a5e782e6bb5e962aa76676a17be0f997f0b" + }, + { + "name": "fix/release-stable-build-desktop-too", + "sha": "8c24693a70bcda69fbecd16f13bb610d0e9cb039" + }, + { + "name": "fix/release-stable-defer-linux", + "sha": "c00eae8828f8ccd1966607da3b6d7594402696cb" + }, + { + "name": "fix/release-stable-linux-no-corepack", + "sha": "d5549bf016d0ac791c40eac8bdf4bea8ae4485d8" + }, + { + "name": "fix/release-stable-linux-pnpm-standalone", + "sha": "58d9f476eba8ca24a77fefc2b40afef55daeb376" + }, + { + "name": "fix/release-stable-verify-typecheck-order", + "sha": "11bdc05eaa42463e643aa3fa14333b5481c3c5fa" + }, + { + "name": "fix/retry-stopped-task-accumulation", + "sha": "d47fa79b63675e353841312331b246839bcf5fc9" + }, + { + "name": "fix/todo-dark-mode", + "sha": "36ae0a91e99b4f49b3a0e354fac430ff979da231" + }, + { + "name": "fix/tools-pr-stats-chunk", + "sha": "d5839938d3e4c240e6a4f9199644085a485d3df2" + }, + { + "name": "fix/tools-pr-unresolved-cr-fallback", + "sha": "5a62cdeda90eb8fedb2fcd2236703a300713a382" + }, + { + "name": "fix/web-header-layout", + "sha": "f9bc0bf05dd1997eb80f6febe053abdec4ecb573" + }, + { + "name": "garnet-hemisphere", + "sha": "89bd42c3d40fda5398aac8fe4a54d789e01fed1a" + }, + { + "name": "glow-seashore", + "sha": "c4b383591f566d3db8110eb957f4b4874f7e97b3" + }, + { + "name": "holistic-pipe", + "sha": "11c248a81bfad0dbd4d3ae33230c1e890fd333e1" + }, + { + "name": "hyperframes-html-in-canvas", + "sha": "dc5a11fa304b63061823d6ec0d1a5ce3e0a6a70d" + }, + { + "name": "i18n-content", + "sha": "2dadad4c1b0af33b888c9a4b5ae8f7774a1828f4" + }, + { + "name": "incredible-vessel", + "sha": "821eeaf228d9df050f2a1a23c4aed32820c79a15" + }, + { + "name": "init-agents-md", + "sha": "0f249841de96e3cae46c7e64fdbfcb71a2d9b0a8" + }, + { + "name": "issue-38-document-preview", + "sha": "65b03c30efd38177f75aec41216f453b81f27110" + }, + { + "name": "joey/add-maintainer-rules", + "sha": "eabd478cb9c41318171534c7d86f4d1543caa6b0" + }, + { + "name": "landing-page/auto-deploy-and-facets", + "sha": "f58a4a25555bc7592e59d439972629d18685a646" + }, + { + "name": "looper/149-improve-tools-dev-diagnostics-c60e851c2e396383", + "sha": "c7e1d6e3870d934149bdf1f484804acb7a6006a9" + }, + { + "name": "looper/47-add-visible-conversation-times-108bcb81859dd5ff", + "sha": "3c472513f1a8a04dbab11c726163dc857b27a0d1" + }, + { + "name": "looper/53-preview-pagination-state-leaks-9709166f7bac9a55", + "sha": "e4c829adeff6f4a91acdfcc63e9d8bf3b00b9c93" + }, + { + "name": "looper/57-deck-preview-pagination-contro-d9ce790cc18e1f35", + "sha": "0fc46f90aaa81f285aeba5b796e95bd97a5deb1c" + }, + { + "name": "looper/69-react-9193ae20697560a7", + "sha": "bd944da63a6e467f477c7c4cf7e531382acbc8bd" + }, + { + "name": "main", + "sha": "23218eacd9f7a6de2bbcb10532d17fab56676a8b" + }, + { + "name": "merge-conflict", + "sha": "158b655e56aaa08570da25cc155b1bce3bc2aa41" + }, + { + "name": "metrics-run-25239911967", + "sha": "5befea59dc9d39abba0c880b93286627c308103d" + }, + { + "name": "metrics-run-25266427929", + "sha": "e4bdc6a5f355d67b61ddbb484430d3cdfda56683" + }, + { + "name": "metrics-run-25410988456", + "sha": "16fde6b883569f4cd0e420a70bbafb046b5a35b0" + }, + { + "name": "metrics-run-25706979263", + "sha": "5a08c76347c3604c652b18f7ad4b985a938a628b" + }, + { + "name": "optimize-electron-dist-bundle-size", + "sha": "c57d393ddb612bf91d482666c420117ddfd07681" + }, + { + "name": "pr-770", + "sha": "227d523e3f52462ad67798400265fa1d6a5b36af" + }, + { + "name": "pr17-modernized", + "sha": "248fa878ed5fb5452e1cf6d2b5c1e7a3ece500f7" + }, + { + "name": "prickle-kite", + "sha": "17747689a453dc45519a5a38d6523f9ccf0d6d89" + }, + { + "name": "quiver-saw", + "sha": "ea84582e5c30e1416f51717e456098d2aa56d113" + }, + { + "name": "release/v0.1.0", + "sha": "9023ed8021da13d14b3650edfd0622f1e4c11274" + }, + { + "name": "release/v0.2.0", + "sha": "63b167c14f719767fe9b8a04d8d50880b9e89f5e" + }, + { + "name": "release/v0.3.0", + "sha": "dc1c92ec22bd40633c5910861bcf5bb4d0bf0c32" + }, + { + "name": "release/v0.4.0", + "sha": "25b9ac68172eebef387bfca04c69ca8ced24e370" + }, + { + "name": "release/v0.4.1", + "sha": "a92baeccade13452ac39f6ddb62c7b9a3d88b027" + }, + { + "name": "release/v0.5.0", + "sha": "c21cbc60e63f98d4eabe1d11a6d4a698388726a7" + }, + { + "name": "release/v0.5.1", + "sha": "74cf8a2b3a5d800e83c3d63d073e9ce88070b6c1" + }, + { + "name": "release/v0.6.1", + "sha": "04d72f0b79371be1d21d18739f6d0d335a820d4a" + }, + { + "name": "release/v0.7.0", + "sha": "2a0ebea50b0438df2f0cec3e340403757be10f2a" + }, + { + "name": "server-ts", + "sha": "7cbec20359e6edeb8fc7f49a454ba9821672d270" + }, + { + "name": "spike/release-beta-windows-signer-hello", + "sha": "3661ff6b16a16e8f6c69b93d7a3dfdd4a4bfd569" + }, + { + "name": "stellar-owl", + "sha": "0b1a1aa7de66471da007bb06bb7be78a63b685da" + }, + { + "name": "tailwind-phase-1", + "sha": "e10d1a2a98d4d2cc72cbfdc769d16ac066ab47b1" + }, + { + "name": "tar-mandible", + "sha": "5afbda712afb47e4a99273480a2bb8c6eb56e6d5" + }, + { + "name": "template/digits-fintech-swiss-template", + "sha": "e60373eee6cf83f464f92fdb4a63e3c14c29b889" + }, + { + "name": "template/field-notes-editorial-template", + "sha": "a852fabb3c463408ad1a39f1841e2d3d8cef63fc" + }, + { + "name": "template/html-ppt-retro-quarterly-review", + "sha": "46ada528714e7c438bfe6707698c0765d9cee54f" + }, + { + "name": "template/hyperframes-video-template", + "sha": "f458a005732a4e367303103acebb07b469d042a1" + }, + { + "name": "template/social-media-matrix-live-artifact", + "sha": "5a5cab4148d411b3a66f41fbc4eba00625ffe19a" + }, + { + "name": "token-first-tailwind", + "sha": "9238eafb792e73d6981493e592b7e141f1622243" + }, + { + "name": "ts-doc", + "sha": "5bc9a6c2d350b7fa2e7d01ec677c207ee79d3150" + }, + { + "name": "update-contributors-wall", + "sha": "b881a1a9f4b93c15084203b482bf76e107c03fe5" + }, + { + "name": "worktree-feat-posthog-analytics", + "sha": "8b824fb326daa026b7c0eb7e2e27c24e23a62174" + }, + { + "name": "worktree-fix-issue-10-windows-spawn", + "sha": "dd0f00f9d2c4de814f8be68eca82212495d54af0" + }, + { + "name": "worktree-fix-issue-6-skills-permission", + "sha": "ab2c00a1fd4c1480d2de8d979afde7fa57af8c92" + }, + { + "name": "worktree-fix-issue-8-model-picker", + "sha": "bbeb8680400e6745a335b88a4440fe2debe337f3" + }, + { + "name": "worktree-nextjs-16-refactor", + "sha": "409064809fb74bb42a749617ffb4e9e1d77aebbf" + } + ], + "error_summary": "" + } + ] +} diff --git a/docs/security/github-target-repo-approval-package.snapshot.json b/docs/security/github-target-repo-approval-package.snapshot.json new file mode 100644 index 00000000..54af7283 --- /dev/null +++ b/docs/security/github-target-repo-approval-package.snapshot.json @@ -0,0 +1,198 @@ +{ + "schema_version": "github_target_repo_approval_package_v1", + "status": "draft", + "source_snapshot": "docs/security/github-target-decision.snapshot.json", + "package_count": 7, + "approval_items": [ + { + "github_repo": "owenhytsai/awoooi", + "source_key": "wooo/awoooi", + "risk": "HIGH", + "approval_action": "reconcile_refs_after_full_inventory", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "security-commander", "human-owner"], + "blocked_until": [ + "Gitea server-side 全量 repo inventory status=ok", + "branches/tags/workflows/webhooks/secrets 名稱 inventory 完成", + "部署真相來源已決定", + "GitHub primary ADR 與 rollback plan 完成" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "產生 draft migration PR 或 ADR", + "更新 migration matrix 與 evidence" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 GitHub primary", + "直接停用 Gitea", + "搬 secret value" + ], + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "Gitea/GitHub main SHA、branches、tags 未對齊,必須先做 reconcile plan。" + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "source_key": "wooo/clawbot-v5", + "risk": "MEDIUM", + "approval_action": "reconcile_refs_after_full_inventory", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "human-owner"], + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub 缺 Gitea tag 的處理方式已決定" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "更新 migration matrix" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 primary", + "刪除任一端 repo" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "GitHub repo 可見,但 main SHA 與 tag 狀態未對齊。" + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "source_key": "wooo/wooo-aiops", + "risk": "MEDIUM", + "approval_action": "reconcile_refs_after_full_inventory", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "human-owner"], + "blocked_until": [ + "Gitea/GitHub main SHA 對齊或人工指定真相來源", + "GitHub-only branch 與 tags 的來源已釐清" + ], + "allowed_after_approval": [ + "產生 refs reconcile plan", + "更新 migration matrix" + ], + "still_forbidden": [ + "直接 push refs", + "直接切 primary", + "刪除 GitHub-only refs" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "GitHub tags 比 Gitea 多,需先釐清真相來源。" + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "source_key": "wooo/wooo-infra-config", + "risk": "MEDIUM", + "approval_action": "confirm_internal_remote_purpose", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "security-commander", "human-owner"], + "blocked_until": [ + "110 internal remote 用途已確認", + "若 110 remote 為舊主控,已降級或移除", + "infra secrets 名稱 inventory 完成" + ], + "allowed_after_approval": [ + "標記 110 remote 為 mirror、legacy 或 active source", + "更新 canonical decision table" + ], + "still_forbidden": [ + "直接刪除 remote", + "直接同步 refs", + "搬 infra secret value" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "GitHub 與本機 main 對齊,但 110 internal remote 不可讀,需判斷用途。" + }, + { + "github_repo": "owenhytsai/ewoooc", + "source_key": "wooo/ewoooc / root/momo-pro-system / momo working trees", + "risk": "HIGH", + "approval_action": "create_or_grant_access_after_canonical_approval", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "security-commander", "human-owner"], + "blocked_until": [ + "ewoooc/momo-pro-system canonical 關係人工確認", + "server-side refs diff 完成", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 mirror", + "自動合併 unrelated histories", + "刪除任一 momo/ewoooc working tree", + "切 GitHub primary" + ], + "evidence_refs": [ + "docs/security/GITEA-REPO-INVENTORY-SNAPSHOT.md", + "docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "momo/ewoooc lineage sample 目前 unrelated,不能自動視為同 repo。" + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "source_key": "bitan-pharmacy", + "risk": "MEDIUM", + "approval_action": "create_or_grant_access_after_canonical_approval", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "human-owner"], + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 repo", + "自動 push refs", + "刪除 110 remote" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "110 remote 與本機 main 對齊,可作 source candidate;GitHub target 未確認。" + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "source_key": "tsenyang-website", + "risk": "MEDIUM", + "approval_action": "create_or_grant_access_after_canonical_approval", + "approval_status": "pending", + "required_reviewers": ["migration-engineer", "human-owner"], + "blocked_until": [ + "確認 repo 是否仍 active", + "GitHub repo owner 與 visibility 決策完成" + ], + "allowed_after_approval": [ + "決定建立 GitHub repo 或授權既有 private repo", + "產生 migration plan" + ], + "still_forbidden": [ + "自動建立 repo", + "自動 push refs", + "刪除 110 remote" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md" + ], + "notes": "110 remote 與本機 main 對齊,可作 source candidate;GitHub target 未確認。" + } + ] +} diff --git a/docs/security/local-git-remote-inventory.snapshot.json b/docs/security/local-git-remote-inventory.snapshot.json new file mode 100644 index 00000000..6b5d45af --- /dev/null +++ b/docs/security/local-git-remote-inventory.snapshot.json @@ -0,0 +1,303 @@ +{ + "schema_version": "local_git_remote_inventory_v1", + "status": "partial", + "roots": [ + "/Users/ogt", + "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs" + ], + "max_depth": 4, + "gitea_host_fragment": "192.168.0.110:3001", + "repo_count": 13, + "gitea_linked_count": 6, + "github_linked_count": 6, + "mapped_count": 4, + "gitea_only_count": 2, + "github_only_count": 2, + "internal_110_only_count": 3, + "unique_gitea_repo_count": 4, + "unique_github_repo_count": 5, + "unique_internal_110_repo_count": 4, + "unique_gitea_repos": [ + "wooo/awoooi", + "wooo/clawbot-v5", + "wooo/ewoooc", + "wooo/wooo-aiops" + ], + "unique_github_repos": [ + "nexu-io/open-design", + "owenhytsai/awoooi", + "owenhytsai/clawbot-v5", + "owenhytsai/wooo-aiops", + "owenhytsai/wooo-infra-config" + ], + "unique_internal_110_repos": [ + "bitan-pharmacy", + "root/momo-pro-system", + "tsenyang-website", + "wooo/wooo-infra-config" + ], + "repos": [ + { + "repo_path": "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/awoooi", + "repo_name": "awoooi", + "status": "mapped", + "gitea_repos": [ + "wooo/awoooi" + ], + "github_repos": [ + "owenhytsai/awoooi" + ], + "internal_110_repos": [], + "remotes": [ + { + "name": "origin", + "kind": "github", + "url_redacted": "https://github.com/owenhytsai/awoooi.git", + "repo_slug": "owenhytsai/awoooi" + }, + { + "name": "gitea", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "repo_slug": "wooo/awoooi" + } + ] + }, + { + "repo_path": "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system", + "repo_name": "momo-pro-system", + "status": "gitea_only_local", + "gitea_repos": [ + "wooo/ewoooc" + ], + "github_repos": [], + "internal_110_repos": [], + "remotes": [ + { + "name": "origin", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git", + "repo_slug": "wooo/ewoooc" + } + ] + }, + { + "repo_path": "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/open-design", + "repo_name": "open-design", + "status": "github_only_local", + "gitea_repos": [], + "github_repos": [ + "nexu-io/open-design" + ], + "internal_110_repos": [], + "remotes": [ + { + "name": "origin", + "kind": "github", + "url_redacted": "https://github.com/nexu-io/open-design.git", + "repo_slug": "nexu-io/open-design" + } + ] + }, + { + "repo_path": "/Users/ogt/aider-watch", + "repo_name": "aider-watch", + "status": "other_remote", + "gitea_repos": [], + "github_repos": [], + "internal_110_repos": [], + "remotes": [] + }, + { + "repo_path": "/Users/ogt/awoooi", + "repo_name": "awoooi", + "status": "mapped", + "gitea_repos": [ + "wooo/awoooi" + ], + "github_repos": [ + "owenhytsai/awoooi" + ], + "internal_110_repos": [], + "remotes": [ + { + "name": "origin", + "kind": "github", + "url_redacted": "https://github.com/owenhytsai/awoooi.git", + "repo_slug": "owenhytsai/awoooi" + }, + { + "name": "gitea", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "repo_slug": "wooo/awoooi" + } + ] + }, + { + "repo_path": "/Users/ogt/bitan-pharmacy", + "repo_name": "bitan-pharmacy", + "status": "internal_110_only", + "gitea_repos": [], + "github_repos": [], + "internal_110_repos": [ + "bitan-pharmacy" + ], + "remotes": [ + { + "name": "origin", + "kind": "internal_git_110", + "url_redacted": "192.168.0.110:bitan-pharmacy.git", + "repo_slug": "bitan-pharmacy" + } + ] + }, + { + "repo_path": "/Users/ogt/clawbot-v5", + "repo_name": "clawbot-v5", + "status": "mapped", + "gitea_repos": [ + "wooo/clawbot-v5" + ], + "github_repos": [ + "owenhytsai/clawbot-v5" + ], + "internal_110_repos": [], + "remotes": [ + { + "name": "gitea", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/clawbot-v5.git", + "repo_slug": "wooo/clawbot-v5" + }, + { + "name": "origin", + "kind": "github", + "url_redacted": "https://github.com/owenhytsai/clawbot-v5.git", + "repo_slug": "owenhytsai/clawbot-v5" + } + ] + }, + { + "repo_path": "/Users/ogt/momo-pro-system", + "repo_name": "momo-pro-system", + "status": "gitea_only_local", + "gitea_repos": [ + "wooo/ewoooc" + ], + "github_repos": [], + "internal_110_repos": [], + "remotes": [ + { + "name": "origin", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git", + "repo_slug": "wooo/ewoooc" + } + ] + }, + { + "repo_path": "/Users/ogt/momo_pro_system", + "repo_name": "momo_pro_system", + "status": "internal_110_only", + "gitea_repos": [], + "github_repos": [], + "internal_110_repos": [ + "root/momo-pro-system" + ], + "remotes": [ + { + "name": "gitlab", + "kind": "gitlab_110", + "url_redacted": "http://192.168.0.110:8929/root/momo-pro-system.git", + "repo_slug": "root/momo-pro-system" + } + ] + }, + { + "repo_path": "/Users/ogt/stockPlatform", + "repo_name": "stockPlatform", + "status": "other_remote", + "gitea_repos": [], + "github_repos": [], + "internal_110_repos": [], + "remotes": [] + }, + { + "repo_path": "/Users/ogt/tsenyang-website", + "repo_name": "tsenyang-website", + "status": "internal_110_only", + "gitea_repos": [], + "github_repos": [], + "internal_110_repos": [ + "tsenyang-website" + ], + "remotes": [ + { + "name": "origin", + "kind": "internal_git_110", + "url_redacted": "192.168.0.110:tsenyang-website.git", + "repo_slug": "tsenyang-website" + } + ] + }, + { + "repo_path": "/Users/ogt/wooo-aiops", + "repo_name": "wooo-aiops", + "status": "mapped", + "gitea_repos": [ + "wooo/wooo-aiops" + ], + "github_repos": [ + "owenhytsai/wooo-aiops" + ], + "internal_110_repos": [], + "remotes": [ + { + "name": "gitea", + "kind": "gitea", + "url_redacted": "http://192.168.0.110:3001/wooo/wooo-aiops.git", + "repo_slug": "wooo/wooo-aiops" + }, + { + "name": "github", + "kind": "github", + "url_redacted": "https://github.com/owenhytsai/wooo-aiops.git", + "repo_slug": "owenhytsai/wooo-aiops" + }, + { + "name": "origin", + "kind": "github", + "url_redacted": "https://github.com/owenhytsai/wooo-aiops.git", + "repo_slug": "owenhytsai/wooo-aiops" + } + ] + }, + { + "repo_path": "/Users/ogt/wooo-infra-config", + "repo_name": "wooo-infra-config", + "status": "github_only_local", + "gitea_repos": [], + "github_repos": [ + "owenhytsai/wooo-infra-config" + ], + "internal_110_repos": [ + "wooo/wooo-infra-config" + ], + "remotes": [ + { + "name": "origin", + "kind": "github", + "url_redacted": "github.com:owenhytsai/wooo-infra-config.git", + "repo_slug": "owenhytsai/wooo-infra-config" + }, + { + "name": "gitea", + "kind": "internal_git_110", + "url_redacted": "192.168.0.110:wooo/wooo-infra-config.git", + "repo_slug": "wooo/wooo-infra-config" + } + ] + } + ] +} diff --git a/docs/security/local-repo-canonical-ewoooc-momo.snapshot.json b/docs/security/local-repo-canonical-ewoooc-momo.snapshot.json new file mode 100644 index 00000000..f4eceaa6 --- /dev/null +++ b/docs/security/local-repo-canonical-ewoooc-momo.snapshot.json @@ -0,0 +1,328 @@ +{ + "schema_version": "local_repo_canonical_probe_v1", + "group_name": "ewoooc-momo-pro-system", + "status": "unrelated", + "sample_limit": 100, + "git_timeout_seconds": 8, + "repo_count": 3, + "comparison_count": 3, + "repos": [ + { + "label": "local-momo-gitea", + "repo_path": "/Users/ogt/momo-pro-system", + "exists": true, + "head_sha": "61a9c4c1e3861f773d221c7efed6c5cd5ac9ef39", + "head_short": "61a9c4c", + "branch": "main", + "commit_sample_count": 82, + "commits": [ + "61a9c4c1e3861f773d221c7efed6c5cd5ac9ef39", + "a13683d6555d96a14c2458eaafcb8a3cd656eade", + "0c9a3cd875cf0578699c2ab0f9524f9d0c3f78d7", + "cac7303e46cc1ce13336362e851cf06a0e5d283b", + "1f7b903d36b996ffd280a7c22010f61a07cad2ca", + "2e0de960ce5d984e2b4b34c47cb5f4aa0a65f9d6", + "38200a5e934cd29836e7d3bdd96a859b72a0a373", + "784a3135c19a2ca26808f6273569a313004320d0", + "a62b83f488c262134baf626b9400da744e4b1197", + "31dfbcdd4d215703d0f7f51f509efaa48987498a", + "0cc940fdb146541544acc0f8cb1c98b4bced7b52", + "c447cbee44ce234306fc8c6aebff12bf641bcc8b", + "bf5f0d256ada693284bdbd2acf46722581bed593", + "e343a8532232cf462a7d980f29a294bb8ae66e52", + "3127466a853fc3fe759464a81cbd83c0e90a5015", + "4f4e7ef062b338ea17a5a2a065a259b947ff6c59", + "b8e6f752fa10587b35ad7844a2a34c7915261e67", + "8df8b24043cdebc8ccacf3ca51578a7cbafaebd2", + "b37658f7be92c18b7dda748634b28c68e5d46760", + "74de1dc68a2b0608d62c056b37c56b2a116f2379", + "48804553cdd9e06e924a6435cd192b1baf332bd0", + "d349b09afd1fc59d4c35dd7d0b9a060d00b43648", + "b2803c90be8469703a7bef257f5da010a90552ce", + "34620b7b045bb3b54624bba5264395df964e9260", + "65de5d7893bd1f05153f2337c808365d8e8cd926", + "c8da68125d70a5918900c4fece09526b8227c916", + "704f5b6538152468987c4a14acbf19f29a44b54f", + "9ce8a51326bcf3e3603a22184a849f26f39efef1", + "cab57c4fb5ed5db9b991b641d341e02f3d3563b2", + "4c8edecd12b24303ad066e2ac0a30abde50dbe74", + "fca235eb8d4057354b66904a92bb372775da2df4", + "2ffbe06eabe64dc6d11163d721e7e80046b9f887", + "456c0319550211f3d4604e82623ea2e7f19278cf", + "e0d3b54527ad84d57a7af6f7312a20e01c392e17", + "6435bed005d7781a15f9a93b3b5f49b9d7308588", + "3da9ba247c24d919d95ea4a1ab35cccf465fe7d6", + "043ad3e6d940be978466ac389e6a5dfbdad62993", + "20e83306fe3b2cab1b57bf675e77bd17bdc4efb5", + "38586deff13545d909b6749ec11cbebfd9f0fbd3", + "96e19b6b7211d736b6cb13a301b803645b707fd8", + "1c03d213acfe01233dc17fac52734e9b8b48b708", + "61496af2c5668167307aba79be7a05f29915d420", + "d8d1f3dee8dcb3c4cb625aaa3ba7c120d20343c5", + "47cfd79513b1d94159712e8b79df3c15830785ab", + "aef8982cbbe1df6418c86ef7d7f5f69bfd807cf8", + "f2b20c1892cf82db42baa9b84b4046525dfa483b", + "266af27fd6e4d8f3973d43a8b8acc30e81fe328e", + "ba86f985142c82b94fdbab2fc0fa3381480cac4a", + "f5faf478bb6da0074e71952ecae53dfb09a4a288", + "055eca1cd84732a8d86fb4f37cbb89499fe880ee", + "72b047625e00e42cb030c0d6393982302df6c38f", + "c73b430566798926537fc43ec26ca05c5c0ed700", + "ce934d5f72285d11bc4cc91690dbb1426ddd4399", + "d9d807a8cbcf4bff945a3cc3fad158ad42a48d66", + "4bc73894777af530c3e2b63d8f50250b2ce72a5b", + "4ee4ec097ed24e517f3e4c48ebdbf83648819e89", + "69df1436b7fbb590d5a779c2dd370eb32f1d9ab1", + "15c899915a98c38376fa4f5bc01e5335734ddb88", + "551bab5fe665fb5aadb0cf919db1ce3111a620ec", + "19342a0044fffdbbefb85cd2d1920f8f63320c85", + "fb0dad22892edc1bb658367b3e07bb791b3a90d8", + "352a99db58a93ac8ac19cc129af3b00f3bb01e1e", + "cb03f6b3e88065eb89f693a6a463fbf7c1494fda", + "e6642d5e178959f55861055ab88d7c2e6acfd685", + "77d3a1da48bcb9ddf95f35757abfc2b0db1edc91", + "7fbeaaf213de6748fd86038c1fd0f8e4270dbc18", + "1fd1622007cabba9636587ec027248d854a25976", + "bda4edd23beb0e5bc6fe7b26ec2d8d82eceb5d38", + "0b4f80ee8ac608b39d0178867ad5efce1371a1ac", + "528a6c0468a68ecd53252c75f46ae40757a3b1c1", + "8d0b79cd00dba73b221af6a647a1e7f4d8480117", + "986908222dc380fd2fbbd4b3550d224945a2f477", + "2394d65634bc51cadfab68d910c35c36d81369fd", + "e6109c2ef8a8477481032a22088430dbbb25ce34", + "8c6fe961cb8ddeb79cf30699394baab683081e28", + "709efb6e377f06176e3f9e2db84b8cc13ce215de", + "c49c2c4f6f1570d91e2e56f8b11180b64b965b79", + "62d8504d8fe774efc5557349fec9119395cf5069", + "abefca99e50bd9f16d68f644276fd00817bc30a2", + "676c711e7a73241d12fe2f4ff649b03011cf5e6b", + "30e44851427a84969c5749895d935a6226e65c10", + "1b4f3a7bbea1f1f627a7124b4ba03b6e24829d37" + ], + "remotes": [ + { + "name": "origin", + "url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git" + } + ], + "probe_error": "" + }, + { + "label": "icloud-momo-gitea", + "repo_path": "/Users/ogt/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system", + "exists": true, + "head_sha": "fe6180b8ad95db0129d6aff8e7b99f2fc69784b1", + "head_short": "fe6180b", + "branch": "main", + "commit_sample_count": 100, + "commits": [ + "fe6180b8ad95db0129d6aff8e7b99f2fc69784b1", + "605250619ccf7938e90967fa0d9c1f4f7c019e9f", + "28fc3587bb8ebdbaf47564557d9955c3dc7c3373", + "57e4c575b1cd73fda94f02cfa8a34257f5347c56", + "9e857ee04aa3dd23038abd51635a5bbd5ab1686e", + "aff9fdec2127572fbd88e1e2e59c2d98e442bba9", + "e317a2f70afdebfb78b61249e5b188fd5ac5be0d", + "ba9aecf66135af47a6597cd1b251339dc79e21a3", + "dc137e33ca8e3a8abf38e2b4015c214ee2e715c6", + "464cb6b0373e4600c76a64328384f6444ebcc6be", + "7bc81e966b2c0a74c2c36a85b6a71a893711b925", + "11ccda0e1c0e2b92d7c124d8b52afafc4aee984c", + "e6328e543e34599366eb6de63bcf0d8115368080", + "30a173cf694955a646af715306bcbb9fc3406e65", + "153e4c973417bef0f835ede98b1538d147979ad7", + "308efdce25e66c3ce57fb3c1b458ce2b27d5caba", + "dc7fe371bd44b32ace0fcbf04689feb255f6cc47", + "0904a602377d45f3fe26d4b2c4fd9146f9211284", + "a6100a3d019295bc7fb7fff15b78549ac127fd92", + "8cb82d4cd5a7f9776506c6323c9eb2b3252da769", + "215bd9b73c178a2e378c4be28b266749372aa503", + "4380fa641ceb78fc00d60dfbef8ea89a3c922d9d", + "3db8f5c5b2227693147aad695ccf6395d9987f87", + "7225e81c08d2f0f71385cad261d2e800c7ebc95a", + "ca22b7fe7cd18a20f72b7d66af42ca6b11092fc5", + "7ce74e32fe4b259d638d2ea90c475dcfbebf9088", + "65eea5eb9aca28d078a006259e816714a1da9760", + "ce7dd6068c16f9731eb9512394cc3e21e417fa1a", + "be1d1aec03f078a27b33c485d8ff35252840d0fa", + "cdcbcf1d803d506ee13be6c337ac90cdc2a73b55", + "346e9672a66e29d8a3ab62f800291714425b22fc", + "15f7c8660dc03b02b7be33df92344b29754be028", + "6d015c5b6b635e27f7fd1a5b6455451b6c6bf541", + "b21b40cae23c3f595a5241f32c0e75ce7c36edd3", + "d93ad659ba127fdb130b345f3956e16b1ce6e6c3", + "422137efa824691ee4d43042bcff41e505167272", + "e7d567c6becdf8077f4851200e8071d42d667699", + "8643ed12ad45946454740be8b5cd0ec25a05bdaf", + "3fca720fa156099ae3b8cb20afdfaa7ea77dbee5", + "6a0d5c138d3ccbc86d8fcc16c0dd906facc59394", + "b963dcf209b82a04fee8b27313832e22d950f1c8", + "62276f8b0cb19f5fa8207a76900e0cd7e6391c5a", + "07c9e200d02651c12136dbc09737a0972175f4e8", + "fa3e0884ade0ee17dc74fc6ff83ef1416389beec", + "ddcfd9603b39d834b396b74face542a007cbf350", + "ccd26415f3834fe099d51f6e628dfd0a84fab2c9", + "668d98cd3cadedc42b870e98cd183ebc13ad03cb", + "2c11a3dc8102daa89344992d77059385fe828cfe", + "4a745c27b42fe0fd66e203c3427518ba036082f3", + "3b9a74773c1f3da4dd12812aa73097992ea9328b", + "be986b8b97e21b666f5830e1cac7b95092d3ee41", + "e28f604ec6e5229d5ec5995a751491c7c03e0c62", + "4afcf3376b4e4feecdd52c2b0af3a16e4d8ac07d", + "c7242971e33be97e8bb2de48d7d7c24c838d566e", + "67b93a8b5013b4a7e258f27af504c83c9f3073dd", + "c38f22e67a43ffe76774a9171f66c2661adcf8ae", + "505cbe20c747c23aea4dd49046415419fc542f91", + "6f8fdc14bac521f7f3d1b5332c257a24e0d5f2a3", + "9b908ca4260ef98cf5c45a02aee6f75784187e53", + "f6a2a05e3f490c847ab74f92c41921bc88244c5a", + "c57b8f40eed6c3387e35925cb437a7bed90546e0", + "054685826a451f6b470744fa3e829535245e54f4", + "2bb2e16442fda3ab08739a9f199408790996c5be", + "326285d8b93cf9a68d35533c922a314c4373b5c2", + "df2311d4f044b408b68c272136ca855c3eee4ca9", + "90e8366a8d8fa0a6996deb77f5dcbf3fb5c20d1e", + "118f10701bb79327a39e3318c83041761586e9d4", + "7a10d27d617cd1c57988e6266cb9a20c2e231d75", + "a142e858800680fd526f49beb85574ca18429bdf", + "2a3ea6f581106533e6bb765a7513b0417fcf059f", + "e0a8d87c2cc6cd721e0aee7d8c6b835b8adcf528", + "87d460e243d9795f052ba11f41da7be3dd976d0f", + "822789c81005dce03ef7f0eada4a851ac489f89a", + "95db06ad9d932106c367b12822a07144479d7357", + "2e124db602f3b5596a45b34c9eb871cfe11f6894", + "347efb8ea100ea331a8337cfc5af41caf6940c0f", + "849e189b609a42eef960ad26ba0da4ab6ebe1455", + "72cbcb298f88ed7607582dc6bc8019631fe4f5c3", + "f10999ed1c8c026ddeff1cb0b4699efa7273b664", + "d5a4e273449541b47d13ba1d4b0af1d73551afbe", + "4020b734a5a944e9a70918db807008fa3460896d", + "65f236da2db3d5a9dff23f521f1d8f209d717ebf", + "79cf08c58c7e140ef12cb657e5c0d63a9774c49d", + "5935a6512c1923bd421e486001a5dd9712acb49c", + "0b130554662eb9b1b989ad258ab3f8258be4d6da", + "19f1340f5c67714908b44e694e1ad63ddd98ab7f", + "9bc6664dc08edf7765df7cf1ab7d83d81836ae77", + "64fe4fb651afe287f8616244c24deed36e3bf261", + "46255720ee8c416c8a0f7dbf129a8897bd9e4eaa", + "927d7072cebda4c62872622505e57a4c3fe4c3fa", + "86f1fd5f50ac4bda494e17fcc754926e29b67308", + "99d2f3c543e683f2bfeb3b1d6c55bdace2d15dcd", + "82595ab4acbd1450b795e19ff365fa5cd79eb80b", + "f2fbe5f929bc81e8acadf93241221c5847a9924c", + "69ccf8029b5cdf4f6772e8c90837686911c7b7f0", + "48b8fda7db950cb2e7a04afe6612ff3b50d82636", + "c7d04b2855ad231c8c9e9d7ee4b45e4fe240bb1a", + "72a7c385d5bcb497d36a4ea0e6f06d854a6394db", + "bd32e04dad102d563fb806b56d926cbd3b8b0f6e", + "0476d3ae4e0e4012133a92327800c0d207074f53" + ], + "remotes": [ + { + "name": "origin", + "url_redacted": "http://192.168.0.110:3001/wooo/ewoooc.git" + } + ], + "probe_error": "" + }, + { + "label": "local-momo-gitlab", + "repo_path": "/Users/ogt/momo_pro_system", + "exists": true, + "head_sha": "5a4294e5c08d238825c47714343d483d0b0e37b0", + "head_short": "5a4294e", + "branch": "main", + "commit_sample_count": 52, + "commits": [ + "5a4294e5c08d238825c47714343d483d0b0e37b0", + "f2a6459b6d354c5436fc04b4e428eb3c74d890d2", + "28b39f23f5573713cbdef69bfb76f71a9b17d461", + "4921a6a7ec185d9fbb6deee5c9533a105a2a5eed", + "20426eaa4b5a5e3c0cd18d5dd465ecee6f0b5b68", + "3d97d3b13aa4c006e880f768f28f404203d600ce", + "8edb75381926656c98cd1edcbfc38649fbcd482b", + "b325e2f55bfe4302c31b43660a0bf0498215f9cc", + "fb5fd468d24c71694610cb4d3edcc95133bf70bf", + "72a859d7d1673d054d8bc5d9ad07711c4693d362", + "d9340d075c7eae81af5bc1a59fb50105be68fbd5", + "db07eb08470b1e2f11f4cc3f7f7542e161959eb7", + "d6473f7730447f689de146797116bd960652e518", + "2e4244a0952c64cea69737d258a0998b9317c2f0", + "f6959a162a50c1584b477218390dc3b798ddde1b", + "ba9de21460e0b2ca0cbda1935ab15db855b1fae7", + "c0f526e34d9acb5b018295f876f6260c3e9386b6", + "eacb97ace261f707b842cc6f5781537628275ab0", + "23bebd4583c5c0d6e2d6608c9f79d3f163cac019", + "15d54d4e537f9b3f79143831533b7dbfebce3282", + "87039f3b49f4774df9345f7cd0b0a8b1428ca60d", + "011306fd8499eaba1e20db236c7bf37660369aa5", + "98bd6bb4b257b83a22ee7b21994e753cb555e6ab", + "79f499967fb3e67dc6a510e21b9924a0e1017f16", + "4cafbd29ddd853c9494e3424ed023cb533f3d532", + "0a529bc0b2aa197273e85685c8d1ab5bf9dfddee", + "bd2dbafe6b0763cd95fa9cd61a3e558656474ba9", + "3d53852f4a4083d8d6a7092b57f26a30f6ef5b06", + "bb9746591ecb5fe57de9855fbde4f2ff965d6d36", + "8a1aaac915904efd3042da70ccab77949793ef34", + "3de7cdf23fcd0748285e2e6e32b9c728a53fa2e4", + "eba9512eaead9daacab10585185d14fe984012c7", + "4bf8add11df553a5360bbd90bd92c5a86d89ab51", + "c09ecdeefdf272389d938b7f36ca441ef06577af", + "816e65461868482340a6dda3151fb3227f725030", + "c943e6615103f9a43c14baa499aaf05734d0a231", + "40386fb3d6ec109651085785b09cd2186988715b", + "46b98996375ccc8b0315d0e65ea16bbc093662ac", + "b41b41973bbc1d98216c7929ec9be8a5454904b5", + "234c7abbff1e521558ab36a871d51017f7e85a47", + "faff07d4a4725dccfa22beb5a32122bdea18f1fd", + "81a1fb548f3ec7506ac654bd942b03ee06bd9506", + "410e73cfd112422f93477685825fb887e4ed0127", + "ed666f59cbcd21ea387f9c7c00b36b1e0a991b40", + "ee37642c60a109bf860a4faefc66c880161e9d8f", + "1736223bb6fb769e9d59f47ad3c2fe0d6185c6e0", + "28ed6a77bb7c7ec2af07704116ee4bdb68d8a015", + "85857af3357d0f8e38e242a21402f18381b389da", + "2289aba9093e460fac678cfa1d80a76c5939a32c", + "595b8146d11d1d2738fdad41788c6785de7203fa", + "28f93ee45901e4df68c3dd99f62db023c224922f", + "c5937c978cf2c7d5b581211ee96c01e2713f9d52" + ], + "remotes": [ + { + "name": "gitlab", + "url_redacted": "http://192.168.0.110:8929/root/momo-pro-system.git" + } + ], + "probe_error": "" + } + ], + "comparisons": [ + { + "left_label": "local-momo-gitea", + "right_label": "icloud-momo-gitea", + "relation": "no_shared_history", + "left_head": "61a9c4c1e3861f773d221c7efed6c5cd5ac9ef39", + "right_head": "fe6180b8ad95db0129d6aff8e7b99f2fc69784b1", + "common_commit_count": 0, + "common_commit_samples": [] + }, + { + "left_label": "local-momo-gitea", + "right_label": "local-momo-gitlab", + "relation": "no_shared_history", + "left_head": "61a9c4c1e3861f773d221c7efed6c5cd5ac9ef39", + "right_head": "5a4294e5c08d238825c47714343d483d0b0e37b0", + "common_commit_count": 0, + "common_commit_samples": [] + }, + { + "left_label": "icloud-momo-gitea", + "right_label": "local-momo-gitlab", + "relation": "no_shared_history", + "left_head": "fe6180b8ad95db0129d6aff8e7b99f2fc69784b1", + "right_head": "5a4294e5c08d238825c47714343d483d0b0e37b0", + "common_commit_count": 0, + "common_commit_samples": [] + } + ] +} diff --git a/docs/security/security-rollout-policy.snapshot.json b/docs/security/security-rollout-policy.snapshot.json new file mode 100644 index 00000000..8f3e4fa5 --- /dev/null +++ b/docs/security/security-rollout-policy.snapshot.json @@ -0,0 +1,105 @@ +{ + "schema_version": "security_rollout_policy_v1", + "status": "draft", + "default_mode": "observe", + "enforcement_level": "mirror_only", + "policy_items": [ + { + "condition": "read_only_inventory_or_evidence_mirror", + "mode": "observe", + "allowed": [ + "collect_metadata", + "write_redacted_snapshot", + "update_docs", + "mirror_to_awooop_runtime_state" + ], + "forbidden": [ + "change_runtime", + "write_to_remote_system", + "delete_or_archive_repo", + "sync_refs" + ], + "reason": "初期先建立可見性與追溯性,不阻擋產品與架構推進。" + }, + { + "condition": "low_or_medium_observation_without_irreversible_change", + "mode": "warn", + "allowed": [ + "label_risk", + "create_followup_item", + "add_evidence_ref", + "prepare_draft_plan" + ], + "forbidden": [ + "block_deploy", + "force_owner_decision", + "auto_patch", + "auto_merge" + ], + "reason": "LOW / MEDIUM observation 先累積 evidence,不把日常流程變成審批地獄。" + }, + { + "condition": "uses_readonly_token_or_admin_export", + "mode": "approve_required", + "allowed": [ + "request_human_approval", + "run_once_after_approval", + "store_token_present_boolean_only", + "write_redacted_inventory" + ], + "forbidden": [ + "store_token_value", + "reuse_write_token", + "write_to_gitea", + "create_repo" + ], + "reason": "只讀 token 與管理匯出會碰敏感邊界,需 approval,但仍不授權任何同步或寫入。" + }, + { + "condition": "repo_creation_visibility_change_or_refs_sync", + "mode": "approve_required", + "allowed": [ + "create_approval_candidate", + "prepare_migration_plan", + "prepare_rollback_plan" + ], + "forbidden": [ + "execute_without_owner_approval", + "push_refs", + "change_visibility", + "switch_primary" + ], + "reason": "這些動作會改供應鏈控制面,必須逐 repo 核准。" + }, + { + "condition": "secret_rbac_network_firewall_deploy_or_primary_switch", + "mode": "approve_required", + "allowed": [ + "create_approval_required_event", + "prepare_dry_run_plan", + "define_rollback" + ], + "forbidden": [ + "auto_execute", + "store_secret_value", + "skip_human_review" + ], + "reason": "這些動作有生產、權限或安全 blast radius,不進入初期自動化。" + }, + { + "condition": "destructive_action_without_rollback_or_secret_value_storage", + "mode": "block_candidate", + "allowed": [ + "record_block_reason", + "request_manual_exception" + ], + "forbidden": [ + "force_push", + "delete_repo", + "store_raw_secret", + "disable_audit" + ], + "reason": "不可逆且無 rollback 的動作不屬於初期框架建置範圍。" + } + ] +} diff --git a/docs/security/security-supply-chain-contract-manifest.snapshot.json b/docs/security/security-supply-chain-contract-manifest.snapshot.json new file mode 100644 index 00000000..294de058 --- /dev/null +++ b/docs/security/security-supply-chain-contract-manifest.snapshot.json @@ -0,0 +1,160 @@ +{ + "schema_version": "security_supply_chain_contract_manifest_v1", + "status": "draft", + "default_enforcement_level": "mirror_only", + "contract_count": 12, + "contracts": [ + { + "contract": "security_rollout_policy_v1", + "schema_path": "docs/schemas/security_rollout_policy_v1.schema.json", + "snapshot_paths": ["docs/security/security-rollout-policy.snapshot.json"], + "human_docs": ["docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md"], + "consumer": "AwoooP read-only policy / Operator Console", + "consumption_mode": "read_only_policy", + "allowed_actions": ["mirror_policy", "display_mode", "recommend_observe_warn_approval"], + "forbidden_actions": ["runtime_enforcement", "auto_block_low_medium_observation"], + "notes": "初期 observe-first / mirror-only,不把資安網變成流程負擔。" + }, + { + "contract": "security_finding_v1", + "schema_path": "docs/schemas/security_finding_v1.schema.json", + "snapshot_paths": [], + "human_docs": ["docs/security/KALI-SECURITY-MESH-BLUEPRINT.md"], + "consumer": "AwoooP Runtime State / Channel Event / Audit", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_redacted_finding", "display_security_posture"], + "forbidden_actions": ["active_scan", "store_raw_secret", "store_exploit_payload"], + "notes": "承接 Kali / Trivy / ZAP / Semgrep / detect-secrets 類 findings。" + }, + { + "contract": "coding_task_v1", + "schema_path": "docs/schemas/coding_task_v1.schema.json", + "snapshot_paths": [], + "human_docs": ["docs/security/CODEX-PATCH-ONLY-HANDOFF-PROMPT.md"], + "consumer": "AwoooP Approval candidate / Codex patch-only handoff", + "consumption_mode": "suggest_only", + "allowed_actions": ["create_patch_backlog", "request_reviewers", "open_draft_plan"], + "forbidden_actions": ["auto_merge", "production_deploy", "secret_rotation"], + "notes": "Code Review 後需要 coding 的工作只能進 patch-only / draft PR lane。" + }, + { + "contract": "source_control_migration_event_v1", + "schema_path": "docs/schemas/source_control_migration_event_v1.schema.json", + "snapshot_paths": [ + "docs/security/gitea-github-awoooi-inventory.snapshot.json", + "docs/security/source-control-clawbot-v5.snapshot.json", + "docs/security/source-control-wooo-aiops.snapshot.json" + ], + "human_docs": [ + "docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md", + "docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md" + ], + "consumer": "AwoooP migration matrix evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_diff_summary", "display_blocking_reason"], + "forbidden_actions": ["sync_refs", "switch_github_primary", "delete_gitea_repo"], + "notes": "目前 mapped repos 仍 blocked,不可切 primary。" + }, + { + "contract": "gitea_repo_inventory_v1", + "schema_path": "docs/schemas/gitea_repo_inventory_v1.schema.json", + "snapshot_paths": [ + "docs/security/gitea-repo-inventory.snapshot.json", + "docs/security/gitea-public-repo-search.snapshot.json", + "docs/security/gitea-org-repo-inventory-blocked.snapshot.json" + ], + "human_docs": [ + "docs/security/GITEA-SERVER-SIDE-INVENTORY-RUNBOOK.md", + "docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md" + ], + "consumer": "AwoooP migration matrix evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_public_only_inventory", "create_readonly_inventory_approval_candidate"], + "forbidden_actions": ["store_token_value", "write_to_gitea", "delete_or_archive_repo"], + "notes": "目前是 partial/public_only,private/internal 全量需批准後補齊。" + }, + { + "contract": "local_git_remote_inventory_v1", + "schema_path": "docs/schemas/local_git_remote_inventory_v1.schema.json", + "snapshot_paths": ["docs/security/local-git-remote-inventory.snapshot.json"], + "human_docs": ["docs/security/LOCAL-GIT-REMOTE-INVENTORY-SNAPSHOT.md"], + "consumer": "AwoooP source-control coverage evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_remote_coverage", "display_internal_110_risk"], + "forbidden_actions": ["modify_remote", "treat_as_server_full_inventory"], + "notes": "本機可見 working tree 只能作輔助 evidence。" + }, + { + "contract": "github_target_probe_v1", + "schema_path": "docs/schemas/github_target_probe_v1.schema.json", + "snapshot_paths": ["docs/security/github-target-probe.snapshot.json"], + "human_docs": ["docs/security/GITHUB-TARGET-PROBE-SNAPSHOT.md"], + "consumer": "AwoooP migration target evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_target_visibility", "display_not_found_or_private"], + "forbidden_actions": ["auto_create_repo", "assume_not_found_means_absent"], + "notes": "not_found_or_private 只代表未授權 probe 看不到。" + }, + { + "contract": "github_target_decision_v1", + "schema_path": "docs/schemas/github_target_decision_v1.schema.json", + "snapshot_paths": ["docs/security/github-target-decision.snapshot.json"], + "human_docs": ["docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md"], + "consumer": "AwoooP approval candidate / migration target evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_target_decision", "create_approval_candidate"], + "forbidden_actions": ["change_visibility", "create_repo", "sync_refs"], + "notes": "8 個 targets 中 7 個需要人工批准。" + }, + { + "contract": "github_target_repo_approval_package_v1", + "schema_path": "docs/schemas/github_target_repo_approval_package_v1.schema.json", + "snapshot_paths": ["docs/security/github-target-repo-approval-package.snapshot.json"], + "human_docs": ["docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md"], + "consumer": "AwoooP approval queue draft", + "consumption_mode": "approval_only", + "allowed_actions": ["display_repo_approval_queue", "request_owner_decision"], + "forbidden_actions": ["execute_approval_item", "push_refs", "change_visibility"], + "notes": "7 個 pending packages,逐 repo 低摩擦批准。" + }, + { + "contract": "local_repo_canonical_probe_v1", + "schema_path": "docs/schemas/local_repo_canonical_probe_v1.schema.json", + "snapshot_paths": ["docs/security/local-repo-canonical-ewoooc-momo.snapshot.json"], + "human_docs": ["docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md"], + "consumer": "AwoooP canonical decision evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_lineage_summary", "display_unrelated_warning"], + "forbidden_actions": ["auto_merge_histories", "delete_working_tree"], + "notes": "momo/ewoooc 目前 sample 無共同 commit,不可自動合併。" + }, + { + "contract": "git_remote_refs_probe_v1", + "schema_path": "docs/schemas/git_remote_refs_probe_v1.schema.json", + "snapshot_paths": [ + "docs/security/git-remote-refs-bitan-tsenyang.snapshot.json", + "docs/security/git-remote-refs-wooo-infra-config.snapshot.json" + ], + "human_docs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md" + ], + "consumer": "AwoooP source readiness evidence", + "consumption_mode": "mirror_only", + "allowed_actions": ["mirror_refs_readiness", "display_unreachable_remote"], + "forbidden_actions": ["fetch", "push", "sync_refs"], + "notes": "只做 ls-remote 類 read-only refs evidence。" + }, + { + "contract": "approval_required_event_v1", + "schema_path": "docs/schemas/approval_required_event_v1.schema.json", + "snapshot_paths": ["docs/security/gitea-readonly-inventory-approval.snapshot.json"], + "human_docs": ["docs/security/GITEA-READONLY-INVENTORY-APPROVAL-PACKAGE.md"], + "consumer": "AwoooP approval queue / Audit", + "consumption_mode": "approval_only", + "allowed_actions": ["display_approval_candidate", "record_human_decision"], + "forbidden_actions": ["auto_approve", "store_token_value", "execute_without_approval"], + "notes": "高風險或敏感邊界的唯一升級入口。" + } + ] +} diff --git a/docs/security/source-control-clawbot-v5.snapshot.json b/docs/security/source-control-clawbot-v5.snapshot.json new file mode 100644 index 00000000..384e7624 --- /dev/null +++ b/docs/security/source-control-clawbot-v5.snapshot.json @@ -0,0 +1,19 @@ +{ + "schema_version": "source_control_migration_event_v1", + "gitea_repo": "wooo/clawbot-v5", + "github_repo": "owenhytsai/clawbot-v5", + "branch_count_gitea": 1, + "branch_count_github": 1, + "tag_count_gitea": 1, + "tag_count_github": 0, + "latest_sha_gitea": "22074fbe4d6ec6c11c86f76139eea55756d1d160", + "latest_sha_github": "7a769de46450087f9d6a8ef0d2ac23ed15565d2c", + "workflows_mapped": false, + "webhooks_mapped": false, + "secrets_inventory_only": true, + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致", + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md" + ] +} diff --git a/docs/security/source-control-wooo-aiops.snapshot.json b/docs/security/source-control-wooo-aiops.snapshot.json new file mode 100644 index 00000000..4fb6c302 --- /dev/null +++ b/docs/security/source-control-wooo-aiops.snapshot.json @@ -0,0 +1,19 @@ +{ + "schema_version": "source_control_migration_event_v1", + "gitea_repo": "wooo/wooo-aiops", + "github_repo": "owenhytsai/wooo-aiops", + "branch_count_gitea": 2, + "branch_count_github": 3, + "tag_count_gitea": 0, + "tag_count_github": 19, + "latest_sha_gitea": "507384a2e1943f4183942bf17d7b52e223067853", + "latest_sha_github": "7c7aa109d93da6d75d687d6ee5131151afee37e8", + "workflows_mapped": false, + "webhooks_mapped": false, + "secrets_inventory_only": true, + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致", + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md" + ] +} diff --git a/scripts/security/git-remote-refs-probe.py b/scripts/security/git-remote-refs-probe.py new file mode 100644 index 00000000..efb976c7 --- /dev/null +++ b/scripts/security/git-remote-refs-probe.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Git remote refs 只讀探測工具。 + +此工具用 `git ls-remote --heads --tags` 讀取指定本機 repo 的 remote refs, +並比對本機 HEAD / branch。它不 fetch、不 clone、不 push,也不修改 remote。 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + + +def redact_url(value: str) -> str: + if "://" not in value: + if "@" in value and ":" in value.split("@", 1)[1]: + return value.split("@", 1)[1] + return value + parts = urlsplit(value) + netloc = parts.netloc.split("@", 1)[-1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + + +def run_git(repo: Path, args: list[str], timeout: int) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run( + ["git", *args], + cwd=repo, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(["git", *args], 124, "", "git command timeout") + + +def git_value(repo: Path, args: list[str], timeout: int) -> str: + result = run_git(repo, args, timeout) + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def parse_refs(output: str) -> tuple[dict[str, str], dict[str, str]]: + heads: dict[str, str] = {} + tags: dict[str, str] = {} + for line in output.splitlines(): + parts = line.split() + if len(parts) != 2: + continue + sha, ref = parts + if ref.startswith("refs/heads/"): + heads[ref.removeprefix("refs/heads/")] = sha + elif ref.startswith("refs/tags/"): + tag = ref.removeprefix("refs/tags/").removesuffix("^{}") + tags[tag] = sha + return heads, tags + + +def parse_repo_arg(value: str) -> tuple[str, Path, str]: + parts = value.split("=") + if len(parts) not in (2, 3): + raise argparse.ArgumentTypeError("--repo 必須是 label=/absolute/path 或 label=/absolute/path=remote") + label = parts[0].strip() + path = Path(parts[1].strip()).expanduser().resolve() + remote = parts[2].strip() if len(parts) == 3 else "origin" + if not label or not str(path) or not remote: + raise argparse.ArgumentTypeError("--repo label/path/remote 不可為空") + return label, path, remote + + +def probe_repo(label: str, repo: Path, remote: str, timeout: int) -> dict[str, object]: + local_head = git_value(repo, ["rev-parse", "HEAD"], timeout) + local_branch = git_value(repo, ["branch", "--show-current"], timeout) + remote_url = git_value(repo, ["remote", "get-url", remote], timeout) + ls_remote = run_git(repo, ["ls-remote", "--heads", "--tags", remote], timeout) + if ls_remote.returncode == 0: + heads, tags = parse_refs(ls_remote.stdout) + remote_branch_sha = heads.get(local_branch, "") + if local_branch and remote_branch_sha and remote_branch_sha == local_head: + status = "aligned_current_branch" + elif heads: + status = "reachable_drift_or_unknown" + else: + status = "reachable_no_heads" + error_summary = "" + else: + heads = {} + tags = {} + remote_branch_sha = "" + status = "unreachable" + stderr = ls_remote.stderr.strip() + if "Permission denied" in stderr: + error_summary = "SSH 權限不足或 remote 不可讀" + elif "Repository not found" in stderr: + error_summary = "remote repo not found 或未授權" + else: + tail = (stderr or ls_remote.stdout.strip()).splitlines()[-1:] + error_summary = tail[0] if tail else "git ls-remote failed" + + return { + "label": label, + "repo_path": str(repo), + "remote": remote, + "remote_url_redacted": redact_url(remote_url), + "status": status, + "local_branch": local_branch, + "local_head": local_head, + "remote_current_branch_sha": remote_branch_sha, + "head_count": len(heads), + "tag_count": len(tags), + "heads": [{"name": name, "sha": sha} for name, sha in sorted(heads.items())], + "tags": [{"name": name, "sha": sha} for name, sha in sorted(tags.items())], + "error_summary": error_summary, + } + + +def build_payload(group_name: str, repo_args: list[tuple[str, Path, str]], timeout: int) -> dict[str, object]: + repos = [probe_repo(label, path, remote, timeout) for label, path, remote in repo_args] + aligned = sum(1 for repo in repos if repo["status"] == "aligned_current_branch") + unreachable = sum(1 for repo in repos if repo["status"] == "unreachable") + return { + "schema_version": "git_remote_refs_probe_v1", + "group_name": group_name, + "status": "ok" if unreachable == 0 else "partial", + "repo_count": len(repos), + "aligned_current_branch_count": aligned, + "unreachable_count": unreachable, + "repos": repos, + } + + +def write_markdown(payload: dict[str, object], path: Path) -> None: + lines = [ + "# Git Remote Refs Probe 快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 群組 | `{payload['group_name']}` |", + f"| 狀態 | `{payload['status']}` |", + f"| repo 數 | `{payload['repo_count']}` |", + f"| aligned current branch | `{payload['aligned_current_branch_count']}` |", + f"| unreachable | `{payload['unreachable_count']}` |", + "", + "## Repo refs", + "", + "| Label | Remote URL | Status | Local branch | Local HEAD | Remote branch SHA | Heads | Tags | Error |", + "|-------|------------|--------|--------------|------------|-------------------|-------|------|-------|", + ] + for repo in payload.get("repos", []): + if not isinstance(repo, dict): + continue + lines.append( + "| " + + " | ".join( + [ + f"`{repo.get('label', '')}`", + f"`{repo.get('remote_url_redacted', '')}`", + f"`{repo.get('status', '')}`", + f"`{repo.get('local_branch', '')}`", + f"`{str(repo.get('local_head', ''))[:7]}`", + f"`{str(repo.get('remote_current_branch_sha', ''))[:7]}`", + f"`{repo.get('head_count', 0)}`", + f"`{repo.get('tag_count', 0)}`", + str(repo.get("error_summary", "") or "無"), + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> 注意:本檔只使用 `git ls-remote` 做 read-only refs 探測;未 fetch、未 clone、未 push。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--group-name", required=True) + parser.add_argument("--repo", action="append", type=parse_repo_arg, required=True) + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + parser.add_argument("--timeout", type=int, default=10) + args = parser.parse_args() + + payload = build_payload(args.group_name, args.repo, args.timeout) + Path(args.output_json).write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(payload, Path(args.output_md)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/gitea-repo-inventory.py b/scripts/security/gitea-repo-inventory.py new file mode 100644 index 00000000..7f35349c --- /dev/null +++ b/scripts/security/gitea-repo-inventory.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Read-only Gitea repo 全量盤點工具。 + +此工具可查詢 Gitea org/user API endpoint,或吃管理介面匯出的 JSON。 +執行期間不寫入 Gitea,也不會把 API token 寫入輸出檔。 +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request +from pathlib import Path +from urllib.parse import quote, urlencode, urlsplit, urlunsplit + + +def redact_url(value: str) -> str: + if "://" not in value: + if "@" in value and ":" in value.split("@", 1)[1]: + return value.split("@", 1)[1] + return value + parts = urlsplit(value) + netloc = parts.netloc.split("@", 1)[-1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + + +def api_get_json(url: str, token: str | None, timeout: int) -> tuple[int, object]: + headers = { + "Accept": "application/json", + "User-Agent": "awoooi-security-inventory/1.0", + } + if token: + headers["Authorization"] = f"token {token}" + request = urllib.request.Request(url, headers=headers, method="GET") + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + body = response.read().decode("utf-8") + return response.status, json.loads(body) + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", errors="replace") + try: + payload: object = json.loads(body) + except json.JSONDecodeError: + payload = {"message": body.strip()} + return exc.code, payload + + +def repo_summary(repo: dict[str, object], github_owner: str | None) -> dict[str, object]: + owner = repo.get("owner") if isinstance(repo.get("owner"), dict) else {} + owner_name = owner.get("login") if isinstance(owner, dict) else None + raw_full_name = str(repo.get("full_name") or "") + name = str(repo.get("name") or raw_full_name.rsplit("/", 1)[-1] or "") + full_name = raw_full_name or (f"{owner_name}/{name}" if owner_name else name) + clone_url = str(repo.get("clone_url") or repo.get("html_url") or "") + ssh_url = str(repo.get("ssh_url") or "") + github_repo = f"{github_owner}/{name}" if github_owner and name else "" + return { + "gitea_repo": full_name, + "name": name, + "owner": owner_name or "", + "private": bool(repo.get("private", False)), + "empty": bool(repo.get("empty", False)), + "archived": bool(repo.get("archived", False)), + "default_branch": str(repo.get("default_branch") or ""), + "clone_url_redacted": redact_url(clone_url), + "ssh_url_redacted": redact_url(ssh_url), + "github_repo_candidate": github_repo, + } + + +def load_export(path: Path) -> object: + return json.loads(path.read_text(encoding="utf-8")) + + +def extract_repos(payload: object) -> list[dict[str, object]]: + if isinstance(payload, list): + return [item for item in payload if isinstance(item, dict)] + if isinstance(payload, dict): + for key in ("data", "repos", "repositories"): + value = payload.get(key) + if isinstance(value, list): + return [item for item in value if isinstance(item, dict)] + return [] + + +def build_inventory( + *, + base_url: str, + org: str, + github_owner: str | None, + token_present: bool, + http_status: int | None, + payload: object, + query_mode: str, + query: str, +) -> dict[str, object]: + repos = [repo_summary(repo, github_owner) for repo in extract_repos(payload)] + if query_mode == "export": + visibility_scope = "admin_export" + elif token_present: + visibility_scope = "authenticated" + else: + visibility_scope = "public_only" + + if repos and visibility_scope in ("authenticated", "admin_export"): + status = "ok" + elif repos: + status = "partial" + else: + status = "blocked" + blocking_reason = "" + if repos and status == "partial": + blocking_reason = "未提供 token,結果只代表公開可見 repo;private/internal repos 仍需只讀 token 或管理匯出" + elif not repos: + if http_status in (401, 403): + blocking_reason = "Gitea API 需要只讀 token 或權限不足" + elif query_mode == "search" and http_status == 200: + blocking_reason = "Gitea public repo search 未回傳 repo,可能沒有公開 repo 或需要只讀 token" + elif http_status == 404: + blocking_reason = "Gitea API 查無 org/user repos,需確認 org 名稱或使用管理匯出" + elif http_status is None: + blocking_reason = "匯入檔案沒有可解析的 repo list" + else: + blocking_reason = f"Gitea API 回應無 repo list,HTTP {http_status}" + + return { + "schema_version": "gitea_repo_inventory_v1", + "base_url": redact_url(base_url.rstrip("/")), + "org": org, + "github_owner": github_owner or "", + "query_mode": query_mode, + "query": query, + "visibility_scope": visibility_scope, + "token_present": token_present, + "http_status": http_status, + "status": status, + "blocking_reason": blocking_reason, + "repo_count": len(repos), + "repos": repos, + } + + +def write_markdown(inventory: dict[str, object], path: Path) -> None: + lines = [ + "# Gitea Repo 全量盤點快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 狀態 | `{inventory['status']}` |", + f"| Gitea base URL | `{inventory['base_url']}` |", + f"| Org/User | `{inventory['org']}` |", + f"| GitHub owner 候選 | `{inventory['github_owner']}` |", + f"| 查詢模式 | `{inventory.get('query_mode', '')}` |", + f"| 查詢字串 | `{inventory.get('query', '')}` |", + f"| 可見性範圍 | `{inventory.get('visibility_scope', '')}` |", + f"| 是否提供 token | `{inventory['token_present']}` |", + f"| HTTP status | `{inventory['http_status']}` |", + f"| Repo 數量 | `{inventory['repo_count']}` |", + f"| 阻塞原因 | {inventory['blocking_reason'] or '無'} |", + "", + "## Repo 清單", + "", + "| Gitea repo | GitHub 候選 | default branch | private | archived |", + "|------------|------------------|----------------|---------|----------|", + ] + repos = inventory.get("repos") + if isinstance(repos, list): + for repo in repos: + if not isinstance(repo, dict): + continue + lines.append( + "| " + + " | ".join( + [ + f"`{repo.get('gitea_repo', '')}`", + f"`{repo.get('github_repo_candidate', '')}`", + f"`{repo.get('default_branch', '')}`", + f"`{repo.get('private', '')}`", + f"`{repo.get('archived', '')}`", + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> 注意:本檔由 read-only Gitea inventory 工具產生,不包含 API token 或 remote URL 帳密。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--base-url", default="http://192.168.0.110:3001") + parser.add_argument("--org", default="wooo") + parser.add_argument("--scope", choices=["org", "user", "search"], default="org") + parser.add_argument("--query", default="") + parser.add_argument("--limit", type=int, default=50) + parser.add_argument("--github-owner", default="") + parser.add_argument("--token-env", default="GITEA_READONLY_TOKEN") + parser.add_argument("--input-json") + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + parser.add_argument("--timeout", type=int, default=5) + args = parser.parse_args() + + token = os.environ.get(args.token_env) + http_status: int | None = None + if args.input_json: + payload = load_export(Path(args.input_json)) + query_mode = "export" + query = "input-json" + else: + if args.scope == "search": + params = {"limit": str(args.limit)} + if args.query: + params["q"] = args.query + url = f"{args.base_url.rstrip('/')}/api/v1/repos/search?{urlencode(params)}" + else: + quoted = quote(args.org, safe="") + prefix = "orgs" if args.scope == "org" else "users" + url = f"{args.base_url.rstrip('/')}/api/v1/{prefix}/{quoted}/repos" + http_status, payload = api_get_json(url, token, args.timeout) + query_mode = args.scope + query = args.query + + inventory = build_inventory( + base_url=args.base_url, + org=args.org, + github_owner=args.github_owner or None, + token_present=bool(token), + http_status=http_status, + payload=payload, + query_mode=query_mode, + query=query, + ) + Path(args.output_json).write_text( + json.dumps(inventory, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(inventory, Path(args.output_md)) + + if inventory["status"] == "blocked": + print(inventory["blocking_reason"], file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/github-target-probe.py b/scripts/security/github-target-probe.py new file mode 100644 index 00000000..1580732f --- /dev/null +++ b/scripts/security/github-target-probe.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""GitHub target repo 只讀存在性探測。 + +此工具使用 `git ls-remote --heads` 檢查候選 GitHub repo 是否可讀。 +它不 clone、不 fetch、不 push,也不寫入任何 remote。 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path + + +def probe_candidate(candidate: str, timeout: int) -> dict[str, object]: + url = f"https://github.com/{candidate}.git" + try: + result = subprocess.run( + ["git", "ls-remote", "--heads", url], + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + return { + "github_repo": candidate, + "url_redacted": url, + "status": "timeout", + "head_count": 0, + "heads": [], + "error_summary": "git ls-remote timeout", + } + + heads = [] + if result.returncode == 0: + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) != 2 or not parts[1].startswith("refs/heads/"): + continue + heads.append( + { + "name": parts[1].removeprefix("refs/heads/"), + "sha": parts[0], + } + ) + status = "exists" if heads else "exists_empty_or_no_heads" + error_summary = "" + else: + stderr = result.stderr.strip() + if "Repository not found" in stderr: + status = "not_found_or_private" + error_summary = "GitHub 回應 repository not found;可能未建立或為 private 且未授權" + else: + status = "error" + error_summary = stderr.splitlines()[-1] if stderr else "git ls-remote failed" + + return { + "github_repo": candidate, + "url_redacted": url, + "status": status, + "head_count": len(heads), + "heads": heads, + "error_summary": error_summary, + } + + +def write_markdown(payload: dict[str, object], path: Path) -> None: + lines = [ + "# GitHub Target Probe 快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 狀態 | `{payload['status']}` |", + f"| 候選 repo 數 | `{payload['candidate_count']}` |", + f"| exists | `{payload['exists_count']}` |", + f"| not found or private | `{payload['not_found_or_private_count']}` |", + "", + "## 候選 Repo", + "", + "| GitHub repo | status | heads | error |", + "|-------------|--------|-------|-------|", + ] + for candidate in payload.get("candidates", []): + if not isinstance(candidate, dict): + continue + lines.append( + "| " + + " | ".join( + [ + f"`{candidate.get('github_repo', '')}`", + f"`{candidate.get('status', '')}`", + f"`{candidate.get('head_count', 0)}`", + str(candidate.get("error_summary", "") or "無"), + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> 注意:`not_found_or_private` 只代表未授權 read-only probe 看不到,不等同確認不存在。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--candidate", action="append", required=True) + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + parser.add_argument("--timeout", type=int, default=10) + args = parser.parse_args() + + candidates = [probe_candidate(candidate, args.timeout) for candidate in args.candidate] + exists_count = sum(1 for item in candidates if item["status"].startswith("exists")) + not_found_count = sum(1 for item in candidates if item["status"] == "not_found_or_private") + payload = { + "schema_version": "github_target_probe_v1", + "status": "ok", + "candidate_count": len(candidates), + "exists_count": exists_count, + "not_found_or_private_count": not_found_count, + "candidates": candidates, + } + Path(args.output_json).write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(payload, Path(args.output_md)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/local-git-remote-inventory.py b/scripts/security/local-git-remote-inventory.py new file mode 100644 index 00000000..8331e25b --- /dev/null +++ b/scripts/security/local-git-remote-inventory.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +"""本機 Git remote 只讀盤點工具。 + +此工具掃描指定 root 底下可見的 Git working tree,讀取 `.git/config` +中的 remote URL,並在輸出前移除 URL 內的帳密。它不會 fetch、push、 +修改 remote,也不會連線到 GitHub 或 Gitea。 +""" + +from __future__ import annotations + +import argparse +import configparser +import json +import os +import sys +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + + +DEFAULT_EXCLUDE_NAMES = { + ".cache", + ".cargo", + ".claude", + ".codex", + ".gemini", + ".git", + ".gradle", + ".npm", + ".nvm", + ".openclaw", + ".pyenv", + ".rustup", + ".Trash", + ".venv", + "__pycache__", + "Applications", + "Applications (Parallels)", + "Caches", + "DerivedData", + "Library", + "Movies", + "Music", + "node_modules", + "Parallels", + "Pictures", + "venv", +} + + +def redact_url(value: str) -> str: + if "://" not in value: + if "@" in value and ":" in value.split("@", 1)[1]: + return value.split("@", 1)[1] + return value + parts = urlsplit(value) + netloc = parts.netloc.split("@", 1)[-1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + + +def repo_slug_from_url(value: str) -> str: + redacted = redact_url(value).removesuffix("/") + if "://" in redacted: + path = urlsplit(redacted).path.strip("/") + elif ":" in redacted: + path = redacted.split(":", 1)[1].strip("/") + else: + path = redacted.strip("/") + return path.removesuffix(".git") + + +def classify_remote(url: str, gitea_fragment: str) -> str: + lowered = url.lower() + if gitea_fragment.lower() in lowered: + return "gitea" + if "github.com" in lowered: + return "github" + if "192.168.0.110:8929" in lowered: + return "gitlab_110" + if "192.168.0.110" in lowered: + return "internal_git_110" + return "other" + + +def git_config_path(repo_path: Path) -> Path | None: + git_path = repo_path / ".git" + if git_path.is_dir(): + config_path = git_path / "config" + return config_path if config_path.exists() else None + if not git_path.is_file(): + return None + + text = git_path.read_text(encoding="utf-8", errors="replace") + for line in text.splitlines(): + if line.startswith("gitdir:"): + raw_gitdir = line.split(":", 1)[1].strip() + gitdir = Path(raw_gitdir) + if not gitdir.is_absolute(): + gitdir = (repo_path / gitdir).resolve() + config_path = gitdir / "config" + return config_path if config_path.exists() else None + return None + + +def remote_name(section: str) -> str | None: + prefix = 'remote "' + if section.startswith(prefix) and section.endswith('"'): + return section[len(prefix) : -1] + return None + + +def read_remotes(repo_path: Path, gitea_fragment: str) -> list[dict[str, str]]: + config_path = git_config_path(repo_path) + if config_path is None: + return [] + + parser = configparser.RawConfigParser(strict=False) + parser.read(config_path, encoding="utf-8") + remotes: list[dict[str, str]] = [] + for section in parser.sections(): + name = remote_name(section) + if not name or not parser.has_option(section, "url"): + continue + raw_url = parser.get(section, "url").strip() + redacted_url = redact_url(raw_url) + remotes.append( + { + "name": name, + "kind": classify_remote(redacted_url, gitea_fragment), + "url_redacted": redacted_url, + "repo_slug": repo_slug_from_url(redacted_url), + } + ) + return remotes + + +def should_skip_dir(path: Path, root: Path, max_depth: int, exclude_names: set[str]) -> bool: + if path.name in exclude_names: + return True + try: + depth = len(path.relative_to(root).parts) + except ValueError: + return True + return depth > max_depth + + +def find_repos(roots: list[Path], max_depth: int, exclude_names: set[str]) -> list[Path]: + repos: dict[str, Path] = {} + for root in roots: + if not root.exists(): + continue + for current, dirs, _files in os.walk(root): + current_path = Path(current) + if should_skip_dir(current_path, root, max_depth, exclude_names): + dirs[:] = [] + continue + if (current_path / ".git").exists(): + repos[str(current_path.resolve())] = current_path.resolve() + dirs[:] = [] + continue + dirs[:] = [ + name + for name in dirs + if not should_skip_dir(current_path / name, root, max_depth, exclude_names) + ] + return sorted(repos.values(), key=lambda path: str(path)) + + +def summarize_repo(repo_path: Path, remotes: list[dict[str, str]]) -> dict[str, object]: + gitea = [remote["repo_slug"] for remote in remotes if remote["kind"] == "gitea"] + github = [remote["repo_slug"] for remote in remotes if remote["kind"] == "github"] + internal_110 = [ + remote["repo_slug"] + for remote in remotes + if remote["kind"] in ("internal_git_110", "gitlab_110") + ] + if gitea and github: + status = "mapped" + elif gitea: + status = "gitea_only_local" + elif github: + status = "github_only_local" + elif internal_110: + status = "internal_110_only" + else: + status = "other_remote" + return { + "repo_path": str(repo_path), + "repo_name": repo_path.name, + "status": status, + "gitea_repos": sorted(set(gitea)), + "github_repos": sorted(set(github)), + "internal_110_repos": sorted(set(internal_110)), + "remotes": remotes, + } + + +def build_inventory( + roots: list[Path], + max_depth: int, + exclude_names: set[str], + gitea_fragment: str, +) -> dict[str, object]: + repo_paths = find_repos(roots, max_depth, exclude_names) + repos = [ + summarize_repo(repo_path, read_remotes(repo_path, gitea_fragment)) + for repo_path in repo_paths + ] + gitea_linked = [repo for repo in repos if repo["gitea_repos"]] + github_linked = [repo for repo in repos if repo["github_repos"]] + mapped = [repo for repo in repos if repo["status"] == "mapped"] + gitea_only = [repo for repo in repos if repo["status"] == "gitea_only_local"] + github_only = [repo for repo in repos if repo["status"] == "github_only_local"] + internal_110 = [repo for repo in repos if repo["status"] == "internal_110_only"] + unique_gitea = sorted( + { + item + for repo in repos + for item in repo.get("gitea_repos", []) + if isinstance(item, str) + } + ) + unique_github = sorted( + { + item + for repo in repos + for item in repo.get("github_repos", []) + if isinstance(item, str) + } + ) + unique_internal_110 = sorted( + { + item + for repo in repos + for item in repo.get("internal_110_repos", []) + if isinstance(item, str) + } + ) + return { + "schema_version": "local_git_remote_inventory_v1", + "status": "partial" if repos else "empty", + "roots": [str(root) for root in roots], + "max_depth": max_depth, + "gitea_host_fragment": gitea_fragment, + "repo_count": len(repos), + "gitea_linked_count": len(gitea_linked), + "github_linked_count": len(github_linked), + "mapped_count": len(mapped), + "gitea_only_count": len(gitea_only), + "github_only_count": len(github_only), + "internal_110_only_count": len(internal_110), + "unique_gitea_repo_count": len(unique_gitea), + "unique_github_repo_count": len(unique_github), + "unique_internal_110_repo_count": len(unique_internal_110), + "unique_gitea_repos": unique_gitea, + "unique_github_repos": unique_github, + "unique_internal_110_repos": unique_internal_110, + "repos": repos, + } + + +def write_markdown(inventory: dict[str, object], path: Path) -> None: + lines = [ + "# 本機 Git Remote 盤點快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 狀態 | `{inventory['status']}` |", + f"| 掃描 root | `{', '.join(inventory['roots'])}` |", + f"| max depth | `{inventory['max_depth']}` |", + f"| Gitea host fragment | `{inventory['gitea_host_fragment']}` |", + f"| repo 數量 | `{inventory['repo_count']}` |", + f"| Gitea linked | `{inventory['gitea_linked_count']}` |", + f"| GitHub linked | `{inventory['github_linked_count']}` |", + f"| mapped | `{inventory['mapped_count']}` |", + f"| Gitea-only local | `{inventory['gitea_only_count']}` |", + f"| GitHub-only local | `{inventory['github_only_count']}` |", + f"| Internal 110-only local | `{inventory['internal_110_only_count']}` |", + f"| 去重後 Gitea repo | `{inventory['unique_gitea_repo_count']}` |", + f"| 去重後 GitHub repo | `{inventory['unique_github_repo_count']}` |", + f"| 去重後 110 內部 repo | `{inventory['unique_internal_110_repo_count']}` |", + "", + "## Repo 對照", + "", + "| 狀態 | 本機路徑 | Gitea repo | GitHub repo | 110 內部 remote |", + "|------|----------|------------|-------------|----------------|", + ] + repos = inventory.get("repos") + if isinstance(repos, list): + for repo in repos: + if not isinstance(repo, dict): + continue + gitea = ", ".join(f"`{item}`" for item in repo.get("gitea_repos", [])) or "-" + github = ", ".join(f"`{item}`" for item in repo.get("github_repos", [])) or "-" + internal_110 = ( + ", ".join(f"`{item}`" for item in repo.get("internal_110_repos", [])) or "-" + ) + lines.append( + "| " + + " | ".join( + [ + f"`{repo.get('status', '')}`", + f"`{repo.get('repo_path', '')}`", + gitea, + github, + internal_110, + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> 注意:本檔只代表本機指定 roots 可見的 Git working tree,不等同 Gitea server 全量 repo 清單。", + "> 輸出前已移除 remote URL 中的 username、password、token。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", action="append", required=True) + parser.add_argument("--max-depth", type=int, default=4) + parser.add_argument("--exclude-name", action="append", default=[]) + parser.add_argument("--gitea-host-fragment", default="192.168.0.110:3001") + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + args = parser.parse_args() + + roots = [Path(root).expanduser().resolve() for root in args.root] + exclude_names = set(DEFAULT_EXCLUDE_NAMES) + exclude_names.update(args.exclude_name) + inventory = build_inventory( + roots=roots, + max_depth=args.max_depth, + exclude_names=exclude_names, + gitea_fragment=args.gitea_host_fragment, + ) + Path(args.output_json).write_text( + json.dumps(inventory, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(inventory, Path(args.output_md)) + if inventory["status"] == "empty": + print("沒有找到本機 Git working tree", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/local-repo-canonical-probe.py b/scripts/security/local-repo-canonical-probe.py new file mode 100644 index 00000000..1afbe3ba --- /dev/null +++ b/scripts/security/local-repo-canonical-probe.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +"""本機 repo canonical lineage 只讀探測。 + +此工具比較多個本機 Git working tree 的 HEAD、branch、remote 與近期 +commit ancestry,協助判斷它們是否可能屬於同一個 canonical repo。 +它不 fetch、不push、不修改 remote,也不讀取 commit message。 +""" + +from __future__ import annotations + +import argparse +import configparser +import json +import subprocess +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + + +def redact_url(value: str) -> str: + if "://" not in value: + if "@" in value and ":" in value.split("@", 1)[1]: + return value.split("@", 1)[1] + return value + parts = urlsplit(value) + netloc = parts.netloc.split("@", 1)[-1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + + +def run_git(repo: Path, args: list[str], timeout: int) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run( + ["git", *args], + cwd=repo, + check=False, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(["git", *args], 124, "", "git command timeout") + + +def git_value(repo: Path, args: list[str], timeout: int) -> str: + result = run_git(repo, args, timeout) + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def git_config_path(repo_path: Path) -> Path | None: + git_path = repo_path / ".git" + if git_path.is_dir(): + config_path = git_path / "config" + return config_path if config_path.exists() else None + if not git_path.is_file(): + return None + text = git_path.read_text(encoding="utf-8", errors="replace") + for line in text.splitlines(): + if line.startswith("gitdir:"): + raw_gitdir = line.split(":", 1)[1].strip() + gitdir = Path(raw_gitdir) + if not gitdir.is_absolute(): + gitdir = (repo_path / gitdir).resolve() + config_path = gitdir / "config" + return config_path if config_path.exists() else None + return None + + +def remote_name(section: str) -> str | None: + prefix = 'remote "' + if section.startswith(prefix) and section.endswith('"'): + return section[len(prefix) : -1] + return None + + +def read_remotes(repo_path: Path) -> list[dict[str, str]]: + config_path = git_config_path(repo_path) + if config_path is None: + return [] + parser = configparser.RawConfigParser(strict=False) + parser.read(config_path, encoding="utf-8") + remotes: list[dict[str, str]] = [] + for section in parser.sections(): + name = remote_name(section) + if not name or not parser.has_option(section, "url"): + continue + remotes.append( + { + "name": name, + "url_redacted": redact_url(parser.get(section, "url").strip()), + } + ) + return remotes + + +def parse_repo_arg(value: str) -> tuple[str, Path]: + if "=" not in value: + raise argparse.ArgumentTypeError("--repo 必須是 label=/absolute/path") + label, raw_path = value.split("=", 1) + if not label.strip() or not raw_path.strip(): + raise argparse.ArgumentTypeError("--repo label 與 path 不可為空") + return label.strip(), Path(raw_path).expanduser().resolve() + + +def repo_summary(label: str, repo_path: Path, sample_limit: int, git_timeout: int) -> dict[str, object]: + exists = (repo_path / ".git").exists() + if not exists: + return { + "label": label, + "repo_path": str(repo_path), + "exists": False, + "head_sha": "", + "head_short": "", + "branch": "", + "commit_sample_count": 0, + "commits": [], + "remotes": [], + "probe_error": "repo missing", + } + + probe_errors: list[str] = [] + head_result = run_git(repo_path, ["rev-parse", "HEAD"], git_timeout) + head_sha = head_result.stdout.strip() if head_result.returncode == 0 else "" + if head_result.returncode != 0: + probe_errors.append("HEAD 讀取失敗或逾時") + + branch = git_value(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"], git_timeout) + rev_list_result = run_git(repo_path, ["rev-list", f"--max-count={sample_limit}", "HEAD"], git_timeout) + commits = rev_list_result.stdout.splitlines() if rev_list_result.returncode == 0 else [] + if rev_list_result.returncode != 0: + probe_errors.append("rev-list 讀取失敗或逾時") + return { + "label": label, + "repo_path": str(repo_path), + "exists": True, + "head_sha": head_sha, + "head_short": head_sha[:7], + "branch": branch, + "commit_sample_count": len(commits), + "commits": commits, + "remotes": read_remotes(repo_path), + "probe_error": ";".join(probe_errors), + } + + +def compare_repos(left: dict[str, object], right: dict[str, object]) -> dict[str, object]: + left_commits = set(left.get("commits", [])) + right_commits = set(right.get("commits", [])) + left_head = str(left.get("head_sha") or "") + right_head = str(right.get("head_sha") or "") + common = sorted(left_commits & right_commits) + + if not left.get("exists") or not right.get("exists"): + relation = "missing_repo" + elif left_head and left_head == right_head: + relation = "same_head" + elif left.get("probe_error") or right.get("probe_error"): + relation = "partial_probe" + elif right_head and right_head in left_commits: + relation = "left_descends_from_right" + elif left_head and left_head in right_commits: + relation = "right_descends_from_left" + elif common: + relation = "shared_history" + else: + relation = "no_shared_history" + + return { + "left_label": left["label"], + "right_label": right["label"], + "relation": relation, + "left_head": left_head, + "right_head": right_head, + "common_commit_count": len(common), + "common_commit_samples": common[:5], + } + + +def build_payload( + group_name: str, + repo_args: list[tuple[str, Path]], + sample_limit: int, + git_timeout: int, +) -> dict[str, object]: + repos = [repo_summary(label, path, sample_limit, git_timeout) for label, path in repo_args] + comparisons = [] + for left_index, left in enumerate(repos): + for right in repos[left_index + 1 :]: + comparisons.append(compare_repos(left, right)) + partial = any(item["relation"] == "partial_probe" for item in comparisons) + related = any( + item["relation"] + in ("same_head", "left_descends_from_right", "right_descends_from_left", "shared_history") + for item in comparisons + ) + no_shared = any(item["relation"] == "no_shared_history" for item in comparisons) + if partial: + status = "partial" + elif related and no_shared: + status = "mixed" + elif related: + status = "related" + elif comparisons: + status = "unrelated" + else: + status = "partial" + return { + "schema_version": "local_repo_canonical_probe_v1", + "group_name": group_name, + "status": status, + "sample_limit": sample_limit, + "git_timeout_seconds": git_timeout, + "repo_count": len(repos), + "comparison_count": len(comparisons), + "repos": repos, + "comparisons": comparisons, + } + + +def write_markdown(payload: dict[str, object], path: Path) -> None: + lines = [ + "# 本機 Repo Canonical Lineage Probe 快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 群組 | `{payload['group_name']}` |", + f"| 狀態 | `{payload['status']}` |", + f"| repo 數 | `{payload['repo_count']}` |", + f"| 比對數 | `{payload['comparison_count']}` |", + f"| sample limit | `{payload['sample_limit']}` |", + f"| git timeout seconds | `{payload['git_timeout_seconds']}` |", + "", + "## Repo HEAD", + "", + "| Label | Path | Branch | HEAD | Remotes |", + "|-------|------|--------|------|---------|", + ] + for repo in payload.get("repos", []): + if not isinstance(repo, dict): + continue + remotes = repo.get("remotes", []) + remote_text = ", ".join( + f"`{remote.get('name', '')}:{remote.get('url_redacted', '')}`" + for remote in remotes + if isinstance(remote, dict) + ) + lines.append( + "| " + + " | ".join( + [ + f"`{repo.get('label', '')}`", + f"`{repo.get('repo_path', '')}`", + f"`{repo.get('branch', '')}`", + f"`{repo.get('head_short', '')}`", + remote_text or "-", + ] + ) + + " |" + ) + + lines.extend(["", "## Lineage 比對", "", "| Left | Right | Relation | Common commits |", "|------|-------|----------|----------------|"]) + for comparison in payload.get("comparisons", []): + if not isinstance(comparison, dict): + continue + lines.append( + "| " + + " | ".join( + [ + f"`{comparison.get('left_label', '')}`", + f"`{comparison.get('right_label', '')}`", + f"`{comparison.get('relation', '')}`", + f"`{comparison.get('common_commit_count', 0)}`", + ] + ) + + " |" + ) + lines.extend( + [ + "", + "> 注意:本檔只比較本機 Git 物件,未 fetch 遠端;common commit sample 只用 SHA,不含 commit message。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--group-name", required=True) + parser.add_argument("--repo", action="append", type=parse_repo_arg, required=True) + parser.add_argument("--sample-limit", type=int, default=5000) + parser.add_argument("--git-timeout", type=int, default=10) + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + args = parser.parse_args() + + payload = build_payload(args.group_name, args.repo, args.sample_limit, args.git_timeout) + Path(args.output_json).write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(payload, Path(args.output_md)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/source-control-migration-inventory.py b/scripts/security/source-control-migration-inventory.py new file mode 100644 index 00000000..7b9344b3 --- /dev/null +++ b/scripts/security/source-control-migration-inventory.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +"""Read-only Gitea/GitHub 遷移盤點工具。 + +此工具只用 `git ls-remote` 比對兩端 refs,不 push、不 fetch、不寫入任一 remote。 +寫入 evidence 前會先遮蔽 remote URL 內的帳密。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + + +SHA_RE = re.compile(r"^[0-9a-fA-F]{7,40}$") + + +@dataclass(frozen=True) +class GitRefs: + heads: dict[str, str] + tags: dict[str, str] + raw_tag_ref_count: int + + +def run_git(repo: Path, args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + cwd=repo, + check=False, + capture_output=True, + text=True, + ) + + +def require_git(repo: Path, args: list[str]) -> str: + result = run_git(repo, args) + if result.returncode != 0: + stderr = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"git {' '.join(args)} failed: {stderr}") + return result.stdout + + +def redact_url(url: str) -> str: + """Remove userinfo from common URL formats before storing evidence.""" + if "://" in url: + parts = urlsplit(url) + netloc = parts.netloc + if "@" in netloc: + netloc = netloc.split("@", 1)[1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + + # scp-like syntax: user@host:path + if "@" in url and ":" in url.split("@", 1)[1]: + return url.split("@", 1)[1] + return url + + +def remote_url(repo: Path, remote: str) -> str: + return redact_url(require_git(repo, ["remote", "get-url", remote]).strip()) + + +def repo_slug_from_url(url: str) -> str: + redacted = redact_url(url).removesuffix("/") + if "://" in redacted: + path = urlsplit(redacted).path.strip("/") + elif ":" in redacted: + path = redacted.split(":", 1)[1].strip("/") + else: + path = redacted.strip("/") + return path.removesuffix(".git") or redacted + + +def parse_ls_remote(output: str, prefix: str) -> dict[str, str]: + refs: dict[str, str] = {} + for line in output.splitlines(): + if not line.strip(): + continue + try: + sha, ref = line.split(None, 1) + except ValueError: + continue + if not SHA_RE.match(sha) or not ref.startswith(prefix): + continue + name = ref.removeprefix(prefix) + refs[name] = sha + return refs + + +def refs_for_remote(repo: Path, remote: str) -> GitRefs: + heads_out = require_git(repo, ["ls-remote", "--heads", remote]) + tags_out = require_git(repo, ["ls-remote", "--tags", remote]) + raw_tags = parse_ls_remote(tags_out, "refs/tags/") + tags = { + name.removesuffix("^{}"): sha + for name, sha in raw_tags.items() + if not name.endswith("^{}") + } + peeled = { + name.removesuffix("^{}"): sha + for name, sha in raw_tags.items() + if name.endswith("^{}") + } + tags.update({name: peeled_sha for name, peeled_sha in peeled.items()}) + return GitRefs( + heads=parse_ls_remote(heads_out, "refs/heads/"), + tags=tags, + raw_tag_ref_count=len(raw_tags), + ) + + +def compare_maps(left: dict[str, str], right: dict[str, str]) -> dict[str, object]: + left_names = set(left) + right_names = set(right) + common = sorted(left_names & right_names) + return { + "only_left": sorted(left_names - right_names), + "only_right": sorted(right_names - left_names), + "sha_mismatch": [ + { + "name": name, + "left_sha": left[name], + "right_sha": right[name], + } + for name in common + if left[name] != right[name] + ], + "matching": [name for name in common if left[name] == right[name]], + } + + +def build_inventory(repo: Path, gitea_remote: str, github_remote: str) -> dict[str, object]: + gitea = refs_for_remote(repo, gitea_remote) + github = refs_for_remote(repo, github_remote) + gitea_url = remote_url(repo, gitea_remote) + github_url = remote_url(repo, github_remote) + head_diff = compare_maps(gitea.heads, github.heads) + tag_diff = compare_maps(gitea.tags, github.tags) + + latest_sha_gitea = gitea.heads.get("main", "") + latest_sha_github = github.heads.get("main", "") + status = "verified" + blocking_reasons: list[str] = [] + + if head_diff["only_left"] or head_diff["only_right"] or head_diff["sha_mismatch"]: + status = "blocked" + blocking_reasons.append("branches 尚未完全對齊") + if tag_diff["only_left"] or tag_diff["only_right"] or tag_diff["sha_mismatch"]: + status = "blocked" + blocking_reasons.append("tags 尚未完全對齊") + if latest_sha_gitea and latest_sha_github and latest_sha_gitea != latest_sha_github: + status = "blocked" + blocking_reasons.append("main SHA 不一致") + if not latest_sha_gitea or not latest_sha_github: + status = "blocked" + blocking_reasons.append("其中一端缺少 main") + + return { + "repo_path": str(repo), + "gitea_remote": gitea_remote, + "github_remote": github_remote, + "gitea_url_redacted": gitea_url, + "github_url_redacted": github_url, + "gitea_repo": repo_slug_from_url(gitea_url), + "github_repo": repo_slug_from_url(github_url), + "branch_count_gitea": len(gitea.heads), + "branch_count_github": len(github.heads), + "tag_count_gitea": len(gitea.tags), + "tag_count_github": len(github.tags), + "raw_tag_ref_count_gitea": gitea.raw_tag_ref_count, + "raw_tag_ref_count_github": github.raw_tag_ref_count, + "latest_sha_gitea": latest_sha_gitea, + "latest_sha_github": latest_sha_github, + "workflows_mapped": False, + "webhooks_mapped": False, + "secrets_inventory_only": True, + "status": status, + "blocking_reason": ";".join(blocking_reasons) if blocking_reasons else "", + "heads": head_diff, + "tags": tag_diff, + } + + +def event_payload(inventory: dict[str, object], evidence_ref: str | None) -> dict[str, object]: + payload = { + "schema_version": "source_control_migration_event_v1", + "gitea_repo": inventory["gitea_repo"], + "github_repo": inventory["github_repo"], + "branch_count_gitea": inventory["branch_count_gitea"], + "branch_count_github": inventory["branch_count_github"], + "tag_count_gitea": inventory["tag_count_gitea"], + "tag_count_github": inventory["tag_count_github"], + "latest_sha_gitea": inventory["latest_sha_gitea"], + "latest_sha_github": inventory["latest_sha_github"], + "workflows_mapped": inventory["workflows_mapped"], + "webhooks_mapped": inventory["webhooks_mapped"], + "secrets_inventory_only": inventory["secrets_inventory_only"], + "status": inventory["status"], + "blocking_reason": inventory["blocking_reason"], + } + if evidence_ref: + payload["evidence_refs"] = [evidence_ref] + return payload + + +def write_markdown(inventory: dict[str, object], path: Path) -> None: + heads = inventory["heads"] + tags = inventory["tags"] + assert isinstance(heads, dict) + assert isinstance(tags, dict) + + lines = [ + "# Source Control 遷移盤點快照", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 狀態 | `{inventory['status']}` |", + f"| Gitea remote | `{inventory['gitea_remote']}` |", + f"| GitHub remote | `{inventory['github_remote']}` |", + f"| Gitea repo | `{inventory['gitea_repo']}` |", + f"| GitHub repo | `{inventory['github_repo']}` |", + f"| Gitea URL | `{inventory['gitea_url_redacted']}` |", + f"| GitHub URL | `{inventory['github_url_redacted']}` |", + f"| Gitea 分支數 | `{inventory['branch_count_gitea']}` |", + f"| GitHub 分支數 | `{inventory['branch_count_github']}` |", + f"| Gitea tags | `{inventory['tag_count_gitea']}` |", + f"| GitHub tags | `{inventory['tag_count_github']}` |", + f"| Gitea main | `{inventory['latest_sha_gitea']}` |", + f"| GitHub main | `{inventory['latest_sha_github']}` |", + f"| 阻塞原因 | {inventory['blocking_reason'] or '無'} |", + "", + "## 分支差異", + "", + f"- 只在 Gitea:`{len(heads['only_left'])}`", + f"- 只在 GitHub:`{len(heads['only_right'])}`", + f"- SHA 不一致:`{len(heads['sha_mismatch'])}`", + f"- SHA 一致:`{len(heads['matching'])}`", + "", + "## Tag 差異", + "", + f"- 只在 Gitea:`{len(tags['only_left'])}`", + f"- 只在 GitHub:`{len(tags['only_right'])}`", + f"- SHA 不一致:`{len(tags['sha_mismatch'])}`", + f"- SHA 一致:`{len(tags['matching'])}`", + "", + "> 注意:本檔由 read-only inventory 工具產生,不包含 remote URL 內的帳密。", + "", + ] + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo", default=".") + parser.add_argument("--gitea-remote", default="gitea") + parser.add_argument("--github-remote", default="origin") + parser.add_argument("--output-json") + parser.add_argument("--output-md") + args = parser.parse_args() + + repo = Path(args.repo).resolve() + try: + inventory = build_inventory(repo, args.gitea_remote, args.github_remote) + except RuntimeError as exc: + print(str(exc), file=sys.stderr) + return 2 + + payload = json.dumps( + event_payload(inventory, args.output_md), + ensure_ascii=False, + indent=2, + ) + if args.output_json: + Path(args.output_json).write_text(payload + "\n", encoding="utf-8") + else: + print(payload) + + if args.output_md: + write_markdown(inventory, Path(args.output_md)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())