feat(iwooos): add Wazuh registry export preflight
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m42s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-06-25 14:17:12 +08:00
parent 5dbe2870b2
commit ffeab51bc1
8 changed files with 259 additions and 23 deletions

View File

@@ -29476,6 +29476,14 @@ def validate(root: Path) -> None:
]
)
for expected in [
"iwooos-wazuh-owner-evidence-preflight-board",
"wazuhOwnerEvidencePreflight",
"wazuh_agent_visibility_owner_evidence_required_field_count=23",
"wazuh_agent_visibility_owner_evidence_expected_scope_alias_count=6",
"wazuh_agent_visibility_owner_evidence_per_host_required_field_count=9",
"wazuh_agent_visibility_owner_evidence_registry_export_received_count=0",
"wazuh_agent_visibility_owner_evidence_registry_export_accepted_count=0",
"wazuh_agent_visibility_owner_evidence_runtime_gate_count=0",
"iwooos-wazuh-managed-host-coverage-board",
"wazuhManagedHostCoverage",
"wazuh_managed_host_coverage_manager_registry_accepted_count=0",

View File

@@ -24,6 +24,7 @@ REQUIRED_FIELDS = [
"decision",
"decision_reason",
"affected_scope",
"collection_method",
"agent_total",
"agent_active",
"agent_disconnected",
@@ -31,6 +32,10 @@ REQUIRED_FIELDS = [
"last_seen_window_start",
"last_seen_window_end",
"registry_collected_at",
"registry_export_scope_aliases",
"per_host_registry_matrix",
"registry_gap_reason_by_alias",
"registry_export_summary_ref",
"manager_health_ref",
"dashboard_api_status_ref",
"redacted_evidence_refs",
@@ -42,6 +47,9 @@ REQUIRED_FIELDS = [
REVIEWER_CHECKS = [
"欄位齊全且皆為脫敏 metadata。",
"agent_total 不可小於 active + disconnected + never_connected。",
"collection_method 只能是既有唯讀 API 或 manager 端脫敏 CLI 匯出,不可要求 secret 明文。",
"registry_export_scope_aliases 必須完整覆蓋 6 個公開節點別名。",
"per_host_registry_matrix 每列只能使用公開別名不得包含內網位址、agent 原名或 raw payload。",
"last_seen 時間窗需能覆蓋事故觀察區間。",
"manager health ref 與 dashboard API status ref 不可互相替代。",
"redacted evidence refs 不得包含 raw payload、截圖原文或主機完整輸出。",
@@ -68,6 +76,7 @@ FORBIDDEN_PAYLOADS = [
"bearer_token",
"basic_auth",
"password",
"token",
"cookie",
"private_key",
"client_keys",
@@ -89,6 +98,39 @@ FORBIDDEN_TEXT_PATTERNS = [
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
]
REGISTRY_EXPORT_SCOPE_ALIASES = [
"managed_core_node_a",
"managed_core_node_b",
"managed_dev_node_a",
"managed_dev_node_b",
"managed_control_node_a",
"managed_control_node_b",
]
REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS = [
"wazuh_api_readonly_redacted_counts",
"manager_agent_control_redacted_export",
]
PER_HOST_REGISTRY_REQUIRED_FIELDS = [
"node_alias",
"scope_role",
"registry_presence",
"agent_status_bucket",
"last_seen_state",
"manager_group_ref",
"agent_id_redacted_ref",
"gap_reason",
"redacted_evidence_ref",
]
REGISTRY_EXPORT_REDACTION_REQUIREMENTS = [
"只允許公開節點別名,不允許內網位址、主機原名或 agent 原名。",
"agent id 僅能用不可逆 evidence ref不得放完整值、雜湊、前後綴或 client key。",
"每個缺口必須有 gap reason不得以 Dashboard 空白或口頭說明補成綠燈。",
"只收計數、狀態桶、時間窗與證據 ref不收 raw API payload、完整 CLI output 或截圖原文。",
]
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
@@ -143,11 +185,23 @@ def build_snapshot() -> dict[str, Any]:
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"forbidden_payloads": FORBIDDEN_PAYLOADS,
"registry_export_contract": {
"expected_scope_aliases": REGISTRY_EXPORT_SCOPE_ALIASES,
"allowed_collection_methods": REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS,
"per_host_required_fields": PER_HOST_REGISTRY_REQUIRED_FIELDS,
"redaction_requirements": REGISTRY_EXPORT_REDACTION_REQUIREMENTS,
"registry_export_received_count": 0,
"registry_export_accepted_count": 0,
},
"summary": {
"required_field_count": len(REQUIRED_FIELDS),
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"forbidden_payload_count": len(FORBIDDEN_PAYLOADS),
"expected_scope_alias_count": len(REGISTRY_EXPORT_SCOPE_ALIASES),
"per_host_required_field_count": len(PER_HOST_REGISTRY_REQUIRED_FIELDS),
"registry_export_received_count": 0,
"registry_export_accepted_count": 0,
"owner_evidence_received_count": 0,
"owner_evidence_accepted_count": 0,
"owner_evidence_rejected_count": 0,
@@ -171,6 +225,7 @@ def build_snapshot() -> dict[str, Any]:
"operator_interpretation": [
"這是 Wazuh agent registry 脫敏證據收件前的預檢,不代表已收到或已接受 owner evidence。",
"agent service active、TCP 連線存在、Dashboard 可見或口頭宣稱都不可替代 manager registry counts。",
"逐主機 registry export 必須使用固定公開節點別名與狀態桶,不能把 agent 原名或內網識別資訊帶到前台。",
"若 evidence 夾帶 raw log、未脫敏截圖、內網位址、agent 原名或 secret必須隔離不得渲染到前台。",
"任何 active response、host write、firewall、Nginx、Docker、K8s 或 secret 變更都要切獨立人工批准。",
],
@@ -194,13 +249,48 @@ def validate(root: Path) -> None:
assert_equal("reviewer_checks", snapshot.get("reviewer_checks"), REVIEWER_CHECKS)
assert_equal("outcome_lanes", snapshot.get("outcome_lanes"), OUTCOME_LANES)
assert_equal("forbidden_payloads", snapshot.get("forbidden_payloads"), FORBIDDEN_PAYLOADS)
contract = snapshot.get("registry_export_contract", {})
assert_equal(
"registry_export_contract.expected_scope_aliases",
contract.get("expected_scope_aliases"),
REGISTRY_EXPORT_SCOPE_ALIASES,
)
assert_equal(
"registry_export_contract.allowed_collection_methods",
contract.get("allowed_collection_methods"),
REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS,
)
assert_equal(
"registry_export_contract.per_host_required_fields",
contract.get("per_host_required_fields"),
PER_HOST_REGISTRY_REQUIRED_FIELDS,
)
assert_equal(
"registry_export_contract.redaction_requirements",
contract.get("redaction_requirements"),
REGISTRY_EXPORT_REDACTION_REQUIREMENTS,
)
assert_zero("registry_export_contract.registry_export_received_count", contract.get("registry_export_received_count"))
assert_zero("registry_export_contract.registry_export_accepted_count", contract.get("registry_export_accepted_count"))
summary = snapshot.get("summary", {})
assert_equal("summary.required_field_count", summary.get("required_field_count"), len(REQUIRED_FIELDS))
assert_equal("summary.reviewer_check_count", summary.get("reviewer_check_count"), len(REVIEWER_CHECKS))
assert_equal("summary.outcome_lane_count", summary.get("outcome_lane_count"), len(OUTCOME_LANES))
assert_equal("summary.forbidden_payload_count", summary.get("forbidden_payload_count"), len(FORBIDDEN_PAYLOADS))
assert_equal(
"summary.expected_scope_alias_count",
summary.get("expected_scope_alias_count"),
len(REGISTRY_EXPORT_SCOPE_ALIASES),
)
assert_equal(
"summary.per_host_required_field_count",
summary.get("per_host_required_field_count"),
len(PER_HOST_REGISTRY_REQUIRED_FIELDS),
)
for key in [
"registry_export_received_count",
"registry_export_accepted_count",
"owner_evidence_received_count",
"owner_evidence_accepted_count",
"owner_evidence_rejected_count",
@@ -245,6 +335,8 @@ def main() -> None:
"WAZUH_AGENT_VISIBILITY_OWNER_EVIDENCE_PREFLIGHT_OK "
f"fields={summary['required_field_count']} "
f"checks={summary['reviewer_check_count']} "
f"aliases={summary['expected_scope_alias_count']} "
f"export_received={summary['registry_export_received_count']} "
f"received={summary['owner_evidence_received_count']} "
f"accepted={summary['owner_evidence_accepted_count']} "
f"runtime_gate={summary['runtime_gate_count']}"