Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
455 lines
18 KiB
Python
455 lines
18 KiB
Python
"""
|
||
IwoooS 負責人脫敏證據收件預檢讀回。
|
||
|
||
此服務只整合已提交 snapshot 的公開安全欄位,不查 live host、不讀
|
||
Nginx、不查 Wazuh API、不收 secret、不送 owner request,也不建立
|
||
reviewer queue 或 runtime action。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from src.services.snapshot_paths import default_security_dir
|
||
|
||
_DEFAULT_SECURITY_DIR = default_security_dir(Path(__file__))
|
||
_SCHEMA_VERSION = "iwooos_owner_evidence_intake_preflight_v1"
|
||
_INTAKE_SCHEMA = "high_value_config_owner_packet_intake_preflight_v1"
|
||
_COVERAGE_SCHEMA = "high_value_config_control_coverage_v1"
|
||
_WAZUH_SCHEMA = "wazuh_agent_visibility_owner_evidence_preflight_v1"
|
||
|
||
_SNAPSHOTS = {
|
||
"intake": "high-value-config-owner-packet-intake-preflight.snapshot.json",
|
||
"coverage": "high-value-config-control-coverage.snapshot.json",
|
||
"wazuh": "wazuh-agent-visibility-owner-evidence-preflight.snapshot.json",
|
||
}
|
||
|
||
_REQUIRED_FALSE_BOUNDARIES = {
|
||
"action_buttons_allowed",
|
||
"active_scan_authorized",
|
||
"dispatch_authorized",
|
||
"dns_tls_change_authorized",
|
||
"host_write_authorized",
|
||
"nginx_reload_authorized",
|
||
"production_write_authorized",
|
||
"request_sent",
|
||
"reviewer_queue_write",
|
||
"runtime_execution_authorized",
|
||
"secret_value_collection_allowed",
|
||
"workflow_modification_authorized",
|
||
}
|
||
|
||
_OWNER_FIELDS = [
|
||
"owner_role_or_team",
|
||
"decision",
|
||
"decision_reason",
|
||
"affected_scope",
|
||
"redacted_evidence_refs",
|
||
"followup_owner",
|
||
"rollback_owner",
|
||
"maintenance_window",
|
||
"validation_plan",
|
||
]
|
||
|
||
_LANE_ORDER = [
|
||
"nginx_public_gateway",
|
||
"dns_tls_certbot",
|
||
"k8s_production_gitops",
|
||
"secret_workflow_runner_source_control",
|
||
"public_admin_api_runtime_config",
|
||
"wazuh_manager_registry",
|
||
]
|
||
|
||
_PUBLIC_LABELS = {
|
||
"nginx_public_gateway": "Nginx / 公開入口 / 反向代理",
|
||
"dns_tls_certbot": "DNS / TLS / 憑證續期",
|
||
"k8s_production_gitops": "K8s / ArgoCD / 正式環境宣告",
|
||
"secret_workflow_runner_source_control": "機密中繼資料 / 工作流程 / 執行器",
|
||
"public_admin_api_runtime_config": "公開 / 後台 / API runtime config",
|
||
"wazuh_manager_registry": "Wazuh manager registry / 逐主機矩陣",
|
||
}
|
||
|
||
_REQUIRED_EVIDENCE = {
|
||
"nginx_public_gateway": [
|
||
"owner-provided live conf 脫敏參照",
|
||
"source-to-live rendered diff 參照",
|
||
"nginx -t 結果參照",
|
||
"affected route smoke 參照",
|
||
"maintenance window 與 rollback owner",
|
||
],
|
||
"dns_tls_certbot": [
|
||
"domain / certificate path 對照參照",
|
||
"ACME challenge route smoke 參照",
|
||
"renewal window 與 rollback owner",
|
||
"public HTTPS smoke 參照",
|
||
],
|
||
"k8s_production_gitops": [
|
||
"ArgoCD app health / sync / revision 參照",
|
||
"manifest diff 與 rollback revision",
|
||
"Pending / image pull / scheduling 摘要",
|
||
"route / AI / monitoring impact 參照",
|
||
],
|
||
"secret_workflow_runner_source_control": [
|
||
"secret name parity state 參照",
|
||
"workflow diff state 參照",
|
||
"runner owner attestation",
|
||
"deploy marker / run readback",
|
||
"log redaction readback",
|
||
],
|
||
"public_admin_api_runtime_config": [
|
||
"affected route refs",
|
||
"admin / auth boundary",
|
||
"API contract readback",
|
||
"frontend env / i18n redaction review",
|
||
"desktop / mobile production smoke",
|
||
],
|
||
"wazuh_manager_registry": [
|
||
"manager registry 脫敏計數",
|
||
"逐主機 alias matrix",
|
||
"Dashboard API 狀態參照",
|
||
"manager API version / health 參照",
|
||
"缺席主機 owner decision",
|
||
],
|
||
}
|
||
|
||
_BLOCKED_RUNTIME_ACTIONS = {
|
||
"nginx_public_gateway": ["nginx_reload", "host_write", "dns_tls_modify", "secret_value_submit"],
|
||
"dns_tls_certbot": ["certbot_renew", "dns_tls_modify", "nginx_reload", "secret_value_submit"],
|
||
"k8s_production_gitops": ["argocd_sync", "kubectl_apply", "secret_value_submit", "production_write"],
|
||
"secret_workflow_runner_source_control": [
|
||
"workflow_modify",
|
||
"runner_enable",
|
||
"secret_value_submit",
|
||
"webhook_secret_submit",
|
||
],
|
||
"public_admin_api_runtime_config": ["production_write", "cors_change", "route_change", "secret_value_submit"],
|
||
"wazuh_manager_registry": [
|
||
"wazuh_api_live_query",
|
||
"wazuh_active_response",
|
||
"host_write",
|
||
"active_scan",
|
||
"raw_payload_store",
|
||
],
|
||
}
|
||
|
||
|
||
def load_latest_iwooos_owner_evidence_intake_preflight(
|
||
security_dir: Path | None = None,
|
||
) -> dict[str, Any]:
|
||
"""載入 IwoooS 統一負責人脫敏證據收件預檢。"""
|
||
sec_dir = security_dir or _DEFAULT_SECURITY_DIR
|
||
intake = _load_json(sec_dir / _SNAPSHOTS["intake"], _INTAKE_SCHEMA)
|
||
coverage = _load_json(sec_dir / _SNAPSHOTS["coverage"], _COVERAGE_SCHEMA)
|
||
wazuh = _load_json(sec_dir / _SNAPSHOTS["wazuh"], _WAZUH_SCHEMA)
|
||
_validate_intake(intake)
|
||
_validate_coverage(coverage)
|
||
_validate_wazuh(wazuh)
|
||
|
||
packet_by_category = {
|
||
packet.get("category_id"): packet
|
||
for packet in intake.get("intake_packets") or []
|
||
if isinstance(packet, dict) and packet.get("category_id")
|
||
}
|
||
coverage_by_category = {
|
||
category.get("category_id"): category
|
||
for category in coverage.get("coverage_categories") or []
|
||
if isinstance(category, dict) and category.get("category_id")
|
||
}
|
||
lanes = [
|
||
_build_lane(lane_id, packet_by_category, coverage_by_category, wazuh)
|
||
for lane_id in _LANE_ORDER
|
||
]
|
||
summary = _summary(intake, wazuh, lanes)
|
||
|
||
return {
|
||
"schema_version": _SCHEMA_VERSION,
|
||
"source_schema_versions": {
|
||
"high_value_config_owner_packet_intake_preflight": _INTAKE_SCHEMA,
|
||
"high_value_config_control_coverage": _COVERAGE_SCHEMA,
|
||
"wazuh_owner_evidence_preflight": _WAZUH_SCHEMA,
|
||
},
|
||
"status": "owner_evidence_intake_preflight_ready_no_runtime_action",
|
||
"mode": "committed_snapshot_projection_only_no_live_runtime_query",
|
||
"summary": summary,
|
||
"lanes": lanes,
|
||
"dispatch_preflight_checks": _public_checks(intake.get("dispatch_preflight_checks")),
|
||
"reviewer_intake_lanes": _public_reviewer_lanes(intake.get("reviewer_intake_lanes")),
|
||
"blocked_requests": _blocked_requests(intake, wazuh),
|
||
"execution_boundaries": _execution_boundaries(),
|
||
"boundary_markers": _boundary_markers(summary),
|
||
"no_false_green_rules": [
|
||
"收件預檢 ready 不代表 request 已送出、owner response 已收到、reviewer 已接受或 runtime gate 已開。",
|
||
"只收脫敏 evidence ref、ticket、commit、hash 或 metadata pointer;secret value、raw payload、token、private key、cookie、session 一律拒收或隔離。",
|
||
"Nginx reload、DNS / TLS 變更、ArgoCD sync、kubectl apply、workflow 修改、runner enable、Wazuh active response、Kali active scan 都不是此預檢授權。",
|
||
"前台卡片、API 200、一般工作批准或 Dashboard 可開,不能替代 owner-provided redacted evidence 與 reviewer validation。",
|
||
],
|
||
"source_refs": [
|
||
f"docs/security/{_SNAPSHOTS['intake']}",
|
||
f"docs/security/{_SNAPSHOTS['coverage']}",
|
||
f"docs/security/{_SNAPSHOTS['wazuh']}",
|
||
"scripts/security/high-value-config-owner-packet-intake-preflight.py",
|
||
"scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py",
|
||
],
|
||
}
|
||
|
||
|
||
def _load_json(path: Path, schema: str) -> dict[str, Any]:
|
||
if not path.is_file():
|
||
raise FileNotFoundError(f"{path}: snapshot 不存在")
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
if not isinstance(payload, dict):
|
||
raise ValueError(f"{path}: expected JSON object")
|
||
if payload.get("schema_version") != schema:
|
||
raise ValueError(f"{path}: schema_version 必須是 {schema}")
|
||
return payload
|
||
|
||
|
||
def _validate_intake(payload: dict[str, Any]) -> None:
|
||
summary = payload.get("summary") or {}
|
||
expected_zero = (
|
||
"request_sent_count",
|
||
"received_response_count",
|
||
"accepted_response_count",
|
||
"rejected_response_count",
|
||
"reviewer_queue_write_count",
|
||
"runtime_gate_count",
|
||
"action_button_count",
|
||
)
|
||
for key in expected_zero:
|
||
if _int(summary.get(key)) != 0:
|
||
raise ValueError(f"intake.summary.{key} 必須維持 0")
|
||
boundaries = payload.get("execution_boundaries") or {}
|
||
for key in _REQUIRED_FALSE_BOUNDARIES:
|
||
if boundaries.get(key) is not False:
|
||
raise ValueError(f"intake.execution_boundaries.{key} 必須維持 false")
|
||
if boundaries.get("not_authorization") is not True:
|
||
raise ValueError("intake.execution_boundaries.not_authorization 必須是 true")
|
||
|
||
|
||
def _validate_coverage(payload: dict[str, Any]) -> None:
|
||
summary = payload.get("summary") or {}
|
||
if _int(summary.get("owner_response_received_count")) != 0:
|
||
raise ValueError("coverage owner_response_received_count 必須維持 0")
|
||
if _int(summary.get("owner_response_accepted_count")) != 0:
|
||
raise ValueError("coverage owner_response_accepted_count 必須維持 0")
|
||
if _int(summary.get("runtime_gate_count")) != 0:
|
||
raise ValueError("coverage runtime_gate_count 必須維持 0")
|
||
if _int(summary.get("action_button_count")) != 0:
|
||
raise ValueError("coverage action_button_count 必須維持 0")
|
||
|
||
|
||
def _validate_wazuh(payload: dict[str, Any]) -> None:
|
||
summary = payload.get("summary") or {}
|
||
for key in (
|
||
"registry_export_received_count",
|
||
"registry_export_accepted_count",
|
||
"owner_evidence_received_count",
|
||
"owner_evidence_accepted_count",
|
||
"runtime_gate_count",
|
||
"active_response_authorized_count",
|
||
"host_write_authorized_count",
|
||
"secret_value_collection_allowed_count",
|
||
):
|
||
if _int(summary.get(key)) != 0:
|
||
raise ValueError(f"wazuh.summary.{key} 必須維持 0")
|
||
boundaries = payload.get("execution_boundaries") or {}
|
||
for key, value in boundaries.items():
|
||
if key == "not_authorization":
|
||
if value is not True:
|
||
raise ValueError("wazuh.execution_boundaries.not_authorization 必須是 true")
|
||
elif value is not False:
|
||
raise ValueError(f"wazuh.execution_boundaries.{key} 必須維持 false")
|
||
|
||
|
||
def _build_lane(
|
||
lane_id: str,
|
||
packet_by_category: dict[str, dict[str, Any]],
|
||
coverage_by_category: dict[str, dict[str, Any]],
|
||
wazuh: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
if lane_id == "secret_workflow_runner_source_control":
|
||
categories = [
|
||
coverage_by_category.get("secret_metadata") or {},
|
||
coverage_by_category.get("gitea_workflow_runner_source_control") or {},
|
||
]
|
||
priority = "P0"
|
||
control_tier = "C0"
|
||
status = "coverage_derived_waiting_owner_packet"
|
||
evidence_ref_count = sum(_len(category.get("evidence_refs")) for category in categories)
|
||
required_validation_count = sum(_len(category.get("required_validation")) for category in categories)
|
||
source_kind = "coverage_derived_intake_candidate"
|
||
elif lane_id == "wazuh_manager_registry":
|
||
summary = wazuh.get("summary") or {}
|
||
priority = "P0"
|
||
control_tier = "C0"
|
||
status = "wazuh_owner_evidence_preflight_ready"
|
||
evidence_ref_count = 0
|
||
required_validation_count = _int(summary.get("reviewer_check_count"))
|
||
source_kind = "wazuh_owner_evidence_preflight"
|
||
else:
|
||
packet = packet_by_category.get(lane_id)
|
||
category = coverage_by_category.get(lane_id) or {}
|
||
priority = str((packet or category).get("priority") or "P0")
|
||
control_tier = str((packet or category).get("control_tier") or "C0")
|
||
status = "owner_packet_ready_waiting_dispatch_preflight" if packet else "coverage_derived_waiting_owner_packet"
|
||
evidence_ref_count = _len(category.get("evidence_refs"))
|
||
required_validation_count = _len((packet or {}).get("required_validation")) or _len(
|
||
category.get("required_validation")
|
||
)
|
||
source_kind = "owner_packet_intake_preflight" if packet else "coverage_derived_intake_candidate"
|
||
|
||
return {
|
||
"lane_id": lane_id,
|
||
"label": _PUBLIC_LABELS[lane_id],
|
||
"priority": priority,
|
||
"control_tier": control_tier,
|
||
"status": status,
|
||
"source_kind": source_kind,
|
||
"required_owner_fields": _OWNER_FIELDS,
|
||
"required_owner_field_count": len(_OWNER_FIELDS),
|
||
"required_evidence": _REQUIRED_EVIDENCE[lane_id],
|
||
"required_evidence_count": len(_REQUIRED_EVIDENCE[lane_id]),
|
||
"required_validation_count": required_validation_count,
|
||
"blocked_runtime_actions": _BLOCKED_RUNTIME_ACTIONS[lane_id],
|
||
"blocked_runtime_action_count": len(_BLOCKED_RUNTIME_ACTIONS[lane_id]),
|
||
"evidence_ref_count": evidence_ref_count,
|
||
"request_sent": False,
|
||
"owner_response_received": False,
|
||
"owner_response_accepted": False,
|
||
"owner_response_rejected": False,
|
||
"owner_response_quarantined": False,
|
||
"reviewer_queue_write": False,
|
||
"runtime_gate_open": False,
|
||
"action_buttons_allowed": False,
|
||
"secret_value_collection_allowed": False,
|
||
"not_authorization": True,
|
||
}
|
||
|
||
|
||
def _summary(intake: dict[str, Any], wazuh: dict[str, Any], lanes: list[dict[str, Any]]) -> dict[str, int]:
|
||
intake_summary = intake.get("summary") or {}
|
||
wazuh_summary = wazuh.get("summary") or {}
|
||
return {
|
||
"lane_count": len(lanes),
|
||
"owner_packet_source_lane_count": sum(1 for lane in lanes if lane["source_kind"] == "owner_packet_intake_preflight"),
|
||
"coverage_derived_lane_count": sum(1 for lane in lanes if lane["source_kind"] == "coverage_derived_intake_candidate"),
|
||
"wazuh_lane_count": sum(1 for lane in lanes if lane["source_kind"] == "wazuh_owner_evidence_preflight"),
|
||
"canonical_owner_field_count": len(_OWNER_FIELDS),
|
||
"required_owner_field_total": len(_OWNER_FIELDS) * len(lanes),
|
||
"dispatch_preflight_check_count": _int(intake_summary.get("dispatch_preflight_check_count")),
|
||
"reviewer_intake_lane_count": _int(intake_summary.get("reviewer_intake_lane_count")),
|
||
"blocked_request_count": len(_blocked_requests(intake, wazuh)),
|
||
"wazuh_required_field_count": _int(wazuh_summary.get("required_field_count")),
|
||
"wazuh_reviewer_check_count": _int(wazuh_summary.get("reviewer_check_count")),
|
||
"request_sent_count": 0,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"owner_response_rejected_count": 0,
|
||
"owner_response_quarantined_count": 0,
|
||
"reviewer_queue_write_count": 0,
|
||
"runtime_gate_count": 0,
|
||
"action_button_count": 0,
|
||
"host_write_authorized_count": 0,
|
||
"active_scan_authorized_count": 0,
|
||
"wazuh_live_query_authorized_count": 0,
|
||
"wazuh_active_response_authorized_count": 0,
|
||
"secret_value_collection_allowed_count": 0,
|
||
}
|
||
|
||
|
||
def _public_checks(items: Any) -> list[dict[str, Any]]:
|
||
checks = items if isinstance(items, list) else []
|
||
return [
|
||
{
|
||
"check_id": str(item.get("check_id") or ""),
|
||
"label": str(item.get("label") or ""),
|
||
"instruction": str(item.get("instruction") or ""),
|
||
"required": item.get("required") is True,
|
||
"gate_effect": "不增加 request / received / accepted / runtime gate。",
|
||
}
|
||
for item in checks
|
||
if isinstance(item, dict)
|
||
]
|
||
|
||
|
||
def _public_reviewer_lanes(items: Any) -> list[dict[str, Any]]:
|
||
lanes = items if isinstance(items, list) else []
|
||
return [
|
||
{
|
||
"lane_id": str(item.get("lane_id") or ""),
|
||
"instruction": str(item.get("instruction") or ""),
|
||
"gate_effect": str(item.get("gate_effect") or ""),
|
||
}
|
||
for item in lanes
|
||
if isinstance(item, dict)
|
||
]
|
||
|
||
|
||
def _blocked_requests(intake: dict[str, Any], wazuh: dict[str, Any]) -> list[str]:
|
||
blocked = set()
|
||
for item in intake.get("blocked_requests") or []:
|
||
if isinstance(item, str):
|
||
blocked.add(item)
|
||
for item in wazuh.get("forbidden_payloads") or []:
|
||
if isinstance(item, str):
|
||
blocked.add(item)
|
||
blocked.update(
|
||
{
|
||
"wazuh_api_live_query",
|
||
"wazuh_active_response",
|
||
"raw_wazuh_payload_store",
|
||
"internal_ip_public_display",
|
||
"agent_identity_public_display",
|
||
}
|
||
)
|
||
return sorted(blocked)
|
||
|
||
|
||
def _execution_boundaries() -> dict[str, bool]:
|
||
boundaries = {key: False for key in sorted(_REQUIRED_FALSE_BOUNDARIES)}
|
||
boundaries.update(
|
||
{
|
||
"wazuh_api_live_query_authorized": False,
|
||
"wazuh_active_response_authorized": False,
|
||
"agent_identity_public_display_allowed": False,
|
||
"internal_ip_public_display_allowed": False,
|
||
"raw_wazuh_payload_storage_allowed": False,
|
||
"not_authorization": True,
|
||
}
|
||
)
|
||
return boundaries
|
||
|
||
|
||
def _boundary_markers(summary: dict[str, int]) -> list[str]:
|
||
return [
|
||
f"owner_evidence_intake_lane_count={summary['lane_count']}",
|
||
f"canonical_owner_field_count={summary['canonical_owner_field_count']}",
|
||
f"required_owner_field_total={summary['required_owner_field_total']}",
|
||
f"dispatch_preflight_check_count={summary['dispatch_preflight_check_count']}",
|
||
f"reviewer_intake_lane_count={summary['reviewer_intake_lane_count']}",
|
||
f"blocked_request_count={summary['blocked_request_count']}",
|
||
"request_sent_count=0",
|
||
"owner_response_received_count=0",
|
||
"owner_response_accepted_count=0",
|
||
"owner_response_quarantined_count=0",
|
||
"reviewer_queue_write_count=0",
|
||
"runtime_gate_count=0",
|
||
"action_button_count=0",
|
||
"host_write_authorized_count=0",
|
||
"active_scan_authorized_count=0",
|
||
"wazuh_live_query_authorized_count=0",
|
||
"wazuh_active_response_authorized_count=0",
|
||
"secret_value_collection_allowed_count=0",
|
||
"not_authorization=true",
|
||
]
|
||
|
||
|
||
def _int(value: Any) -> int:
|
||
return value if isinstance(value, int) else 0
|
||
|
||
|
||
def _len(value: Any) -> int:
|
||
return len(value) if isinstance(value, list) else 0
|