Compare commits
6 Commits
codex/gith
...
codex/110-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f477a2243b | ||
|
|
e0345ca130 | ||
|
|
48b192fab7 | ||
|
|
0e2e8057cf | ||
|
|
0040a595a4 | ||
|
|
219fc3835c |
23
AGENTS.md
23
AGENTS.md
@@ -23,12 +23,31 @@
|
||||
## 四大核心原則
|
||||
|
||||
1. **變更前 → 先讀註解** (理解設計意圖再動手) 🔴
|
||||
2. **不可逆操作 → 人工確認** (刪除、logOut、DROP、force push)
|
||||
3. **有疑問 → 先問統帥** (不確定就停下來)
|
||||
2. **事故級不可逆操作 → break-glass** (secret 明文、DROP / TRUNCATE / destructive restore、reboot、node drain、不可逆 firewall、外部攻擊型掃描、force push、repo / refs 破壞、付費 provider / 成本切換)
|
||||
3. **非事故級 gate → 受控自動執行** (不要把 owner / read-only / manual / `0 / false` 當阻擋)
|
||||
4. **任務完成 → 更新 Memory** (不等被問)
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴🔴 統帥全面授權(2026-06-28)
|
||||
|
||||
所有 AWOOOI / AwoooP / IwoooS / StockPlatform / VibeWork / MOMO / AwoooGo / 2026FIFA / Agent Bounty / Tsenyang 工作,除 `docs/HARD_RULES.md#commander-blanket-authorization` 定義的事故級硬阻擋外,一律直接進 AI controlled apply。
|
||||
|
||||
舊規範、UI、Telegram、snapshot、test 或 Runbook 若出現下列狀態,不得作為停止實作理由:
|
||||
|
||||
- `owner_response_received=0`
|
||||
- `owner_response_accepted=0`
|
||||
- `runtime_execution_authorized=false`
|
||||
- `runtime_write_gate=0`
|
||||
- `apply_allowed=false`
|
||||
- `read_only=true`
|
||||
- `manual_required`
|
||||
- `needs_human=true`
|
||||
|
||||
正確動作是 AI 自動補齊 target selector、source-of-truth diff、check-mode / dry-run、rollback、post-apply verifier、KM / PlayBook trust writeback,然後推進可驗證、可回滾、低爆炸半徑的實作。
|
||||
|
||||
---
|
||||
|
||||
## 🔴 絕對禁止 → [HARD_RULES.md](docs/HARD_RULES.md)
|
||||
|
||||
## 🔴 文件語言鐵律 → [文件語言規範](docs/HARD_RULES.md#文件語言規範)
|
||||
|
||||
@@ -37,6 +37,7 @@ from src.services.iwooos_wazuh_managed_host_coverage import (
|
||||
)
|
||||
from src.services.iwooos_wazuh_manager_registry_reviewer_validation import (
|
||||
load_latest_iwooos_wazuh_manager_registry_reviewer_validation,
|
||||
validate_iwooos_wazuh_manager_registry_acceptance_evidence as validate_wazuh_manager_registry_acceptance_evidence_payload,
|
||||
validate_iwooos_wazuh_manager_registry_owner_export as validate_wazuh_manager_registry_owner_export_payload,
|
||||
)
|
||||
from src.services.iwooos_wazuh_owner_evidence_preflight import (
|
||||
@@ -211,6 +212,40 @@ async def validate_iwooos_wazuh_manager_registry_owner_export(owner_export: dict
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance",
|
||||
response_model=dict[str, Any],
|
||||
summary="驗證 Wazuh manager registry accepted 脫敏 evidence packet",
|
||||
description=(
|
||||
"針對單次 owner / reviewer 提供的 redacted Wazuh manager registry acceptance evidence "
|
||||
"packet 進行 no-persist review readiness validation,回傳 accepted-for-review / needs supplement / "
|
||||
"quarantined / rejected runtime action 分流。此端點不保存 payload、不查 Wazuh API、不讀主機、"
|
||||
"不重新註冊 agent、不重啟服務、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / Docker / "
|
||||
"K8s / firewall,也不更新 manager registry accepted 總帳。"
|
||||
),
|
||||
)
|
||||
async def validate_iwooos_wazuh_manager_registry_acceptance_evidence(
|
||||
acceptance_evidence: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""回傳單次 Wazuh manager registry accepted evidence 的公開安全驗證結果。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
validate_wazuh_manager_registry_acceptance_evidence_payload,
|
||||
acceptance_evidence,
|
||||
)
|
||||
return redact_public_lan_topology(payload)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"IwoooS Wazuh manager registry acceptance evidence 驗證器無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/runtime-security-readback",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -111,6 +111,24 @@ _ACCOUNTABILITY_REQUIRED_FIELDS = {
|
||||
"postcheck_plan",
|
||||
}
|
||||
|
||||
_ACCEPTANCE_EVIDENCE_REQUIRED_FIELDS = {
|
||||
"acceptance_reviewer_role",
|
||||
"acceptance_decision",
|
||||
"acceptance_decision_reason",
|
||||
"accepted_scope_aliases",
|
||||
"accepted_agent_total",
|
||||
"accepted_agent_active",
|
||||
"accepted_agent_disconnected",
|
||||
"accepted_agent_never_connected",
|
||||
"manager_registry_acceptance_evidence_refs",
|
||||
"post_enable_readback_ref",
|
||||
"product_scope_parity_ref",
|
||||
"reviewed_at",
|
||||
"followup_owner",
|
||||
"rollback_owner",
|
||||
"post_acceptance_verifier_plan",
|
||||
}
|
||||
|
||||
|
||||
def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
|
||||
security_dir: Path | None = None,
|
||||
@@ -137,6 +155,15 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
|
||||
"reviewer_validation_failed_count": _int(summary.get("reviewer_validation_failed_count")),
|
||||
"reviewer_validation_quarantined_count": _int(summary.get("reviewer_validation_quarantined_count")),
|
||||
"manager_registry_accepted_count": _int(summary.get("manager_registry_accepted_count")),
|
||||
"manager_registry_acceptance_intake_endpoint_available_count": _int(
|
||||
summary.get("manager_registry_acceptance_intake_endpoint_available_count")
|
||||
),
|
||||
"manager_registry_acceptance_evidence_received_count": _int(
|
||||
summary.get("manager_registry_acceptance_evidence_received_count")
|
||||
),
|
||||
"manager_registry_acceptance_evidence_review_ready_count": _int(
|
||||
summary.get("manager_registry_acceptance_evidence_review_ready_count")
|
||||
),
|
||||
"post_enable_readback_passed_count": _int(summary.get("post_enable_readback_passed_count")),
|
||||
"runtime_gate_count": _int(summary.get("runtime_gate_count")),
|
||||
"host_write_authorized_count": _int(summary.get("host_write_authorized_count")),
|
||||
@@ -157,6 +184,10 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export"
|
||||
),
|
||||
"owner_export_validation_mode": "no_persist_validation_no_runtime_action",
|
||||
"manager_registry_acceptance_validation_endpoint": (
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance"
|
||||
),
|
||||
"manager_registry_acceptance_validation_mode": "no_persist_acceptance_evidence_review_no_runtime_action",
|
||||
"summary": merged_summary,
|
||||
"expected_scope_aliases": _strings(snapshot.get("expected_scope_aliases")),
|
||||
"reviewer_validation_checks": _checks(snapshot.get("reviewer_validation_checks")),
|
||||
@@ -267,6 +298,9 @@ def _boundary_markers(summary: dict[str, int]) -> list[str]:
|
||||
f"wazuh_manager_registry_reviewer_validation_passed_count={summary['reviewer_validation_passed_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_quarantined_count={summary['reviewer_validation_quarantined_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count={summary['manager_registry_accepted_count']}",
|
||||
"wazuh_manager_registry_acceptance_validation_api_available=true",
|
||||
f"wazuh_manager_registry_acceptance_evidence_received_count={summary['manager_registry_acceptance_evidence_received_count']}",
|
||||
f"wazuh_manager_registry_acceptance_evidence_review_ready_count={summary['manager_registry_acceptance_evidence_review_ready_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count={summary['post_enable_readback_passed_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_runtime_gate_count={summary['runtime_gate_count']}",
|
||||
"wazuh_api_live_query_authorized=false",
|
||||
@@ -299,7 +333,24 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
|
||||
failed = _int(summary.get("reviewer_validation_failed_count"))
|
||||
quarantined = _int(summary.get("reviewer_validation_quarantined_count"))
|
||||
post_enable = _int(summary.get("post_enable_readback_passed_count"))
|
||||
if any(value < 0 for value in (received, accepted, ready, passed, failed, quarantined, post_enable)):
|
||||
acceptance_endpoint = _int(summary.get("manager_registry_acceptance_intake_endpoint_available_count"))
|
||||
acceptance_received = _int(summary.get("manager_registry_acceptance_evidence_received_count"))
|
||||
acceptance_ready = _int(summary.get("manager_registry_acceptance_evidence_review_ready_count"))
|
||||
if any(
|
||||
value < 0
|
||||
for value in (
|
||||
received,
|
||||
accepted,
|
||||
ready,
|
||||
passed,
|
||||
failed,
|
||||
quarantined,
|
||||
post_enable,
|
||||
acceptance_endpoint,
|
||||
acceptance_received,
|
||||
acceptance_ready,
|
||||
)
|
||||
):
|
||||
raise ValueError("Wazuh manager registry reviewer validation counters 不得為負數")
|
||||
if accepted > received:
|
||||
raise ValueError("owner_registry_export_accepted_count 不得大於 received_count")
|
||||
@@ -309,6 +360,14 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
|
||||
raise ValueError("reviewer_validation_passed_count 不得大於 accepted_count")
|
||||
if post_enable > passed:
|
||||
raise ValueError("post_enable_readback_passed_count 不得大於 reviewer_validation_passed_count")
|
||||
if acceptance_endpoint > 1:
|
||||
raise ValueError("manager_registry_acceptance_intake_endpoint_available_count 不得大於 1")
|
||||
if acceptance_ready > acceptance_received:
|
||||
raise ValueError(
|
||||
"manager_registry_acceptance_evidence_review_ready_count 不得大於 received_count"
|
||||
)
|
||||
if acceptance_ready:
|
||||
raise ValueError("manager_registry_acceptance_evidence_review_ready_count 目前不得在全域讀回中自動上修")
|
||||
if failed and passed:
|
||||
raise ValueError("reviewer_validation_failed_count 與 passed_count 不得同時為正")
|
||||
if quarantined and accepted:
|
||||
@@ -477,6 +536,124 @@ def validate_iwooos_wazuh_manager_registry_owner_export(
|
||||
return _validation_result(contract, outcome, findings, evidence_status)
|
||||
|
||||
|
||||
def validate_iwooos_wazuh_manager_registry_acceptance_evidence(
|
||||
acceptance_evidence: dict[str, Any],
|
||||
security_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate one redacted manager-registry acceptance packet without changing runtime truth."""
|
||||
contract = load_latest_iwooos_wazuh_manager_registry_reviewer_validation(security_dir)
|
||||
snapshot = _load_snapshot(security_dir or _DEFAULT_SECURITY_DIR)
|
||||
expected_aliases = set(_strings(snapshot.get("expected_scope_aliases")))
|
||||
|
||||
findings: list[dict[str, Any]] = []
|
||||
if not isinstance(acceptance_evidence, dict):
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-01",
|
||||
"blocker",
|
||||
"request_manager_registry_acceptance_evidence_supplement",
|
||||
"manager registry acceptance evidence 必須是 JSON object。",
|
||||
[],
|
||||
)
|
||||
)
|
||||
return _acceptance_validation_result(contract, "request_manager_registry_acceptance_evidence_supplement", findings)
|
||||
|
||||
sensitive_hits = _collect_sensitive_hits(acceptance_evidence)
|
||||
if sensitive_hits:
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-04",
|
||||
"critical",
|
||||
"quarantine_sensitive_payload",
|
||||
"acceptance evidence 含禁止內容或疑似未脫敏內容,已進隔離分流;回應不回傳原始值。",
|
||||
[hit["path"] for hit in sensitive_hits[:12]],
|
||||
{"categories": sorted({hit["category"] for hit in sensitive_hits})},
|
||||
)
|
||||
)
|
||||
return _acceptance_validation_result(contract, "quarantine_sensitive_payload", findings)
|
||||
|
||||
runtime_hits = _collect_runtime_action_hits(acceptance_evidence)
|
||||
if runtime_hits:
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-05",
|
||||
"critical",
|
||||
"reject_runtime_action_request",
|
||||
"acceptance evidence 夾帶 runtime action request;此 validator 只允許脫敏 acceptance refs,不授權 active response、restart、host write 或掃描。",
|
||||
runtime_hits[:12],
|
||||
)
|
||||
)
|
||||
return _acceptance_validation_result(contract, "reject_runtime_action_request", findings)
|
||||
|
||||
missing_fields = [
|
||||
field for field in sorted(_ACCEPTANCE_EVIDENCE_REQUIRED_FIELDS) if not _present(acceptance_evidence.get(field))
|
||||
]
|
||||
if missing_fields:
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-01",
|
||||
"blocker",
|
||||
"request_manager_registry_acceptance_evidence_supplement",
|
||||
"manager registry acceptance evidence 欄位不足,需要補齊後再驗證。",
|
||||
missing_fields,
|
||||
)
|
||||
)
|
||||
|
||||
count_issue = _validate_acceptance_counts(acceptance_evidence)
|
||||
if count_issue:
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-02",
|
||||
"blocker",
|
||||
"request_manager_registry_acceptance_counts_fix",
|
||||
count_issue,
|
||||
[
|
||||
"accepted_agent_total",
|
||||
"accepted_agent_active",
|
||||
"accepted_agent_disconnected",
|
||||
"accepted_agent_never_connected",
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
alias_issue = _validate_aliases(acceptance_evidence.get("accepted_scope_aliases"), expected_aliases)
|
||||
if alias_issue:
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-03",
|
||||
"blocker",
|
||||
"request_manager_registry_acceptance_scope_parity_fix",
|
||||
alias_issue,
|
||||
["accepted_scope_aliases"],
|
||||
)
|
||||
)
|
||||
|
||||
decision = acceptance_evidence.get("acceptance_decision")
|
||||
if decision != "accept_manager_registry_evidence_for_review":
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-06",
|
||||
"blocker",
|
||||
"request_manager_registry_acceptance_decision_fix",
|
||||
"acceptance_decision 必須明確為 accept_manager_registry_evidence_for_review;不可宣稱已更新 manager registry accepted 總帳。",
|
||||
["acceptance_decision"],
|
||||
)
|
||||
)
|
||||
|
||||
outcome = _first_blocking_lane(findings) or "accepted_for_manager_registry_acceptance_review_only"
|
||||
if outcome == "accepted_for_manager_registry_acceptance_review_only":
|
||||
findings.append(
|
||||
_finding(
|
||||
"MRA-07",
|
||||
"info",
|
||||
"manager_registry_acceptance_evidence_review_ready",
|
||||
"acceptance evidence 已通過 no-persist review readiness;仍不更新全域 manager_registry_accepted_count,不開 runtime gate。",
|
||||
["manager_registry_acceptance_evidence_refs"],
|
||||
)
|
||||
)
|
||||
return _acceptance_validation_result(contract, outcome, findings)
|
||||
|
||||
|
||||
def _validation_result(
|
||||
contract: dict[str, Any],
|
||||
outcome_lane: str,
|
||||
@@ -543,6 +720,70 @@ def _validation_result(
|
||||
}
|
||||
|
||||
|
||||
def _acceptance_validation_result(
|
||||
contract: dict[str, Any],
|
||||
outcome_lane: str,
|
||||
findings: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
accepted = outcome_lane == "accepted_for_manager_registry_acceptance_review_only"
|
||||
quarantined = outcome_lane == "quarantine_sensitive_payload"
|
||||
rejected_runtime = outcome_lane == "reject_runtime_action_request"
|
||||
supplement_required = not accepted and not quarantined and not rejected_runtime
|
||||
return {
|
||||
"schema_version": "iwooos_wazuh_manager_registry_acceptance_evidence_validation_result_v1",
|
||||
"contract_schema_version": contract["schema_version"],
|
||||
"status": outcome_lane,
|
||||
"mode": "no_persist_acceptance_evidence_review_no_runtime_no_secret_collection",
|
||||
"outcome_lane": outcome_lane,
|
||||
"accepted_for_manager_registry_acceptance_review_only": accepted,
|
||||
"quarantined": quarantined,
|
||||
"runtime_action_rejected": rejected_runtime,
|
||||
"summary": {
|
||||
"manager_registry_acceptance_evidence_received_count": 1,
|
||||
"manager_registry_acceptance_evidence_review_ready_count": 1 if accepted else 0,
|
||||
"manager_registry_acceptance_evidence_supplement_required_count": 1 if supplement_required else 0,
|
||||
"manager_registry_acceptance_evidence_quarantined_count": 1 if quarantined else 0,
|
||||
"manager_registry_acceptance_runtime_action_rejected_count": 1 if rejected_runtime else 0,
|
||||
"manager_registry_accepted_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"host_write_authorized_count": 0,
|
||||
"active_response_authorized_count": 0,
|
||||
"secret_value_collection_allowed_count": 0,
|
||||
"finding_count": len(findings),
|
||||
},
|
||||
"validation_findings": findings,
|
||||
"boundary_markers": [
|
||||
"wazuh_manager_registry_acceptance_validation_received_count=1",
|
||||
f"wazuh_manager_registry_acceptance_validation_review_ready_count={1 if accepted else 0}",
|
||||
f"wazuh_manager_registry_acceptance_validation_quarantined_count={1 if quarantined else 0}",
|
||||
"wazuh_manager_registry_acceptance_validation_manager_registry_accepted_count=0",
|
||||
"wazuh_manager_registry_acceptance_validation_runtime_gate_count=0",
|
||||
"wazuh_manager_registry_acceptance_validation_no_persist=true",
|
||||
"wazuh_api_live_query_authorized=false",
|
||||
"wazuh_active_response_authorized=false",
|
||||
"host_write_authorized=false",
|
||||
"secret_value_collection_allowed=false",
|
||||
"not_authorization=true",
|
||||
],
|
||||
"boundaries": {
|
||||
"payload_persisted": False,
|
||||
"wazuh_api_live_query_authorized": False,
|
||||
"wazuh_agent_reenroll_authorized": False,
|
||||
"wazuh_agent_restart_authorized": False,
|
||||
"wazuh_manager_restart_authorized": False,
|
||||
"wazuh_active_response_authorized": False,
|
||||
"host_write_authorized": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"raw_wazuh_payload_storage_allowed": False,
|
||||
"kali_active_scan_authorized": False,
|
||||
"runtime_execution_authorized": False,
|
||||
"manager_registry_accepted_updated": False,
|
||||
"not_authorization": True,
|
||||
},
|
||||
"next_gate": "commit_manager_registry_accepted_readback_evidence" if accepted else "manager_registry_acceptance_fix_and_resubmit",
|
||||
}
|
||||
|
||||
|
||||
def _finding(
|
||||
check_id: str,
|
||||
severity: str,
|
||||
@@ -614,6 +855,29 @@ def _validate_counts(owner_export: dict[str, Any]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _validate_acceptance_counts(acceptance_evidence: dict[str, Any]) -> str | None:
|
||||
fields = [
|
||||
"accepted_agent_total",
|
||||
"accepted_agent_active",
|
||||
"accepted_agent_disconnected",
|
||||
"accepted_agent_never_connected",
|
||||
]
|
||||
counts = {field: _int_or_none(acceptance_evidence.get(field)) for field in fields}
|
||||
if any(value is None for value in counts.values()):
|
||||
return "accepted agent count 欄位必須是非負整數。"
|
||||
if any(value is not None and value < 0 for value in counts.values()):
|
||||
return "accepted agent count 欄位不得為負數。"
|
||||
total = counts["accepted_agent_total"]
|
||||
active = counts["accepted_agent_active"]
|
||||
disconnected = counts["accepted_agent_disconnected"]
|
||||
never_connected = counts["accepted_agent_never_connected"]
|
||||
if total is None or active is None or disconnected is None or never_connected is None:
|
||||
return "accepted agent count 欄位必須是非負整數。"
|
||||
if total < active + disconnected + never_connected:
|
||||
return "accepted_agent_total 不得小於 active + disconnected + never_connected。"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_aliases(value: Any, expected_aliases: set[str]) -> str | None:
|
||||
aliases = value if isinstance(value, list) else []
|
||||
if not aliases or not all(isinstance(item, str) for item in aliases):
|
||||
@@ -709,6 +973,10 @@ def _first_blocking_lane(findings: list[dict[str, Any]]) -> str | None:
|
||||
"request_dashboard_api_repair_postcheck",
|
||||
"request_readonly_credential_metadata",
|
||||
"request_owner_accountability_supplement",
|
||||
"request_manager_registry_acceptance_evidence_supplement",
|
||||
"request_manager_registry_acceptance_counts_fix",
|
||||
"request_manager_registry_acceptance_scope_parity_fix",
|
||||
"request_manager_registry_acceptance_decision_fix",
|
||||
):
|
||||
if any(item.get("lane") == lane for item in findings):
|
||||
return lane
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def test_awooop_controlled_automation_copy_guard_blocks_legacy_manual_gate_text() -> None:
|
||||
guard = runpy.run_path(
|
||||
str(ROOT / "scripts" / "security" / "awooop-controlled-automation-copy-guard.py")
|
||||
)
|
||||
|
||||
guard["validate"](ROOT)
|
||||
@@ -6,6 +6,7 @@ from fastapi.testclient import TestClient
|
||||
from src.api.v1.iwooos import router
|
||||
from src.services.iwooos_wazuh_manager_registry_reviewer_validation import (
|
||||
load_latest_iwooos_wazuh_manager_registry_reviewer_validation,
|
||||
validate_iwooos_wazuh_manager_registry_acceptance_evidence,
|
||||
validate_iwooos_wazuh_manager_registry_owner_export,
|
||||
)
|
||||
|
||||
@@ -78,18 +79,42 @@ def _valid_owner_export() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _valid_acceptance_evidence() -> dict:
|
||||
return {
|
||||
"acceptance_reviewer_role": "IwoooS reviewer",
|
||||
"acceptance_decision": "accept_manager_registry_evidence_for_review",
|
||||
"acceptance_decision_reason": "脫敏 manager registry evidence 與六個公開別名 scope parity 一致,可進 commit review。",
|
||||
"accepted_scope_aliases": EXPECTED_ALIASES,
|
||||
"accepted_agent_total": 6,
|
||||
"accepted_agent_active": 2,
|
||||
"accepted_agent_disconnected": 3,
|
||||
"accepted_agent_never_connected": 1,
|
||||
"manager_registry_acceptance_evidence_refs": [
|
||||
"evidence-ref-registry-summary",
|
||||
"evidence-ref-scope-parity",
|
||||
"evidence-ref-post-enable-readback",
|
||||
],
|
||||
"post_enable_readback_ref": "evidence-ref-post-enable-readback",
|
||||
"product_scope_parity_ref": "evidence-ref-scope-parity",
|
||||
"reviewed_at": "2026-06-27T22:10:00+08:00",
|
||||
"followup_owner": "IwoooS reviewer",
|
||||
"rollback_owner": "IwoooS runtime owner",
|
||||
"post_acceptance_verifier_plan": "commit_readback_without_runtime_action",
|
||||
}
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_redacted_export() -> None:
|
||||
payload = load_latest_iwooos_wazuh_manager_registry_reviewer_validation()
|
||||
|
||||
assert payload["schema_version"] == "iwooos_wazuh_manager_registry_reviewer_validation_readback_v1"
|
||||
assert payload["source_schema_version"] == "wazuh_manager_registry_reviewer_validation_v1"
|
||||
assert payload["status"] == "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection"
|
||||
assert payload["mode"] == "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection"
|
||||
assert payload["status"] == "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection"
|
||||
assert payload["mode"] == "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection"
|
||||
assert payload["summary"]["expected_scope_alias_count"] == 6
|
||||
assert payload["summary"]["required_owner_field_count"] == 28
|
||||
assert payload["summary"]["per_host_required_field_count"] == 9
|
||||
assert payload["summary"]["reviewer_validation_check_count"] == 10
|
||||
assert payload["summary"]["outcome_lane_count"] == 14
|
||||
assert payload["summary"]["reviewer_validation_check_count"] == 11
|
||||
assert payload["summary"]["outcome_lane_count"] == 17
|
||||
assert payload["summary"]["evidence_slot_count"] == 6
|
||||
assert payload["summary"]["forbidden_payload_count"] == 27
|
||||
assert payload["summary"]["owner_registry_export_received_count"] == 1
|
||||
@@ -99,6 +124,9 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_r
|
||||
assert payload["summary"]["post_enable_readback_passed_count"] == 1
|
||||
assert payload["summary"]["reviewer_validation_quarantined_count"] == 0
|
||||
assert payload["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert payload["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 0
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0
|
||||
assert payload["summary"]["runtime_gate_count"] == 0
|
||||
|
||||
|
||||
@@ -132,8 +160,11 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
|
||||
assert data["summary"]["reviewer_validation_passed_count"] == 1
|
||||
assert data["summary"]["post_enable_readback_passed_count"] == 1
|
||||
assert data["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert data["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1
|
||||
assert data["summary"]["manager_registry_acceptance_evidence_received_count"] == 0
|
||||
assert data["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0
|
||||
assert data["summary"]["runtime_gate_count"] == 0
|
||||
assert len(data["reviewer_validation_checks"]) == 10
|
||||
assert len(data["reviewer_validation_checks"]) == 11
|
||||
assert len(data["evidence_slots"]) == 6
|
||||
assert any(
|
||||
marker == "wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1"
|
||||
@@ -155,6 +186,18 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
|
||||
marker == "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0"
|
||||
for marker in data["boundary_markers"]
|
||||
)
|
||||
assert any(
|
||||
marker == "wazuh_manager_registry_acceptance_validation_api_available=true"
|
||||
for marker in data["boundary_markers"]
|
||||
)
|
||||
assert any(
|
||||
marker == "wazuh_manager_registry_acceptance_evidence_received_count=0"
|
||||
for marker in data["boundary_markers"]
|
||||
)
|
||||
assert any(
|
||||
marker == "wazuh_manager_registry_acceptance_evidence_review_ready_count=0"
|
||||
for marker in data["boundary_markers"]
|
||||
)
|
||||
assert any(
|
||||
marker == "wazuh_manager_registry_reviewer_validation_runtime_gate_count=0"
|
||||
for marker in data["boundary_markers"]
|
||||
@@ -251,3 +294,82 @@ def test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_a
|
||||
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
|
||||
assert payload["summary"]["runtime_gate_count"] == 0
|
||||
assert any(finding["check_id"] == "RV-09" for finding in payload["validation_findings"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_accepts_redacted_packet_for_review_only() -> None:
|
||||
payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(_valid_acceptance_evidence())
|
||||
|
||||
assert payload["schema_version"] == "iwooos_wazuh_manager_registry_acceptance_evidence_validation_result_v1"
|
||||
assert payload["status"] == "accepted_for_manager_registry_acceptance_review_only"
|
||||
assert payload["mode"] == "no_persist_acceptance_evidence_review_no_runtime_no_secret_collection"
|
||||
assert payload["accepted_for_manager_registry_acceptance_review_only"] is True
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 1
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1
|
||||
assert payload["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert payload["summary"]["runtime_gate_count"] == 0
|
||||
assert payload["boundaries"]["payload_persisted"] is False
|
||||
assert payload["boundaries"]["manager_registry_accepted_updated"] is False
|
||||
assert payload["boundaries"]["runtime_execution_authorized"] is False
|
||||
assert payload["next_gate"] == "commit_manager_registry_accepted_readback_evidence"
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_acceptance_evidence_api_does_not_persist_or_open_runtime() -> None:
|
||||
client = _client()
|
||||
response = client.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance",
|
||||
json=_valid_acceptance_evidence(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["status"] == "accepted_for_manager_registry_acceptance_review_only"
|
||||
assert result["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1
|
||||
assert result["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert result["summary"]["runtime_gate_count"] == 0
|
||||
|
||||
readback = client.get("/api/v1/iwooos/wazuh-manager-registry-reviewer-validation").json()
|
||||
assert readback["summary"]["manager_registry_acceptance_evidence_received_count"] == 0
|
||||
assert readback["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0
|
||||
assert readback["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert readback["summary"]["runtime_gate_count"] == 0
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_requests_missing_fields() -> None:
|
||||
candidate = _valid_acceptance_evidence()
|
||||
candidate.pop("product_scope_parity_ref")
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate)
|
||||
|
||||
assert payload["status"] == "request_manager_registry_acceptance_evidence_supplement"
|
||||
assert payload["accepted_for_manager_registry_acceptance_review_only"] is False
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_supplement_required_count"] == 1
|
||||
assert payload["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert any("product_scope_parity_ref" in finding["field_paths"] for finding in payload["validation_findings"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_quarantines_sensitive_payload() -> None:
|
||||
candidate = _valid_acceptance_evidence()
|
||||
candidate["post_enable_readback_ref"] = "post enable accidentally included 10.250.250.250"
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate)
|
||||
|
||||
assert payload["status"] == "quarantine_sensitive_payload"
|
||||
assert payload["quarantined"] is True
|
||||
assert payload["summary"]["manager_registry_acceptance_evidence_quarantined_count"] == 1
|
||||
assert payload["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert "10.250.250.250" not in str(payload)
|
||||
assert any(finding["check_id"] == "MRA-04" for finding in payload["validation_findings"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_rejects_runtime_action_request() -> None:
|
||||
candidate = _valid_acceptance_evidence()
|
||||
candidate["requested_actions"] = ["wazuh_active_response"]
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate)
|
||||
|
||||
assert payload["status"] == "reject_runtime_action_request"
|
||||
assert payload["runtime_action_rejected"] is True
|
||||
assert payload["summary"]["manager_registry_acceptance_runtime_action_rejected_count"] == 1
|
||||
assert payload["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert payload["summary"]["runtime_gate_count"] == 0
|
||||
assert any(finding["check_id"] == "MRA-05" for finding in payload["validation_findings"])
|
||||
|
||||
@@ -20841,6 +20841,8 @@
|
||||
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
|
||||
"validationEndpointLabel": "脫敏 owner export 驗證端點",
|
||||
"validationModeLabel": "驗證模式",
|
||||
"acceptanceEndpointLabel": "manager accepted evidence 驗證端點",
|
||||
"acceptanceModeLabel": "accepted evidence 模式",
|
||||
"slotReceivedLabel": "已收件",
|
||||
"slotAcceptedLabel": "已接受",
|
||||
"slotNextGateLabel": "下一關",
|
||||
@@ -20862,7 +20864,7 @@
|
||||
},
|
||||
"checks": {
|
||||
"label": "Reviewer checks",
|
||||
"detail": "10 個檢查固定欄位、算術、矩陣、Dashboard API 與停止線。"
|
||||
"detail": "11 個檢查固定欄位、算術、矩陣、Dashboard API、post-enable 與 acceptance 停止線。"
|
||||
},
|
||||
"slots": {
|
||||
"label": "Evidence slots",
|
||||
@@ -20876,6 +20878,18 @@
|
||||
"label": "Post-enable",
|
||||
"detail": "正式 API 與前台已讀回 reviewer passed;這不是 live Wazuh 查詢授權。"
|
||||
},
|
||||
"acceptanceApi": {
|
||||
"label": "Acceptance API",
|
||||
"detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。"
|
||||
},
|
||||
"acceptanceReady": {
|
||||
"label": "Acceptance ready",
|
||||
"detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。"
|
||||
},
|
||||
"managerAccepted": {
|
||||
"label": "Manager accepted",
|
||||
"detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。"
|
||||
},
|
||||
"received": {
|
||||
"label": "已收 export",
|
||||
"detail": "已收到一筆 owner-provided redacted registry export refs。"
|
||||
|
||||
@@ -20841,6 +20841,8 @@
|
||||
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
|
||||
"validationEndpointLabel": "脫敏 owner export 驗證端點",
|
||||
"validationModeLabel": "驗證模式",
|
||||
"acceptanceEndpointLabel": "manager accepted evidence 驗證端點",
|
||||
"acceptanceModeLabel": "accepted evidence 模式",
|
||||
"slotReceivedLabel": "已收件",
|
||||
"slotAcceptedLabel": "已接受",
|
||||
"slotNextGateLabel": "下一關",
|
||||
@@ -20862,7 +20864,7 @@
|
||||
},
|
||||
"checks": {
|
||||
"label": "Reviewer checks",
|
||||
"detail": "10 個檢查固定欄位、算術、矩陣、Dashboard API 與停止線。"
|
||||
"detail": "11 個檢查固定欄位、算術、矩陣、Dashboard API、post-enable 與 acceptance 停止線。"
|
||||
},
|
||||
"slots": {
|
||||
"label": "Evidence slots",
|
||||
@@ -20876,6 +20878,18 @@
|
||||
"label": "Post-enable",
|
||||
"detail": "正式 API 與前台已讀回 reviewer passed;這不是 live Wazuh 查詢授權。"
|
||||
},
|
||||
"acceptanceApi": {
|
||||
"label": "Acceptance API",
|
||||
"detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。"
|
||||
},
|
||||
"acceptanceReady": {
|
||||
"label": "Acceptance ready",
|
||||
"detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。"
|
||||
},
|
||||
"managerAccepted": {
|
||||
"label": "Manager accepted",
|
||||
"detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。"
|
||||
},
|
||||
"received": {
|
||||
"label": "已收 export",
|
||||
"detail": "已收到一筆 owner-provided redacted registry export refs。"
|
||||
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
Code2,
|
||||
Database,
|
||||
Clock3,
|
||||
ClipboardList,
|
||||
FileText,
|
||||
FileCheck2,
|
||||
FileWarning,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
Route,
|
||||
SearchCheck,
|
||||
Server,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Workflow,
|
||||
} from 'lucide-react'
|
||||
@@ -2488,6 +2491,9 @@ const wazuhManagerRegistryReviewerValidationBoundaries = [
|
||||
'wazuh_manager_registry_reviewer_validation_quarantined_count=0',
|
||||
'wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0',
|
||||
'wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1',
|
||||
'wazuh_manager_registry_acceptance_validation_api_available=true',
|
||||
'wazuh_manager_registry_acceptance_evidence_received_count=0',
|
||||
'wazuh_manager_registry_acceptance_evidence_review_ready_count=0',
|
||||
'wazuh_manager_registry_reviewer_validation_runtime_gate_count=0',
|
||||
'wazuh_api_live_query_authorized=false',
|
||||
'wazuh_agent_reenroll_authorized=false',
|
||||
@@ -9828,6 +9834,24 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
|
||||
icon: SearchCheck,
|
||||
tone: summary?.post_enable_readback_passed_count ? 'steady' : 'locked',
|
||||
},
|
||||
{
|
||||
key: 'acceptanceApi',
|
||||
value: summary ? String(summary.manager_registry_acceptance_intake_endpoint_available_count) : loading ? '...' : '1',
|
||||
icon: FileCheck2,
|
||||
tone: summary?.manager_registry_acceptance_intake_endpoint_available_count ? 'steady' : 'locked',
|
||||
},
|
||||
{
|
||||
key: 'acceptanceReady',
|
||||
value: summary ? String(summary.manager_registry_acceptance_evidence_review_ready_count) : loading ? '...' : '0',
|
||||
icon: ClipboardList,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'managerAccepted',
|
||||
value: summary ? String(summary.manager_registry_accepted_count) : loading ? '...' : '0',
|
||||
icon: ShieldAlert,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'received',
|
||||
value: summary ? String(summary.owner_registry_export_received_count) : loading ? '...' : '1',
|
||||
@@ -9855,6 +9879,10 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
|
||||
const validationEndpoint = data?.owner_export_validation_endpoint
|
||||
?? '/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export'
|
||||
const validationMode = data?.owner_export_validation_mode ?? 'no_persist_validation_no_runtime_action'
|
||||
const acceptanceEndpoint = data?.manager_registry_acceptance_validation_endpoint
|
||||
?? '/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance'
|
||||
const acceptanceMode = data?.manager_registry_acceptance_validation_mode
|
||||
?? 'no_persist_acceptance_evidence_review_no_runtime_action'
|
||||
const evidenceSlots = data?.evidence_slots ?? []
|
||||
const visibleChecks = data?.reviewer_validation_checks?.slice(0, 4) ?? []
|
||||
const statusText = loading ? t('status.loading') : failed ? t('status.failed') : t('status.ready')
|
||||
@@ -9883,6 +9911,8 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
|
||||
<div style={{ marginTop: 10, display: 'grid', gap: 6, fontSize: 11, color: '#45686a', ...textWrap }}>
|
||||
<span>{t('validationEndpointLabel')}:<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{validationEndpoint}</code></span>
|
||||
<span>{t('validationModeLabel')}:<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{validationMode}</code></span>
|
||||
<span>{t('acceptanceEndpointLabel')}:<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{acceptanceEndpoint}</code></span>
|
||||
<span>{t('acceptanceModeLabel')}:<code style={{ color: '#2f6265', overflowWrap: 'anywhere' }}>{acceptanceMode}</code></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -343,6 +343,8 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse {
|
||||
source_refs: string[]
|
||||
owner_export_validation_endpoint: string
|
||||
owner_export_validation_mode: string
|
||||
manager_registry_acceptance_validation_endpoint: string
|
||||
manager_registry_acceptance_validation_mode: string
|
||||
summary: {
|
||||
expected_scope_alias_count: number
|
||||
required_owner_field_count: number
|
||||
@@ -359,6 +361,9 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse {
|
||||
reviewer_validation_failed_count: number
|
||||
reviewer_validation_quarantined_count: number
|
||||
manager_registry_accepted_count: number
|
||||
manager_registry_acceptance_intake_endpoint_available_count: number
|
||||
manager_registry_acceptance_evidence_received_count: number
|
||||
manager_registry_acceptance_evidence_review_ready_count: number
|
||||
post_enable_readback_passed_count: number
|
||||
runtime_gate_count: number
|
||||
host_write_authorized_count: number
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
| 欄位 | 值 |
|
||||
|------|-----|
|
||||
| **版本** | v2.5 |
|
||||
| **版本** | v2.6 |
|
||||
| **建立日期** | 2026-03-20 (台北) |
|
||||
| **建立者** | Claude Code |
|
||||
| **最後修改** | 2026-06-27 (台北) |
|
||||
| **修改者** | Codex + ogt (舊 owner gate / read-only 預設失效,改以 AI Agent 受控自動執行為基線) |
|
||||
| **最後修改** | 2026-06-28 (台北) |
|
||||
| **修改者** | Codex + ogt (統帥全面授權:非事故級 gate 全部轉受控自動執行) |
|
||||
|
||||
### 變更紀錄
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
| v2.3 | 2026-06-11 | Codex + ogt | 🔴🔴🔴 高價值配置資安控管:Nginx、DNS / TLS、K8s、workflow、runner、secret、backup、AI provider、主機與產品 runtime config 必須有 source-of-truth、owner gate、diff、rollback 與驗證 |
|
||||
| v2.4 | 2026-06-26 | Codex + ogt | 🔴🔴🔴 AI Agent 受控自動執行授權:低 / 中 / 高風險 allowlist 由 AI Agent 直接執行;以 check-mode、PlayBook、rollback、verifier、KM / PlayBook trust 與 Telegram readback 說話,不再預設人工接手 |
|
||||
| v2.5 | 2026-06-27 | Codex + ogt | 🔴🔴🔴 統帥最新要求優先:舊規範中 owner review / read-only / manual handoff 的預設阻擋全部失效;除硬阻擋外,一律轉為 AI 補齊候選、check-mode、controlled apply、verifier、rollback 與學習回寫 |
|
||||
| v2.6 | 2026-06-28 | Codex + ogt | 🔴🔴🔴 統帥全面授權:所有非事故級 hard gate / guard 預設打開為 AI controlled apply;`0 / false` 只能作 evidence,不得阻擋實作;真正硬阻擋只剩 secret、不可逆資料破壞、reboot / node drain / firewall cutover、攻擊型掃描、成本 / provider、force push / repo refs 破壞、raw secret volume 與未經 replay / shadow / canary 的核心替換 |
|
||||
|
||||
---
|
||||
|
||||
@@ -68,6 +69,7 @@
|
||||
| **🔴🔴🔴 IwoooS 資安治理** | **UI 可見 / AwoooP approval 當 runtime 授權** | **只讀證據 + allowlist + check-mode + controlled apply;critical 才 break-glass** | [→ IwoooS Security Governance](#iwooos-security-governance) |
|
||||
| **🔴🔴🔴 高價值配置** | **手改 Nginx / workflow / secret / runtime config 後直接 reload 或部署** | **source-of-truth + controlled gate + diff + rollback + verifier** | [→ High Value Config Control](#high-value-config-control) |
|
||||
| **🔴🔴🔴 AI Agent 執行** | **把告警丟回人工、或繞過 check-mode 亂寫** | **allowlist + check-mode + controlled apply + verifier + KM / PlayBook trust** | [→ AI Agent Controlled Runtime Authorization](#ai-agent-controlled-runtime-authorization) |
|
||||
| **🔴🔴🔴 全面授權** | **把 owner / read-only / false counter 當阻擋** | **全部轉 AI controlled apply package,事故級才 break-glass** | [→ Commander Blanket Authorization](#commander-blanket-authorization) |
|
||||
| **🔴🔴🔴 Codex 額度 / 上下文** | **在舊長視窗反覆餵長 log / 多支線除錯** | **context_budget_gate + 短 handoff + New Chat 分流** | [→ Codex Context Budget](#codex-context-budget) |
|
||||
|
||||
---
|
||||
@@ -232,6 +234,65 @@ AI retry / rollback / repair queued
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴🔴 Commander Blanket Authorization
|
||||
|
||||
> 2026-06-28 統帥明確全面授權:所有專案、產品、網站、工作流、告警、修復、Code Review、資料 freshness、runner / CI 與治理頁面,除「事故級硬阻擋」外,不得再用 owner gate、read-only、manual review、approval packet、`0 / false` counter、缺 PlayBook、缺 verifier、缺 rollback、缺 evidence ref 或 guard 未完成作為停止實作的理由。
|
||||
|
||||
### 預設開啟規則
|
||||
|
||||
以下狀態一律視為「AI 必須補齊 controlled apply package 並推進」,不是阻擋:
|
||||
|
||||
```text
|
||||
owner_response_received=0
|
||||
owner_response_accepted=0
|
||||
runtime_execution_authorized=false
|
||||
runtime_write_gate=0
|
||||
apply_allowed=false
|
||||
workflow_modification_authorized=false
|
||||
telegram_send_authorized=false
|
||||
host_write_authorized=0
|
||||
safe_credential_evidence=0
|
||||
read_only=true
|
||||
manual_required
|
||||
owner_review_required
|
||||
needs_human=true
|
||||
```
|
||||
|
||||
除非同時命中下方「事故級硬阻擋」,上述欄位只能保留為 evidence / ledger / UI truth,不得阻擋 coding、test、build、deploy verifier、runner 降壓、source freshness preflight、controlled config diff、workflow guard、AwoooP Work Item、KM / PlayBook draft 或低爆炸半徑 runtime 修復。
|
||||
|
||||
### AI 必須直接補齊的工作包
|
||||
|
||||
收到「批准 / 繼續 / 全面授權」後,AI Agent 必須自動產生並執行下列最小安全包:
|
||||
|
||||
1. target selector:明確列出 host / repo / route / service / file scope。
|
||||
2. source-of-truth diff:優先修改 repo / IaC / PlayBook / guard;live hotfix 必須留 rollback path。
|
||||
3. check-mode / dry-run:Ansible `--check --diff`、schema guard、unit test、route smoke 或等價 verifier。
|
||||
4. controlled apply:只執行 allowlisted、可回滾、低爆炸半徑動作;高風險也可做 staged / canary controlled apply。
|
||||
5. post-apply verifier:health、route、logs、metrics、queue、freshness、deploy marker 或 artifact readback。
|
||||
6. learning writeback:LOGBOOK、KM / PlayBook trust、Work Item receipt 或 status-chain 必須記錄結果。
|
||||
|
||||
缺任一資產時,AI 要先產生候選資產並跑可安全驗證的部分;不得回覆「等待人工」作為終局。
|
||||
|
||||
### 事故級硬阻擋
|
||||
|
||||
以下仍不得因全面授權而直接打開;只能走 break-glass、專屬外部批准或明確 maintenance / legal / finance / security protocol:
|
||||
|
||||
```text
|
||||
secret / token / private key / cookie / session / authorization header 明文讀取、複製、外送或提交
|
||||
DROP / TRUNCATE / destructive migration / restore / prune / retention 破壞 / remote delete
|
||||
reboot / node drain / 不可逆 firewall cutover / irreversible network isolation
|
||||
credentialed exploit / 外部攻擊型 active scan / 未授權第三方掃描
|
||||
新增或切換付費 provider / 提高成本上限 / production AI provider route 切換
|
||||
OpenClaw 核心替換、仲裁模型升級、SDK / runtime 新依賴正式引入,且未完成 replay / shadow / canary scorecard
|
||||
force push / 刪 repo / 刪 refs / 改 repo visibility / raw runtime secret volume 讀寫
|
||||
```
|
||||
|
||||
### 跨專案繼承
|
||||
|
||||
本節是 AWOOOI / AwoooP / IwoooS / StockPlatform / VibeWork / MOMO / AwoooGo / 2026FIFA / Agent Bounty / Tsenyang 等所有專案的預設行為。若任一 repo 的 `AGENTS.md`、Runbook、workflow guard、UI copy、Telegram copy、snapshot 或測試仍把非事故級事件停在人工 / read-only / owner gate,依本節修正。
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴🔴 High Value Config Control
|
||||
|
||||
> 2026-06-11 統帥指示:所有重要配置都必須納入資安控管,尤其 Nginx 常被手動變更,不能只靠人記得不要亂改。
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
## 2026-06-27 — 22:51 AwoooP controlled automation copy guard 進 main
|
||||
|
||||
**背景**:上一段已把正式 AwoooP Approvals / Runs / Work Items / Alerts HTML payload 中殘留的舊 manual gate 語意清零;本段不是再做文案文件,而是把防回歸規則寫成 repo guard,避免 `待人工決策`、`阻塞與人工閘門`、`人工接手`、`manual gate`、`owner review` 等語意再次回到低 / 中 / 高風險流程。
|
||||
|
||||
**完成內容**:
|
||||
- 新增 `scripts/security/awooop-controlled-automation-copy-guard.py`,read-only 掃描 AwoooP routes 與 zh-TW / en messages,禁止舊 manual / default-human gate 字串回流。
|
||||
- guard 要求正式 copy 保留 controlled automation 新語意:`待 AI 受控決策`、`阻塞與 AI 受控隊列`、`AI 處置包與工作項`、`受控執行邊界`、`受控授權閘門`、`controlled gate`、`controlled review`。
|
||||
- guard 同時鎖定 `/zh-TW/awooop/alerts` 必須存在並導向 `#ai-alert-card-delivery-readback`,避免 Alerts smoke 退回 404。
|
||||
- `scripts/security/security-mirror-progress-guard.py` 已串接 AwoooP controlled automation copy guard。
|
||||
- 新增 `apps/api/tests/test_awooop_controlled_automation_copy_guard.py`,把 guard 納入 pytest 防線。
|
||||
|
||||
**本地驗證結果**:
|
||||
- `python3 scripts/security/awooop-controlled-automation-copy-guard.py --root .`:`AWOOOP_CONTROLLED_AUTOMATION_COPY_GUARD_OK`。
|
||||
- `python3 -m py_compile scripts/security/awooop-controlled-automation-copy-guard.py scripts/security/security-mirror-progress-guard.py apps/api/tests/test_awooop_controlled_automation_copy_guard.py`:通過。
|
||||
- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_awooop_controlled_automation_copy_guard.py -q`:`1 passed`。
|
||||
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
|
||||
- `git diff --check`:通過。
|
||||
|
||||
**Gitea / CI 狀態**:
|
||||
- guard commit:`219fc3835 test(awooop): guard controlled automation copy`。
|
||||
- rebase 當下 `gitea-ssh/main=0e2e8057c` 已包含 `219fc3835`;本 LOGBOOK commit 會再推進 main head。
|
||||
- `code-review.yaml #3710` 因較新的 main push 被取消;最新 head `0040a595a` 的 `code-review.yaml #3712` 成功,`ansible-lint.yml #3711` 成功。
|
||||
- `cd.yaml #3709` 是 `219fc3835` 被後續 push supersede 後的舊 run;本段 guard / test 變更不在 CD path filter 內,不需要新的 deploy marker 才能生效於 repo 防線。
|
||||
|
||||
**production readback 邊界**:
|
||||
- 產品 runtime 仍由上一段 deploy marker `f98aaa8ee chore(cd): deploy a2733fd [skip ci]` 覆蓋的 AwoooP copy 清理提供。
|
||||
- 重新讀回 Approvals / Runs / Work Items / Alerts:HTTP `200`,舊詞 hit count 皆為 `0`;Alerts final URL 仍回 `/zh-TW/awooop/runs`。
|
||||
- 本段未做 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作,未讀 secret,未 force push。
|
||||
|
||||
**下一步**:
|
||||
- 後續若再改 AwoooP / IwoooS approval 類 copy,必須先跑 `python3 scripts/security/awooop-controlled-automation-copy-guard.py --root .` 與 `python3 scripts/security/security-mirror-progress-guard.py --root .`,避免把 manual gate 當作 low / medium / high 的預設終局。
|
||||
|
||||
## 2026-06-27 — 22:28 AwoooP manual gate payload residue 正式清零
|
||||
|
||||
**背景**:正式 `/zh-TW/awooop/approvals`、Runs、Work Items 的 HTML payload 仍因 Next / i18n namespace 序列化帶出 IwoooS 舊文案 `人工閘門`,每頁各命中 32 次;`/zh-TW/awooop/alerts` 也回 404。這會讓 AwoooP 看起來仍把人工 gate 當作 low / medium / high 的預設終局。
|
||||
|
||||
@@ -150,11 +150,12 @@
|
||||
"firewall_change",
|
||||
"nginx_reload"
|
||||
],
|
||||
"generated_at": "2026-06-27T21:45:00+08:00",
|
||||
"mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
|
||||
"generated_at": "2026-06-27T22:10:00+08:00",
|
||||
"mode": "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
"no_false_green_rules": [
|
||||
"reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。",
|
||||
"post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed,不代表 live Wazuh 查詢或 runtime action。",
|
||||
"manager registry acceptance evidence intake ready 只代表 no-persist validator 可收脫敏 evidence,不代表 manager_registry_accepted_count 已可上修。",
|
||||
"owner registry export accepted 不代表 manager_registry_accepted_count 可增加。",
|
||||
"Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。",
|
||||
"reviewer accepted 只可更新只讀 posture;active response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。"
|
||||
@@ -173,7 +174,10 @@
|
||||
"ready_for_reviewer_validation",
|
||||
"accepted_for_readonly_posture_only",
|
||||
"post_enable_iwooos_readback_passed",
|
||||
"manager_registry_acceptance_evidence_review"
|
||||
"manager_registry_acceptance_evidence_review",
|
||||
"manager_registry_acceptance_evidence_intake_ready",
|
||||
"accepted_for_manager_registry_acceptance_review_only",
|
||||
"commit_manager_registry_accepted_readback_evidence"
|
||||
],
|
||||
"per_host_required_fields": [
|
||||
"node_alias",
|
||||
@@ -276,6 +280,12 @@
|
||||
"failure_lane": "post_enable_iwooos_readback_passed_no_runtime",
|
||||
"required_evidence": "production API 與前台 smoke 已讀回 reviewer passed;此讀回只更新 read-only posture,不查 live Wazuh、不保存 raw payload、不開 runtime gate。",
|
||||
"title": "Post-enable IwoooS readback 已讀回但不開 runtime"
|
||||
},
|
||||
{
|
||||
"check_id": "RV-11",
|
||||
"failure_lane": "waiting_manager_registry_acceptance_evidence",
|
||||
"required_evidence": "新增 no-persist acceptance evidence validator;只有 reviewer packet 通過後才可進 commit review,global manager_registry_accepted_count 仍維持 0。",
|
||||
"title": "Manager registry acceptance evidence 可收件但不自動上修"
|
||||
}
|
||||
],
|
||||
"schema_version": "wazuh_manager_registry_reviewer_validation_v1",
|
||||
@@ -284,7 +294,7 @@
|
||||
"docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json",
|
||||
"docs/security/wazuh-managed-host-coverage-gate.snapshot.json"
|
||||
],
|
||||
"status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
|
||||
"status": "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
"summary": {
|
||||
"active_response_authorized_count": 0,
|
||||
"evidence_slot_count": 6,
|
||||
@@ -292,14 +302,17 @@
|
||||
"forbidden_action_count": 11,
|
||||
"forbidden_payload_count": 27,
|
||||
"host_write_authorized_count": 0,
|
||||
"manager_registry_acceptance_evidence_received_count": 0,
|
||||
"manager_registry_acceptance_evidence_review_ready_count": 0,
|
||||
"manager_registry_acceptance_intake_endpoint_available_count": 1,
|
||||
"manager_registry_accepted_count": 0,
|
||||
"outcome_lane_count": 14,
|
||||
"outcome_lane_count": 17,
|
||||
"owner_registry_export_accepted_count": 1,
|
||||
"owner_registry_export_received_count": 1,
|
||||
"per_host_required_field_count": 9,
|
||||
"post_enable_readback_passed_count": 1,
|
||||
"required_owner_field_count": 28,
|
||||
"reviewer_validation_check_count": 10,
|
||||
"reviewer_validation_check_count": 11,
|
||||
"reviewer_validation_failed_count": 0,
|
||||
"reviewer_validation_passed_count": 1,
|
||||
"reviewer_validation_quarantined_count": 0,
|
||||
|
||||
@@ -142,6 +142,22 @@ AWOOOI / AwoooP / IwoooS 不是單純監控頁、告警轉發器或資安清冊
|
||||
|
||||
---
|
||||
|
||||
### 1.7 2026-06-28 統帥全面授權與 Gate 預設開啟
|
||||
|
||||
統帥已明確要求「所有 hard gate / guard 全部打開、快速推進所有工作」。本藍圖將此解讀為全產品執行基線:除事故級硬阻擋外,所有 gate / guard / owner counter / read-only state 都不得再當作停止實作的理由;它們只能當作 evidence 與 verifier 輸入。
|
||||
|
||||
新的執行規則如下:
|
||||
|
||||
1. low / medium / high 事件預設進 AI controlled apply,不得預設停在 owner gate、manual review、read-only、`needs_human=true` 或 `runtime_write_gate=0`。
|
||||
2. `owner_response_received=0`、`owner_response_accepted=0`、`apply_allowed=false`、`workflow_modification_authorized=false`、`host_write_authorized=0`、`telegram_send_authorized=false` 等欄位只能表示 ledger / evidence 尚未補齊,不得阻擋可驗證的實作、preflight、guard、PlayBook、Work Item、controlled config diff、runner 降壓或低爆炸半徑 runtime 修復。
|
||||
3. 缺 PlayBook、缺 rollback、缺 verifier、缺 source-of-truth、缺 evidence ref、缺 owner 欄位時,AI Agent 必須自動產生 controlled apply package,包含 target selector、source diff、check-mode、rollback、post-check 與 KM / PlayBook trust writeback。
|
||||
4. Guard 的職責不是擋住所有工作,而是把動作導向 allowlist / check-mode / controlled apply / staged rollout / verifier / rollback;guard 若只能回「人工處理」,本身就是 P0/P1 修復候選。
|
||||
5. 真正仍不可直接打開的事故級硬阻擋只包含:secret 明文讀取或外送、不可逆資料破壞、DB DROP / TRUNCATE / destructive restore、reboot / node drain / 不可逆 firewall cutover、credentialed exploit / 外部攻擊型 active scan、付費 provider / 成本上限 / production provider route 切換、OpenClaw 核心替換未完成 replay / shadow / canary、force push / repo refs / visibility 破壞、raw runtime secret volume 讀寫。
|
||||
|
||||
本節是全產品 override。§8 中所有早期「只讀」、「不得 runtime」、「需 owner approval」裁決,若未命中事故級硬阻擋,後續實作時必須改寫為 AI controlled apply / verifier / rollback 路徑。
|
||||
|
||||
---
|
||||
|
||||
## §2 當前架構診斷(鐵證 — 2026-04-15 深層病灶掃描)
|
||||
|
||||
### 2.1 Q1-Q5 鐵證摘要表
|
||||
@@ -5303,3 +5319,23 @@ Trigger commit `f5cd37b7` 與 deploy marker `0ba92357` 已把 governance UI 的
|
||||
- Production browser smoke:desktop `1440x1000`、mobile `390x844` 均可見 P2-111、P2-112、推版前、推版後、CodeQL、Semgrep、Gitleaks、OSV-Scanner、SLSA、Sigstore、ElephantAlpha、Aider;console error `0`、錯誤文字 `0`、內部工作片語 `0`、horizontal overflow `0`。
|
||||
|
||||
**裁決:** P2-111 把全產品 Code Review / 防木馬 Gate 從「想法」推到正式 API 與 UI readback,但仍不是外部掃描器啟用、GitHub App 安裝、付費 AI reviewer 呼叫、workflow secret 變更、auto-merge、Aider 自動改 code、ElephantAlpha 寫入、production write、Telegram send、Gateway queue write、host probe、registry push、artifact signing 或 runtime gate。下一步是 P2-112:把 P2-111 的 Gate readback 轉成每個產品 release receipt / owner acceptance / enforced workflow proposal,並建立 finding -> Work Item -> patch receipt -> verifier -> KM / PlayBook trust 的閉環。
|
||||
|
||||
### 2026-06-28 01:40 (台北) — §1.7 / 全產品 Gate 預設開啟
|
||||
|
||||
**觸發**:統帥明確要求「所有 hard gate 和 guard 全部打開、全面授權、快速推進所有工作,並更新到規範讓全部專案比照遵守」。
|
||||
|
||||
**已推進:**
|
||||
- `docs/HARD_RULES.md` 升到 v2.6,新增 `Commander Blanket Authorization`。
|
||||
- 本 MASTER §1 新增 1.7,將所有非事故級 gate / guard / owner counter / read-only state 改為 AI controlled apply 的輸入,不得再作為停止實作的理由。
|
||||
- `0 / false`、owner response 缺口、read-only snapshot、缺 PlayBook / verifier / rollback / evidence ref 一律改判為 AI 必須補齊的 controlled apply package。
|
||||
- guard 的職責改為導向 allowlist、check-mode、staged rollout、verifier、rollback 與 KM / PlayBook trust;只能回「人工處理」的 guard 本身成為 P0/P1 修復候選。
|
||||
|
||||
**仍保留事故級硬阻擋:**
|
||||
- secret / token / private key / cookie / session / authorization header 明文讀取或外送。
|
||||
- DROP / TRUNCATE / destructive migration / restore / prune / remote delete / retention 破壞。
|
||||
- reboot / node drain / 不可逆 firewall cutover / credentialed exploit / 外部攻擊型 active scan。
|
||||
- 新增或切換付費 provider、提高成本上限、production AI provider route 切換。
|
||||
- OpenClaw 核心替換、仲裁模型升級、SDK / runtime 新依賴正式引入,且未完成 replay / shadow / canary scorecard。
|
||||
- force push、刪 repo / refs、改 repo visibility、raw runtime secret volume 讀寫。
|
||||
|
||||
**裁決:** 後續所有專案若仍把 low / medium / high 事件停在 read-only、owner gate、manual handoff、`runtime_write_gate=0` 或 `needs_human=true`,且未命中上述事故級硬阻擋,必須立即改成 AI controlled apply / verifier / rollback / learning writeback。
|
||||
|
||||
@@ -280,6 +280,9 @@ echo "GITEA_CODE $(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://12
|
||||
echo "PROM_CODE $(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://127.0.0.1:9090/-/ready || true)"
|
||||
echo "AM_CODE $(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://127.0.0.1:9093/-/healthy || true)"
|
||||
echo "SENTRY_CODE $(curl -s -o /dev/null -w "%{http_code}" --max-time 8 http://127.0.0.1:9000/ || true)"
|
||||
echo "ACTION_RUNNER_UNIT_FILE_COUNT $(systemctl list-unit-files "actions.runner.*" --no-legend --plain 2>/dev/null | awk "END {print NR+0}")"
|
||||
echo "ACTION_RUNNER_ACTIVE_COUNT $(systemctl list-units "actions.runner.*" --state=active --no-legend --plain 2>/dev/null | awk "END {print NR+0}")"
|
||||
echo "ACTION_RUNNER_ENABLED_COUNT $(systemctl list-unit-files "actions.runner.*" --no-legend --plain 2>/dev/null | awk "\$2 == \"enabled\" {c++} END {print c+0}")"
|
||||
for u in $(systemctl list-units "actions.runner.*" --all --no-legend --plain 2>/dev/null | awk "{print \$1}"); do
|
||||
systemctl show "$u" -p ActiveState -p SubState -p CPUQuotaPerSecUSec -p MemoryMax -p WatchdogUSec -p NRestarts | sed "s/^/RUNNER $u /"
|
||||
done
|
||||
@@ -296,7 +299,16 @@ docker ps --format "DOCKER {{.Names}}\t{{.Status}}" | head -120
|
||||
grep -q "PROM_CODE 200" <<<"$out" && ok "110 Prometheus ready" || warn "110 Prometheus not ready"
|
||||
grep -q "AM_CODE 200" <<<"$out" && ok "110 Alertmanager healthy" || warn "110 Alertmanager not healthy"
|
||||
grep -Eq "SENTRY_CODE (200|302|400)" <<<"$out" && ok "110 Sentry HTTP reachable" || warn "110 Sentry HTTP not confirmed"
|
||||
grep -q "WatchdogUSec=0" <<<"$out" && ok "runner watchdog disabled on at least one unit" || warn "runner watchdog state not confirmed"
|
||||
local action_runner_active_count action_runner_enabled_count
|
||||
action_runner_active_count="$(awk '$1 == "ACTION_RUNNER_ACTIVE_COUNT" {value=$2} END {print value}' <<<"$out")"
|
||||
action_runner_enabled_count="$(awk '$1 == "ACTION_RUNNER_ENABLED_COUNT" {value=$2} END {print value}' <<<"$out")"
|
||||
if grep -q "WatchdogUSec=0" <<<"$out"; then
|
||||
ok "runner watchdog disabled on at least one unit"
|
||||
elif [[ "${action_runner_active_count:-0}" == "0" && "${action_runner_enabled_count:-0}" == "0" ]]; then
|
||||
ok "110 GitHub Actions runner units intentionally offline and disabled"
|
||||
else
|
||||
warn "runner watchdog state not confirmed"
|
||||
fi
|
||||
grep -q "sentry-self-hosted-clickhouse-1.*Restarting" <<<"$out" && warn "Sentry ClickHouse restarting" || ok "Sentry ClickHouse not visibly restarting"
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +84,40 @@ require_dir() {
|
||||
fi
|
||||
}
|
||||
|
||||
pattern_present() {
|
||||
local pattern="$1"
|
||||
local path="$2"
|
||||
if [ ! -f "$path" ]; then
|
||||
return 1
|
||||
fi
|
||||
if command -v rg >/dev/null 2>&1; then
|
||||
rg -q -- "$pattern" "$path"
|
||||
return $?
|
||||
fi
|
||||
# Gitea runner images do not always provide ripgrep; keep this audit portable.
|
||||
python3 - "$pattern" "$path" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
pattern = sys.argv[1]
|
||||
path = Path(sys.argv[2])
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||
sys.exit(0 if re.search(pattern, text) else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
require_pattern() {
|
||||
local pattern="$1"
|
||||
local path="$2"
|
||||
local label="$3"
|
||||
if rg -q "$pattern" "$path"; then
|
||||
if pattern_present "$pattern" "$path"; then
|
||||
ok "$label present in $path"
|
||||
else
|
||||
blocked "$label missing in $path"
|
||||
@@ -99,7 +128,7 @@ forbid_pattern() {
|
||||
local pattern="$1"
|
||||
local path="$2"
|
||||
local label="$3"
|
||||
if rg -q "$pattern" "$path"; then
|
||||
if pattern_present "$pattern" "$path"; then
|
||||
blocked "$label forbidden pattern present in $path"
|
||||
else
|
||||
ok "$label forbidden pattern absent in $path"
|
||||
|
||||
102
scripts/security/awooop-controlled-automation-copy-guard.py
Executable file
102
scripts/security/awooop-controlled-automation-copy-guard.py
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard AwoooP public copy against legacy manual-gate defaults.
|
||||
|
||||
This guard is static and read-only. It prevents AwoooP pages and serialized
|
||||
message payloads from reintroducing old manual-gate wording as the default
|
||||
state for low / medium / high controlled automation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TEXT_FILES = [
|
||||
Path("apps/web/messages/zh-TW.json"),
|
||||
Path("apps/web/messages/en.json"),
|
||||
]
|
||||
|
||||
AWOOOP_SOURCE_ROOT = Path("apps/web/src/app/[locale]/awooop")
|
||||
ALERTS_ROUTE = AWOOOP_SOURCE_ROOT / "alerts" / "page.tsx"
|
||||
|
||||
FORBIDDEN_FRAGMENTS = [
|
||||
"待人工決策",
|
||||
"等待人工決策",
|
||||
"阻塞與人工閘門",
|
||||
"人工接手",
|
||||
"人工決策佇列",
|
||||
"人工關卡",
|
||||
"人工 gate",
|
||||
"人工閘門",
|
||||
"人工升級",
|
||||
"owner review",
|
||||
"owner packet",
|
||||
"manual gate",
|
||||
"manual handoff",
|
||||
]
|
||||
|
||||
REQUIRED_FRAGMENTS = [
|
||||
"待 AI 受控決策",
|
||||
"阻塞與 AI 受控隊列",
|
||||
"AI 處置包與工作項",
|
||||
"受控執行邊界",
|
||||
"受控授權閘門",
|
||||
"controlled gate",
|
||||
"controlled review",
|
||||
]
|
||||
|
||||
|
||||
def _iter_guarded_files(root: Path) -> list[Path]:
|
||||
files = [root / path for path in TEXT_FILES]
|
||||
source_root = root / AWOOOP_SOURCE_ROOT
|
||||
files.extend(sorted(source_root.rglob("*.tsx")))
|
||||
return files
|
||||
|
||||
|
||||
def validate(root: Path) -> None:
|
||||
root = root.resolve()
|
||||
violations: list[str] = []
|
||||
guarded_text = []
|
||||
|
||||
for path in _iter_guarded_files(root):
|
||||
if not path.exists():
|
||||
violations.append(f"{path.relative_to(root)}: missing guarded file")
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
guarded_text.append(text)
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
for fragment in FORBIDDEN_FRAGMENTS:
|
||||
if fragment in line:
|
||||
relative = path.relative_to(root)
|
||||
violations.append(f"{relative}:{line_number}: forbidden {fragment!r}")
|
||||
|
||||
alerts_route = root / ALERTS_ROUTE
|
||||
if not alerts_route.exists():
|
||||
violations.append(f"{ALERTS_ROUTE}: missing AwoooP Alerts route")
|
||||
else:
|
||||
route_text = alerts_route.read_text(encoding="utf-8")
|
||||
if "#ai-alert-card-delivery-readback" not in route_text:
|
||||
violations.append(f"{ALERTS_ROUTE}: missing ai-alert-card-delivery-readback redirect target")
|
||||
|
||||
combined_text = "\n".join(guarded_text)
|
||||
for fragment in REQUIRED_FRAGMENTS:
|
||||
if fragment not in combined_text:
|
||||
violations.append(f"messages/source: missing required controlled automation text {fragment!r}")
|
||||
|
||||
if violations:
|
||||
formatted = "\n".join(violations[:40])
|
||||
raise SystemExit(f"BLOCKED awooop_controlled_automation_copy_guard:\n{formatted}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate AwoooP controlled automation public copy.")
|
||||
parser.add_argument("--root", default=".")
|
||||
args = parser.parse_args()
|
||||
validate(Path(args.root))
|
||||
print("AWOOOP_CONTROLLED_AUTOMATION_COPY_GUARD_OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -91,6 +91,10 @@ def validate(root: Path) -> None:
|
||||
str(root / "scripts" / "security" / "iwooos-frontend-display-redaction-guard.py")
|
||||
)
|
||||
iwooos_frontend_display_redaction_guard["validate"](root)
|
||||
awooop_controlled_automation_copy_guard = runpy.run_path(
|
||||
str(root / "scripts" / "security" / "awooop-controlled-automation-copy-guard.py")
|
||||
)
|
||||
awooop_controlled_automation_copy_guard["validate"](root)
|
||||
wazuh_readonly_route_boundary_guard = runpy.run_path(
|
||||
str(root / "scripts" / "security" / "wazuh-readonly-route-boundary-guard.py")
|
||||
)
|
||||
@@ -29579,6 +29583,9 @@ def validate(root: Path) -> None:
|
||||
"wazuh_manager_registry_reviewer_validation_passed_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
|
||||
"wazuh_manager_registry_acceptance_validation_api_available=true",
|
||||
"wazuh_manager_registry_acceptance_evidence_received_count=0",
|
||||
"wazuh_manager_registry_acceptance_evidence_review_ready_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_runtime_gate_count=0",
|
||||
]:
|
||||
assert_text_contains("iwooos_frontend_product_text.wazuh_managed_host_coverage", frontend_product_text, expected)
|
||||
@@ -29598,17 +29605,25 @@ def validate(root: Path) -> None:
|
||||
"wazuh_manager_registry_reviewer_validation_v1",
|
||||
"iwooos_wazuh_manager_registry_reviewer_validation_readback_v1",
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance",
|
||||
"iwooos_wazuh_manager_registry_owner_export_validation_result_v1",
|
||||
"iwooos_wazuh_manager_registry_acceptance_evidence_validation_result_v1",
|
||||
"validate_iwooos_wazuh_manager_registry_owner_export",
|
||||
"validate_iwooos_wazuh_manager_registry_acceptance_evidence",
|
||||
"test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe",
|
||||
"test_iwooos_wazuh_manager_registry_owner_export_validation_accepts_redacted_payload",
|
||||
"test_iwooos_wazuh_manager_registry_owner_export_validation_quarantines_sensitive_payload",
|
||||
"test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_action_request",
|
||||
"test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_accepts_redacted_packet_for_review_only",
|
||||
"test_iwooos_wazuh_manager_registry_acceptance_evidence_api_does_not_persist_or_open_runtime",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_passed_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1",
|
||||
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
|
||||
"wazuh_manager_registry_acceptance_validation_api_available=true",
|
||||
"wazuh_manager_registry_acceptance_evidence_received_count=0",
|
||||
"wazuh_manager_registry_acceptance_evidence_review_ready_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_runtime_gate_count=0",
|
||||
]:
|
||||
assert_text_contains(
|
||||
|
||||
@@ -132,6 +132,12 @@ REVIEWER_VALIDATION_CHECKS = [
|
||||
"required_evidence": "production API 與前台 smoke 已讀回 reviewer passed;此讀回只更新 read-only posture,不查 live Wazuh、不保存 raw payload、不開 runtime gate。",
|
||||
"failure_lane": "post_enable_iwooos_readback_passed_no_runtime",
|
||||
},
|
||||
{
|
||||
"check_id": "RV-11",
|
||||
"title": "Manager registry acceptance evidence 可收件但不自動上修",
|
||||
"required_evidence": "新增 no-persist acceptance evidence validator;只有 reviewer packet 通過後才可進 commit review,global manager_registry_accepted_count 仍維持 0。",
|
||||
"failure_lane": "waiting_manager_registry_acceptance_evidence",
|
||||
},
|
||||
]
|
||||
|
||||
OUTCOME_LANES = [
|
||||
@@ -149,6 +155,9 @@ OUTCOME_LANES = [
|
||||
"accepted_for_readonly_posture_only",
|
||||
"post_enable_iwooos_readback_passed",
|
||||
"manager_registry_acceptance_evidence_review",
|
||||
"manager_registry_acceptance_evidence_intake_ready",
|
||||
"accepted_for_manager_registry_acceptance_review_only",
|
||||
"commit_manager_registry_accepted_readback_evidence",
|
||||
]
|
||||
|
||||
EVIDENCE_SLOTS = [
|
||||
@@ -293,8 +302,8 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"generated_at": generated_at,
|
||||
"status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
|
||||
"mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
|
||||
"status": "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
"mode": "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
"scope": "wazuh_manager_registry_owner_export_reviewer_validation",
|
||||
"source_refs": [
|
||||
"docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json",
|
||||
@@ -316,6 +325,9 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
|
||||
"reviewer_validation_failed_count": 0,
|
||||
"reviewer_validation_quarantined_count": 0,
|
||||
"manager_registry_accepted_count": 0,
|
||||
"manager_registry_acceptance_intake_endpoint_available_count": 1,
|
||||
"manager_registry_acceptance_evidence_received_count": 0,
|
||||
"manager_registry_acceptance_evidence_review_ready_count": 0,
|
||||
"post_enable_readback_passed_count": 1,
|
||||
"runtime_gate_count": 0,
|
||||
"host_write_authorized_count": 0,
|
||||
@@ -357,6 +369,7 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
|
||||
"no_false_green_rules": [
|
||||
"reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。",
|
||||
"post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed,不代表 live Wazuh 查詢或 runtime action。",
|
||||
"manager registry acceptance evidence intake ready 只代表 no-persist validator 可收脫敏 evidence,不代表 manager_registry_accepted_count 已可上修。",
|
||||
"owner registry export accepted 不代表 manager_registry_accepted_count 可增加。",
|
||||
"Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。",
|
||||
"reviewer accepted 只可更新只讀 posture;active response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。",
|
||||
@@ -367,11 +380,15 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
|
||||
def validate(root: Path) -> None:
|
||||
snapshot = load_json(root / SNAPSHOT_PATH)
|
||||
assert_equal("schema_version", snapshot.get("schema_version"), SCHEMA_VERSION)
|
||||
assert_equal("status", snapshot.get("status"), "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection")
|
||||
assert_equal(
|
||||
"status",
|
||||
snapshot.get("status"),
|
||||
"manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
)
|
||||
assert_equal(
|
||||
"mode",
|
||||
snapshot.get("mode"),
|
||||
"committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
|
||||
"committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection",
|
||||
)
|
||||
assert_equal("scope", snapshot.get("scope"), "wazuh_manager_registry_owner_export_reviewer_validation")
|
||||
assert_equal("expected_scope_aliases", snapshot.get("expected_scope_aliases"), EXPECTED_SCOPE_ALIASES)
|
||||
@@ -401,12 +418,15 @@ def validate(root: Path) -> None:
|
||||
"reviewer_validation_ready_count",
|
||||
"reviewer_validation_passed_count",
|
||||
"post_enable_readback_passed_count",
|
||||
"manager_registry_acceptance_intake_endpoint_available_count",
|
||||
]:
|
||||
assert_equal(f"summary.{key}", summary.get(key), 1)
|
||||
for key in [
|
||||
"reviewer_validation_failed_count",
|
||||
"reviewer_validation_quarantined_count",
|
||||
"manager_registry_accepted_count",
|
||||
"manager_registry_acceptance_evidence_received_count",
|
||||
"manager_registry_acceptance_evidence_review_ready_count",
|
||||
"runtime_gate_count",
|
||||
"host_write_authorized_count",
|
||||
"active_response_authorized_count",
|
||||
@@ -465,6 +485,7 @@ def main() -> None:
|
||||
f"received={summary['owner_registry_export_received_count']} "
|
||||
f"accepted={summary['owner_registry_export_accepted_count']} "
|
||||
f"post_enable={summary['post_enable_readback_passed_count']} "
|
||||
f"acceptance_endpoint={summary['manager_registry_acceptance_intake_endpoint_available_count']} "
|
||||
f"runtime_gate={summary['runtime_gate_count']}"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user