feat(iwooos): expose Wazuh live metadata gate readback
All checks were successful
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Successful in 1m45s
CD Pipeline / build-and-deploy (push) Successful in 5m8s
CD Pipeline / post-deploy-checks (push) Successful in 1m35s

This commit is contained in:
Your Name
2026-06-27 00:11:54 +08:00
parent bfecd87c04
commit 10a925bab6
9 changed files with 715 additions and 35 deletions

View File

@@ -21,6 +21,7 @@ TAIPEI = timezone(timedelta(hours=8))
NEXT_ROUTE_PATH = Path("apps/web/src/app/api/iwooos/wazuh/route.ts")
BACKEND_ROUTE_PATH = Path("apps/api/src/api/v1/iwooos.py")
BACKEND_SERVICE_PATH = Path("apps/api/src/services/iwooos_wazuh_readonly_status.py")
BACKEND_LIVE_METADATA_GATE_SERVICE_PATH = Path("apps/api/src/services/iwooos_wazuh_live_metadata_gate.py")
PUBLIC_PAGE_PATH = Path("apps/web/src/app/[locale]/iwooos/page.tsx")
PUBLIC_COMPONENT_ROOT = Path("apps/web/src/components/iwooos")
@@ -84,6 +85,22 @@ BACKEND_SERVICE_REQUIRED_TOKENS = [
"agent_visibility_no_false_green_count",
]
BACKEND_LIVE_METADATA_GATE_REQUIRED_TOKENS = [
"iwooos_wazuh_live_metadata_gate_readback_v1",
"committed_snapshot_readback_with_public_safe_wazuh_route_metadata",
"boundary_markers",
"secret_value_collection_allowed",
"raw_wazuh_payload_storage_allowed",
"wazuh_api_live_query_authorized",
"wazuh_active_response_authorized",
"host_write_authorized",
"runtime_gate_count",
"not_authorization",
"正式路由讀回",
"機密明文收集=false",
"原始 Wazuh 載荷保存=false",
]
FORBIDDEN_PATTERNS = [
ForbiddenPattern(
@@ -184,6 +201,7 @@ def collect_forbidden_matches(root: Path) -> list[dict[str, Any]]:
("route", NEXT_ROUTE_PATH),
("route", BACKEND_ROUTE_PATH),
("route", BACKEND_SERVICE_PATH),
("route", BACKEND_LIVE_METADATA_GATE_SERVICE_PATH),
]
targets.extend(("public_ui", path) for path in collect_public_ui_files(root))
@@ -214,12 +232,17 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
next_route = root / NEXT_ROUTE_PATH
backend_route = root / BACKEND_ROUTE_PATH
backend_service = root / BACKEND_SERVICE_PATH
backend_live_metadata_gate_service = root / BACKEND_LIVE_METADATA_GATE_SERVICE_PATH
next_route_present = next_route.exists()
backend_route_present = backend_route.exists()
backend_service_present = backend_service.exists()
backend_live_metadata_gate_service_present = backend_live_metadata_gate_service.exists()
next_route_text = read_text(next_route) if next_route_present else ""
backend_route_text = read_text(backend_route) if backend_route_present else ""
backend_service_text = read_text(backend_service) if backend_service_present else ""
backend_live_metadata_gate_service_text = (
read_text(backend_live_metadata_gate_service) if backend_live_metadata_gate_service_present else ""
)
public_ui_files = collect_public_ui_files(root)
missing_required_tokens = {
NEXT_ROUTE_PATH.as_posix(): collect_missing_required_tokens(next_route_text, ROUTE_REQUIRED_TOKENS),
@@ -227,6 +250,10 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
BACKEND_SERVICE_PATH.as_posix(): collect_missing_required_tokens(
backend_service_text, BACKEND_SERVICE_REQUIRED_TOKENS
),
BACKEND_LIVE_METADATA_GATE_SERVICE_PATH.as_posix(): collect_missing_required_tokens(
backend_live_metadata_gate_service_text,
BACKEND_LIVE_METADATA_GATE_REQUIRED_TOKENS,
),
}
missing_required_token_count = sum(len(tokens) for tokens in missing_required_tokens.values())
forbidden_matches = collect_forbidden_matches(root)
@@ -236,7 +263,14 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
"generated_at": generated_at or now_iso(),
"status": (
"pass"
if next_route_present and backend_route_present and not missing_required_token_count and not forbidden_matches
if (
next_route_present
and backend_route_present
and backend_service_present
and backend_live_metadata_gate_service_present
and not missing_required_token_count
and not forbidden_matches
)
else "blocked"
),
"mode": "repo_source_scan_no_runtime_no_secret_collection",
@@ -244,33 +278,52 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
NEXT_ROUTE_PATH.as_posix(),
BACKEND_ROUTE_PATH.as_posix(),
BACKEND_SERVICE_PATH.as_posix(),
BACKEND_LIVE_METADATA_GATE_SERVICE_PATH.as_posix(),
],
"guarded_public_ui_paths": [path.as_posix() for path in public_ui_files],
"required_route_tokens": {
NEXT_ROUTE_PATH.as_posix(): ROUTE_REQUIRED_TOKENS,
BACKEND_ROUTE_PATH.as_posix(): BACKEND_REQUIRED_TOKENS,
BACKEND_SERVICE_PATH.as_posix(): BACKEND_SERVICE_REQUIRED_TOKENS,
BACKEND_LIVE_METADATA_GATE_SERVICE_PATH.as_posix(): BACKEND_LIVE_METADATA_GATE_REQUIRED_TOKENS,
},
"forbidden_pattern_ids": [pattern.pattern_id for pattern in FORBIDDEN_PATTERNS],
"summary": {
"route_present_count": int(next_route_present) + int(backend_route_present) + int(backend_service_present),
"route_present_count": (
int(next_route_present)
+ int(backend_route_present)
+ int(backend_service_present)
+ int(backend_live_metadata_gate_service_present)
),
"live_metadata_gate_service_present_count": 1 if backend_live_metadata_gate_service_present else 0,
"next_route_present_count": 1 if next_route_present else 0,
"backend_route_present_count": 1 if backend_route_present else 0,
"backend_service_present_count": 1 if backend_service_present else 0,
"public_ui_file_count": len(public_ui_files),
"required_token_count": len(ROUTE_REQUIRED_TOKENS)
+ len(BACKEND_REQUIRED_TOKENS)
+ len(BACKEND_SERVICE_REQUIRED_TOKENS),
+ len(BACKEND_SERVICE_REQUIRED_TOKENS)
+ len(BACKEND_LIVE_METADATA_GATE_REQUIRED_TOKENS),
"missing_required_token_count": missing_required_token_count,
"forbidden_pattern_count": len(FORBIDDEN_PATTERNS),
"forbidden_match_count": len(forbidden_matches),
"readonly_api_default_closed_count": sum(
"IWOOOS_WAZUH_READONLY_ENABLED" in text
for text in [next_route_text, backend_route_text, backend_service_text]
for text in [
next_route_text,
backend_route_text,
backend_service_text,
backend_live_metadata_gate_service_text,
]
),
"server_side_env_required_count": sum(
token in text
for text in [next_route_text, backend_route_text, backend_service_text]
for text in [
next_route_text,
backend_route_text,
backend_service_text,
backend_live_metadata_gate_service_text,
]
for token in ["WAZUH_API_BASE_URL", "WAZUH_API_USERNAME", "WAZUH_API_PASSWORD"]
),
"tls_disable_match_count": sum(
@@ -325,6 +378,10 @@ def validate(root: Path) -> None:
errors.append(f"{BACKEND_ROUTE_PATH.as_posix()}: Wazuh FastAPI 相容 route 不存在")
if report["summary"]["backend_service_present_count"] != 1:
errors.append(f"{BACKEND_SERVICE_PATH.as_posix()}: Wazuh FastAPI 只讀 service 不存在")
if report["summary"]["live_metadata_gate_service_present_count"] != 1:
errors.append(
f"{BACKEND_LIVE_METADATA_GATE_SERVICE_PATH.as_posix()}: Wazuh 即時中繼資料閘門 service 不存在"
)
for path, tokens in report["missing_required_tokens"].items():
for token in tokens:
errors.append(f"{path}: 缺少必要只讀邊界 token {token!r}")