feat(security): 產生高價值配置 owner packet 草案 [skip ci]

This commit is contained in:
Your Name
2026-06-11 11:58:11 +08:00
parent 8c11af7c19
commit f5a5fe1f99
8 changed files with 1224 additions and 25 deletions

View File

@@ -26,7 +26,7 @@ TIER_ORDER = {"C0": 0, "C1": 1, "C2": 2, "C3": 3}
PRIORITY_ORDER = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
REQUIRED_OWNER_FIELDS = [
"owner_role_team",
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
@@ -37,6 +37,18 @@ REQUIRED_OWNER_FIELDS = [
"validation_plan",
]
OWNER_FIELD_ALIASES = {
"owner_role_or_team": ["owner_role_or_team", "owner_role_team", "owner_role", "owner_team", "responsible_team"],
"decision": ["decision", "owner_decision", "scope_decision", "disposition"],
"decision_reason": ["decision_reason", "reason", "decision_summary", "owner_note", "rationale"],
"affected_scope": ["affected_scope", "affected_sources", "affected_repos", "host_scope", "target_scope"],
"redacted_evidence_refs": ["redacted_evidence_refs", "evidence_refs", "redacted_refs", "source_refs"],
"followup_owner": ["followup_owner", "next_owner", "followup_team", "resolution_owner"],
"rollback_owner": ["rollback_owner", "rollback_team"],
"maintenance_window": ["maintenance_window", "change_window"],
"validation_plan": ["validation_plan", "validation_checks", "verification_plan"],
}
REQUIRED_FALSE_FLAGS = [
"runtime_execution_authorized",
"host_write_authorized",
@@ -433,6 +445,11 @@ def load_evidence(path: Path | None) -> dict[str, Any] | None:
return json.loads(path.read_text(encoding="utf-8"))
def has_owner_field(evidence: dict[str, Any], field: str) -> bool:
aliases = OWNER_FIELD_ALIASES.get(field, [field])
return any(bool(evidence.get(alias)) for alias in aliases)
def validate_evidence(evidence: dict[str, Any] | None) -> dict[str, Any]:
if evidence is None:
return {
@@ -443,7 +460,7 @@ def validate_evidence(evidence: dict[str, Any] | None) -> dict[str, Any]:
"note": "未提供 owner response evidence本階段只能分類不得執行 runtime 變更。",
}
missing = [field for field in REQUIRED_OWNER_FIELDS if not evidence.get(field)]
missing = [field for field in REQUIRED_OWNER_FIELDS if not has_owner_field(evidence, field)]
false_flags = evidence.get("false_flags", {})
invalid_false_flags = [
flag for flag in REQUIRED_FALSE_FLAGS if false_flags.get(flag) is not False

View File

@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""
IwoooS 高價值配置 owner response packet 產生器。
本工具讀取 high-value-config-change-gate 的 JSON 報告,為 impacted
categories 產出 owner response packet 草案。它只產生欄位、補件規則與
禁止事項,不送件、不收回覆、不建立 action button、不執行 runtime。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
TAIPEI = timezone(timedelta(hours=8))
CANONICAL_OWNER_FIELDS = [
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"followup_owner",
"rollback_owner",
"maintenance_window",
"validation_plan",
]
ALLOWED_DECISIONS = ["confirm", "defer", "reject", "request_more_evidence"]
FIELD_INSTRUCTIONS = {
"owner_role_or_team": "填角色或團隊不填私人帳號、密碼、token 或私人聯絡資訊。",
"decision": "只能填 confirm、defer、reject、request_more_evidence不得附帶 runtime 執行批准。",
"decision_reason": "填脫敏短理由;不可貼 raw log、raw API body、未脫敏截圖或內部對話。",
"affected_scope": "填受影響 repo、host、domain、namespace、route、service、secret name 或產品邊界。",
"redacted_evidence_refs": "只填文件路徑、snapshot id、ticket id、commit、hash 或脫敏 metadata pointer。",
"followup_owner": "填後續補證、審查或決策負責角色 / 團隊。",
"rollback_owner": "填回滾負責角色 / 團隊;不是直接執行授權。",
"maintenance_window": "填維護窗口或明確寫 deferred / not scheduled不得用口頭同意代替。",
"validation_plan": "填 preflight、post-check、rollback check若只讀文件變更寫 guard / json / doc secret sanity。",
}
FALSE_FLAGS = [
"request_sent",
"response_received",
"response_accepted",
"runtime_execution_authorized",
"host_write_authorized",
"nginx_reload_authorized",
"dns_tls_change_authorized",
"workflow_modification_authorized",
"runner_change_authorized",
"refs_sync_authorized",
"force_push_authorized",
"secret_value_collection_allowed",
"active_scan_authorized",
"action_buttons_allowed",
]
BLOCKED_REQUESTS = [
"repo_create",
"visibility_change",
"refs_sync",
"refs_delete",
"force_push",
"workflow_modify",
"runner_enable",
"secret_value_submit",
"ssh_host_modify",
"nginx_reload",
"dns_tls_modify",
"argocd_sync",
"kubectl_apply",
"active_scan",
"agent_bounty_runtime_execute",
"payout_or_withdrawal",
]
OUTCOME_LANES = [
"keep_waiting_owner_response",
"request_more_evidence",
"quarantine_sensitive_payload",
"reject_execution_request",
"ready_for_reviewer_validation",
]
def git_short_sha(root: Path) -> str:
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=root,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
except Exception:
return "unknown"
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def field_templates() -> list[dict[str, Any]]:
return [
{
"field": field,
"required": True,
"instruction": FIELD_INSTRUCTIONS[field],
}
for field in CANONICAL_OWNER_FIELDS
]
def false_flag_map() -> dict[str, bool]:
return {flag: False for flag in FALSE_FLAGS}
def files_for_category(gate_report: dict[str, Any], category_id: str) -> list[str]:
paths: list[str] = []
for item in gate_report.get("changed_files", []):
if any(category.get("category_id") == category_id for category in item.get("categories", [])):
paths.append(item["path"])
return sorted(paths)
def category_inventory_by_id(gate_report: dict[str, Any]) -> dict[str, dict[str, Any]]:
return {
item["category_id"]: item
for item in gate_report.get("control_category_inventory", [])
}
def packet_for_category(gate_report: dict[str, Any], category: dict[str, Any]) -> dict[str, Any]:
inventory = category_inventory_by_id(gate_report).get(category["category_id"], {})
affected_files = files_for_category(gate_report, category["category_id"])
return {
"packet_id": f"high_value_config_owner_packet:{category['category_id']}",
"status": "draft_waiting_owner_response",
"category_id": category["category_id"],
"label": category["label"],
"priority": category["priority"],
"control_tier": category["control_tier"],
"required_gate": category["required_gate"],
"affected_files": affected_files,
"allowed_decisions": ALLOWED_DECISIONS,
"field_templates": field_templates(),
"required_validation": category.get("required_validation") or inventory.get("required_validation", []),
"redaction_rules": [
"只收 redacted evidence refs不收 secret value。",
"疑似 token、cookie、authorization header、private key、runner token 或 webhook secret 一律 quarantine。",
"內部工作視窗對話、抱怨、口頭同意不得進產品文案或 LOGBOOK raw text。",
],
"blocked_requests": BLOCKED_REQUESTS,
"outcome_lanes": OUTCOME_LANES,
"false_flags": false_flag_map(),
"reviewer_checklist": [
"canonical owner fields 全部存在。",
"decision 只使用允許值。",
"affected scope 可映射到 repo / host / domain / route / service / secret name。",
"redacted evidence refs 不含 raw payload。",
"沒有夾帶執行要求。",
"C0 / C1 若要進 runtime需獨立人工批准與維護窗口。",
],
}
def build_report(root: Path, gate_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
impacted_categories = gate_report.get("impacted_categories", [])
packets = [packet_for_category(gate_report, category) for category in impacted_categories]
c0_packets = [packet for packet in packets if packet["control_tier"] == "C0"]
c1_packets = [packet for packet in packets if packet["control_tier"] == "C1"]
return {
"schema_version": "high_value_config_owner_packet_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_gate_schema_version": gate_report.get("schema_version"),
"source_gate_summary": gate_report.get("summary", {}),
"status": "draft_waiting_owner_response",
"canonical_owner_fields": CANONICAL_OWNER_FIELDS,
"allowed_decisions": ALLOWED_DECISIONS,
"execution_boundaries": {
"request_sent": False,
"response_received": False,
"response_accepted": False,
"runtime_execution_authorized": False,
"host_write_authorized": False,
"secret_value_collected": False,
"action_buttons_allowed": False,
},
"summary": {
"packet_count": len(packets),
"c0_packet_count": len(c0_packets),
"c1_packet_count": len(c1_packets),
"request_sent_count": 0,
"received_response_count": 0,
"accepted_response_count": 0,
"runtime_gate_count": 0,
},
"universal_owner_response_template": {
"allowed_decisions": ALLOWED_DECISIONS,
"field_templates": field_templates(),
"false_flags": false_flag_map(),
},
"packets": packets,
"control_category_inventory": gate_report.get("control_category_inventory", []),
"next_steps": [
"若 packet_count > 0將 packet 交給 owner 補 canonical 欄位;不得把草案視為已送件。",
"若 owner 回覆含 secret 或執行要求,先 quarantine 或 reject_execution_request。",
"只有 reviewer checklist 完成後才可進 acceptedaccepted 仍不開 runtime gate。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS 高價值配置 owner response packet 產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--gate-report",
default="docs/security/high-value-config-change-gate.snapshot.json",
help="high-value-config-change-gate.py 輸出的 JSON",
)
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
gate_report = load_json(root / args.gate_report)
report = build_report(root, gate_report, args.generated_at)
payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(payload + "\n", encoding="utf-8")
else:
print(payload)
summary = report["summary"]
print(
"HIGH_VALUE_CONFIG_OWNER_PACKET_OK "
f"packets={summary['packet_count']} "
f"c0={summary['c0_packet_count']} "
f"c1={summary['c1_packet_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
sys.exit(main())