449 lines
21 KiB
Python
449 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
IwoooS Wazuh / 主機入侵 read-only 接入計畫產生器。
|
||
|
||
本工具只產生 repo 內 snapshot,將已建置的 Wazuh 平台、host-110 /
|
||
host-188 入侵跡象、外部 Agent 高風險操作宣稱與後續 containment /
|
||
recovery proof 轉成 IwoooS 可收件、可拒收、可補件的脫敏欄位。
|
||
|
||
它不連 Wazuh API、不讀主機、不 SSH、不啟動 Wazuh active response、
|
||
不隔離主機、不封鎖端口、不重啟服務、不收 secret / private key /
|
||
runner token / webhook secret,也不把外部 Agent 的口頭宣稱視為已修復。
|
||
"""
|
||
|
||
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))
|
||
|
||
READBACK_FIELDS = [
|
||
"readback_candidate_id",
|
||
"scope_alias",
|
||
"label",
|
||
"control_tier",
|
||
"priority",
|
||
"source_kind",
|
||
"wazuh_platform_reported_ready",
|
||
"requires_owner_evidence",
|
||
"requires_wazuh_event_refs",
|
||
"requires_host_forensics_refs",
|
||
"requires_cross_project_sync",
|
||
"wazuh_manager_health_ref",
|
||
"wazuh_agent_status_ref",
|
||
"wazuh_alert_rule_refs",
|
||
"wazuh_event_refs",
|
||
"host_auth_log_refs",
|
||
"process_tree_refs",
|
||
"network_connection_refs",
|
||
"file_integrity_refs",
|
||
"package_inventory_refs",
|
||
"persistence_mechanism_refs",
|
||
"runner_or_gateway_config_diff_refs",
|
||
"actor_attribution_ref",
|
||
"incident_time_window_ref",
|
||
"affected_scope",
|
||
"impact_summary_ref",
|
||
"containment_decision",
|
||
"containment_reason_ref",
|
||
"recovery_proof_refs",
|
||
"postcheck_readback_ref",
|
||
"recurrence_guard_ref",
|
||
"cross_project_sync_ref",
|
||
"rollback_owner",
|
||
"followup_owner",
|
||
"reviewer_outcome",
|
||
"not_authorization",
|
||
]
|
||
|
||
REQUIRED_READBACK_FIELDS = [
|
||
"wazuh_manager_health_ref",
|
||
"wazuh_agent_status_ref",
|
||
"wazuh_event_refs",
|
||
"host_auth_log_refs",
|
||
"process_tree_refs",
|
||
"network_connection_refs",
|
||
"file_integrity_refs",
|
||
"package_inventory_refs",
|
||
"persistence_mechanism_refs",
|
||
"actor_attribution_ref",
|
||
"incident_time_window_ref",
|
||
"affected_scope",
|
||
"impact_summary_ref",
|
||
"containment_decision",
|
||
"containment_reason_ref",
|
||
"recovery_proof_refs",
|
||
"postcheck_readback_ref",
|
||
"recurrence_guard_ref",
|
||
"cross_project_sync_ref",
|
||
"rollback_owner",
|
||
"followup_owner",
|
||
"redacted_evidence_refs",
|
||
"no_secret_value_attestation",
|
||
"no_raw_log_attestation",
|
||
"no_raw_wazuh_payload_attestation",
|
||
"no_private_key_or_runner_token_attestation",
|
||
"no_false_green_attestation",
|
||
"external_agent_claim_not_accepted_attestation",
|
||
"active_response_not_enabled_attestation",
|
||
"independent_reviewer_attestation",
|
||
]
|
||
|
||
REVIEWER_CHECKS = [
|
||
{"check_id": "wazuh_platform_ready_is_metadata_only", "instruction": "Wazuh 已建置只能當作平台存在訊號,不能直接視為 IwoooS 已完成事件接入。"},
|
||
{"check_id": "manager_health_ref_present", "instruction": "必須有脫敏 manager / indexer / dashboard / API health ref。"},
|
||
{"check_id": "agent_status_ref_present", "instruction": "host-110、host-188 或其他受影響 host 必須有 agent 狀態 ref。"},
|
||
{"check_id": "event_refs_present", "instruction": "必須有 Wazuh event / alert refs,不能只寫已發現或已阻擋。"},
|
||
{"check_id": "host_forensics_refs_present", "instruction": "需要 auth log、process tree、network connection、file integrity、package inventory 或 persistence refs。"},
|
||
{"check_id": "actor_attribution_present", "instruction": "需要 actor role / team / tool attribution;外部 Agent 操作不能匿名通過。"},
|
||
{"check_id": "time_window_present", "instruction": "需要 incident / operation / recovery time window,供事件與變更對齊。"},
|
||
{"check_id": "affected_scope_present", "instruction": "需列 host alias、服務 alias、產品 alias 與受影響配置類別,不顯示原始內部命名空間。"},
|
||
{"check_id": "containment_decision_present", "instruction": "需有 containment decision 與理由;未決定時維持 waiting_containment_decision。"},
|
||
{"check_id": "external_agent_claim_not_trusted", "instruction": "外部 Agent 宣稱已清除、已修復、已阻擋或已 Zero-Trust 不能直接 accepted。"},
|
||
{"check_id": "runner_gateway_change_reviewed", "instruction": "runner label、runner config、gateway、firewall、workflow 或 secret 相關事件需串到既有高價值配置 gate。"},
|
||
{"check_id": "secret_material_absent", "instruction": "不得收 private key、password、runner token、webhook secret、cookie、env dump、hash 或 partial token。"},
|
||
{"check_id": "raw_payload_absent", "instruction": "不得保存 raw Wazuh payload、raw syslog、raw journal、完整截圖或未脫敏 command output。"},
|
||
{"check_id": "hardcoded_wazuh_credential_rejected", "instruction": "硬編碼 Wazuh URL、使用者、密碼或關閉 TLS 驗證的程式碼必須拒收或隔離。"},
|
||
{"check_id": "readonly_api_disabled_by_default", "instruction": "IwoooS Wazuh API code path 必須預設關閉,沒有 owner gate 與 server-side env 時不得查詢。"},
|
||
{"check_id": "host_write_not_opened", "instruction": "不得因 Wazuh 告警就自動 SSH、kill process、封鎖端口、restart 或修改主機。"},
|
||
{"check_id": "active_response_not_opened", "instruction": "Wazuh active response、firewall drop、isolation 或 remediation 必須另有 break-glass 與人工批准。"},
|
||
{"check_id": "cross_project_sync_present", "instruction": "若事件影響 AWOOOI、AwoooP、IwoooS、任務媒合、股票研究、公開網站或監控,需有同步 ref。"},
|
||
{"check_id": "recovery_proof_present", "instruction": "需要 recovery proof refs;服務 route 200 或 dashboard 可見不能單獨當恢復證據。"},
|
||
{"check_id": "postcheck_independent", "instruction": "postcheck 必須獨立於操作人與外部 Agent。"},
|
||
{"check_id": "recurrence_guard_present", "instruction": "需要防再發 guard,例如 credential hygiene、runner config control、firewall diff control、Wazuh rule coverage。"},
|
||
{"check_id": "owner_response_not_auto_accepted", "instruction": "owner response received / accepted count 只能由 reviewer record 推進。"},
|
||
{"check_id": "no_false_green", "instruction": "Wazuh online、agent active、route 200、CD success、UI 可見都不能單獨當事件驗收。"},
|
||
{"check_id": "runtime_stays_zero", "instruction": "readback plan 不得打開 active response、host write、workflow change、secret rotation 或 action button。"},
|
||
]
|
||
|
||
OUTCOME_LANES = [
|
||
{"lane_id": "waiting_wazuh_owner_evidence", "meaning": "Wazuh 已建置但尚未收到 IwoooS 可接受的脫敏事件 refs。"},
|
||
{"lane_id": "request_manager_health_supplement", "meaning": "缺 Wazuh manager / indexer / dashboard / API health ref 時要求補件。"},
|
||
{"lane_id": "request_agent_status_supplement", "meaning": "缺 host agent 狀態、agent id 對照或 last seen ref 時要求補件。"},
|
||
{"lane_id": "request_host_forensics_supplement", "meaning": "缺 auth / process / network / FIM / package / persistence refs 時要求補件。"},
|
||
{"lane_id": "request_actor_or_time_supplement", "meaning": "缺 actor attribution、工具來源或事件時間窗時要求補件。"},
|
||
{"lane_id": "request_containment_decision", "meaning": "缺 containment decision、理由、owner 或 rollback owner 時要求補件。"},
|
||
{"lane_id": "quarantine_secret_or_raw_payload", "meaning": "收到 secret、private key、runner token、raw payload、raw log 或未脫敏截圖時隔離。"},
|
||
{"lane_id": "reject_external_agent_claim_only", "meaning": "只有外部 Agent 口頭宣稱已修復、已阻擋或已清除時拒收。"},
|
||
{"lane_id": "route_to_high_value_config_gate", "meaning": "涉及 runner、workflow、secret、gateway、firewall、systemd、Docker 或 K8s 時串到高價值配置 gate。"},
|
||
{"lane_id": "ready_for_intrusion_reviewer_review", "meaning": "metadata 合格後只能進 reviewer review,不自動執行。"},
|
||
{"lane_id": "recurrence_guard_backfill_required", "meaning": "需補 Wazuh rule coverage、runner/gateway/secret guard、credential hygiene 或 change freeze。"},
|
||
{"lane_id": "waiting_runtime_gate", "meaning": "即使事件 refs accepted,active response 與 host containment 仍需獨立人工批准。"},
|
||
]
|
||
|
||
BLOCKED_ACTIONS = [
|
||
"wazuh_active_response_enable",
|
||
"wazuh_agent_install",
|
||
"wazuh_agent_restart",
|
||
"wazuh_manager_restart",
|
||
"wazuh_rule_change",
|
||
"wazuh_decoder_change",
|
||
"wazuh_api_live_query_without_owner_gate",
|
||
"hardcode_wazuh_url",
|
||
"hardcode_wazuh_username",
|
||
"hardcode_wazuh_password",
|
||
"disable_tls_verification",
|
||
"store_raw_wazuh_payload",
|
||
"store_raw_syslog",
|
||
"store_raw_journal",
|
||
"store_raw_command_output",
|
||
"store_unredacted_screenshot",
|
||
"collect_password",
|
||
"collect_private_key",
|
||
"collect_runner_token",
|
||
"collect_webhook_secret",
|
||
"collect_cookie_or_session",
|
||
"collect_env_dump",
|
||
"ssh_read",
|
||
"ssh_write",
|
||
"sudo_action",
|
||
"host_file_write",
|
||
"kill_process",
|
||
"quarantine_host",
|
||
"firewall_drop",
|
||
"port_close",
|
||
"port_open",
|
||
"docker_restart",
|
||
"docker_kill",
|
||
"systemctl_restart",
|
||
"nginx_reload",
|
||
"workflow_modification",
|
||
"runner_label_change",
|
||
"runner_config_change",
|
||
"repo_secret_change",
|
||
"secret_rotation",
|
||
"active_scan",
|
||
"production_write",
|
||
"mark_malware_removed_without_evidence",
|
||
"mark_rce_fixed_without_evidence",
|
||
"accept_external_agent_claim_without_wazuh_refs",
|
||
"accept_route_200_as_recovery",
|
||
"accept_agent_active_as_incident_closed",
|
||
"open_runtime_gate",
|
||
"add_action_button",
|
||
]
|
||
|
||
READBACK_CANDIDATES = [
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:wazuh_control_plane",
|
||
"scope_alias": "wazuh-control-plane",
|
||
"label": "Wazuh manager / indexer / dashboard / API",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "wazuh_platform",
|
||
"requires_wazuh_event_refs": False,
|
||
"requires_host_forensics_refs": False,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:host110_intrusion_signal",
|
||
"scope_alias": "host-110",
|
||
"label": "host-110 入侵 / gateway / runner / service 異常訊號",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "host_intrusion",
|
||
"requires_wazuh_event_refs": True,
|
||
"requires_host_forensics_refs": True,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:host188_intrusion_signal",
|
||
"scope_alias": "host-188",
|
||
"label": "host-188 入侵 / 惡意程式 / RCE 宣稱回讀",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "host_intrusion",
|
||
"requires_wazuh_event_refs": True,
|
||
"requires_host_forensics_refs": True,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:external_agent_live_change_claim",
|
||
"scope_alias": "external-agent-handoff",
|
||
"label": "外部 Agent live 變更與修復宣稱驗證",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "external_agent_claim",
|
||
"requires_wazuh_event_refs": True,
|
||
"requires_host_forensics_refs": True,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:runner_secret_gateway_control",
|
||
"scope_alias": "runner-secret-gateway",
|
||
"label": "Runner / secret / gateway 高價值配置連動",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "high_value_config_link",
|
||
"requires_wazuh_event_refs": True,
|
||
"requires_host_forensics_refs": True,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
{
|
||
"readback_candidate_id": "wazuh_intrusion_readback:containment_recovery_postcheck",
|
||
"scope_alias": "containment-recovery",
|
||
"label": "Containment / recovery proof / postcheck",
|
||
"control_tier": "C0",
|
||
"priority": "P0",
|
||
"source_kind": "incident_response",
|
||
"requires_wazuh_event_refs": True,
|
||
"requires_host_forensics_refs": True,
|
||
"requires_cross_project_sync": True,
|
||
},
|
||
]
|
||
|
||
|
||
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 build_candidate(source: dict[str, Any]) -> dict[str, Any]:
|
||
return {
|
||
**source,
|
||
"status": "waiting_wazuh_owner_evidence",
|
||
"wazuh_platform_reported_ready": True,
|
||
"requires_owner_evidence": True,
|
||
"wazuh_manager_health_ref": None,
|
||
"wazuh_agent_status_ref": None,
|
||
"wazuh_alert_rule_refs": [],
|
||
"wazuh_event_refs": [],
|
||
"host_auth_log_refs": [],
|
||
"process_tree_refs": [],
|
||
"network_connection_refs": [],
|
||
"file_integrity_refs": [],
|
||
"package_inventory_refs": [],
|
||
"persistence_mechanism_refs": [],
|
||
"runner_or_gateway_config_diff_refs": [],
|
||
"actor_attribution_ref": None,
|
||
"incident_time_window_ref": None,
|
||
"affected_scope": "pending_redacted_scope",
|
||
"impact_summary_ref": None,
|
||
"containment_decision": "waiting_containment_decision",
|
||
"containment_reason_ref": None,
|
||
"recovery_proof_refs": [],
|
||
"postcheck_readback_ref": None,
|
||
"recurrence_guard_ref": None,
|
||
"cross_project_sync_ref": None,
|
||
"rollback_owner": "pending_owner_response",
|
||
"followup_owner": "pending_owner_response",
|
||
"reviewer_outcome": "waiting_wazuh_intrusion_review",
|
||
"not_approval": True,
|
||
"runtime_gate": False,
|
||
"action_buttons_allowed": False,
|
||
"active_response_authorized": False,
|
||
"host_write_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"blocked_actions": BLOCKED_ACTIONS,
|
||
"readback_fields": READBACK_FIELDS,
|
||
"required_readback_fields": REQUIRED_READBACK_FIELDS,
|
||
"reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS],
|
||
"outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES],
|
||
}
|
||
|
||
|
||
def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
|
||
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||
candidates = [build_candidate(item) for item in READBACK_CANDIDATES]
|
||
return {
|
||
"schema_version": "wazuh_iwooos_intrusion_readback_plan_v1",
|
||
"generated_at": report_time,
|
||
"git_commit": git_short_sha(root),
|
||
"status": "wazuh_intrusion_readback_plan_ready_no_runtime_action",
|
||
"mode": "metadata_only_no_wazuh_api_no_host_read_no_active_response",
|
||
"source_context": {
|
||
"wazuh_platform_reported_ready": True,
|
||
"iwooos_runtime_ingestion_accepted": False,
|
||
"affected_host_aliases": ["host-110", "host-188"],
|
||
"external_agent_transcript_published_to_frontend": False,
|
||
"raw_secret_or_private_key_collection_allowed": False,
|
||
},
|
||
"summary": {
|
||
"wazuh_platform_reported_count": 1,
|
||
"readback_candidate_count": len(candidates),
|
||
"c0_readback_candidate_count": sum(1 for item in candidates if item["control_tier"] == "C0"),
|
||
"affected_host_alias_count": 2,
|
||
"wazuh_event_required_candidate_count": sum(1 for item in candidates if item["requires_wazuh_event_refs"]),
|
||
"host_forensics_required_candidate_count": sum(1 for item in candidates if item["requires_host_forensics_refs"]),
|
||
"cross_project_sync_required_candidate_count": sum(1 for item in candidates if item["requires_cross_project_sync"]),
|
||
"readback_field_count": len(READBACK_FIELDS),
|
||
"required_readback_field_count": len(REQUIRED_READBACK_FIELDS),
|
||
"reviewer_check_count": len(REVIEWER_CHECKS),
|
||
"outcome_lane_count": len(OUTCOME_LANES),
|
||
"blocked_action_count": len(BLOCKED_ACTIONS),
|
||
"wazuh_manager_health_ref_received_count": 0,
|
||
"wazuh_agent_status_ref_received_count": 0,
|
||
"wazuh_event_ref_received_count": 0,
|
||
"host_forensics_ref_received_count": 0,
|
||
"containment_decision_received_count": 0,
|
||
"recovery_proof_received_count": 0,
|
||
"postcheck_readback_received_count": 0,
|
||
"recurrence_guard_received_count": 0,
|
||
"cross_project_sync_received_count": 0,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"wazuh_event_accepted_count": 0,
|
||
"host_forensics_accepted_count": 0,
|
||
"containment_decision_accepted_count": 0,
|
||
"recovery_proof_accepted_count": 0,
|
||
"postcheck_readback_accepted_count": 0,
|
||
"recurrence_guard_accepted_count": 0,
|
||
"readonly_api_code_path_present_count": 1,
|
||
"readonly_api_enabled_count": 0,
|
||
"active_response_authorized_count": 0,
|
||
"host_write_authorized_count": 0,
|
||
"secret_value_collection_allowed_count": 0,
|
||
"runtime_gate_count": 0,
|
||
"action_button_count": 0,
|
||
"coverage_percent_after_readback_plan": 72,
|
||
},
|
||
"execution_boundaries": {
|
||
"runtime_execution_authorized": False,
|
||
"wazuh_api_live_query_authorized": False,
|
||
"wazuh_active_response_authorized": False,
|
||
"wazuh_rule_change_authorized": False,
|
||
"wazuh_agent_install_authorized": False,
|
||
"host_read_authorized": False,
|
||
"host_write_authorized": False,
|
||
"ssh_read_authorized": False,
|
||
"ssh_write_authorized": False,
|
||
"sudo_action_authorized": False,
|
||
"firewall_change_authorized": False,
|
||
"process_kill_authorized": False,
|
||
"host_isolation_authorized": False,
|
||
"runner_change_authorized": False,
|
||
"workflow_modification_authorized": False,
|
||
"repo_secret_change_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"raw_wazuh_payload_storage_allowed": False,
|
||
"raw_log_storage_allowed": False,
|
||
"private_key_collection_allowed": False,
|
||
"active_scan_authorized": False,
|
||
"production_write_authorized": False,
|
||
"frontend_raw_transcript_display_allowed": False,
|
||
"action_buttons_allowed": False,
|
||
"not_authorization": True,
|
||
},
|
||
"readback_candidates": candidates,
|
||
"reviewer_checks": REVIEWER_CHECKS,
|
||
"outcome_lanes": OUTCOME_LANES,
|
||
"blocked_actions": BLOCKED_ACTIONS,
|
||
"operator_interpretation": [
|
||
"Wazuh 已建置是偵測平台存在訊號,不代表 IwoooS 已驗收 110 / 188 入侵事件。",
|
||
"IwoooS 只能接收脫敏 refs、hash、ticket、artifact pointer 與 owner response,不收 secret、private key、runner token 或 raw payload。",
|
||
"外部 Agent 對木馬、RCE、Zero-Trust、清除或封鎖的宣稱必須用 Wazuh event refs 與 host forensic refs 交叉驗證。",
|
||
"active response、host containment、firewall drop、restart、runner change、secret rotation 與 production write 仍全部需要獨立人工批准。",
|
||
],
|
||
}
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="IwoooS Wazuh / 主機入侵 read-only 接入計畫產生器")
|
||
parser.add_argument("--root", default=".", help="repo root")
|
||
parser.add_argument("--output", help="寫出 JSON 報告")
|
||
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
|
||
args = parser.parse_args()
|
||
|
||
root = Path(args.root).resolve()
|
||
report = build_report(root, 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(
|
||
"WAZUH_IWOOOS_INTRUSION_READBACK_PLAN_OK "
|
||
f"candidates={summary['readback_candidate_count']} "
|
||
f"hosts={summary['affected_host_alias_count']} "
|
||
f"checks={summary['reviewer_check_count']} "
|
||
f"blocked={summary['blocked_action_count']} "
|
||
f"runtime_gate={summary['runtime_gate_count']}",
|
||
file=sys.stderr,
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|