Merge remote-tracking branch 'gitea/main' into codex/github-backup-missing-targets-20260627
This commit is contained in:
@@ -67,6 +67,9 @@ from src.services.ai_agent_automation_backlog_snapshot import (
|
||||
from src.services.ai_agent_automation_inventory_snapshot import (
|
||||
load_latest_ai_agent_automation_inventory_snapshot,
|
||||
)
|
||||
from src.services.ai_agent_autonomous_runtime_control import (
|
||||
build_ai_agent_autonomous_runtime_control,
|
||||
)
|
||||
from src.services.ai_agent_candidate_operation_dry_run_evidence import (
|
||||
load_latest_ai_agent_candidate_operation_dry_run_evidence,
|
||||
)
|
||||
@@ -822,6 +825,29 @@ async def get_automation_inventory_snapshot() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agent-autonomous-runtime-control",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 AI Agent 目前有效自主化控制層",
|
||||
description=(
|
||||
"回傳目前有效的 AI Agent 自主化控制層;此端點明確覆寫舊 no-send / no-live "
|
||||
"歷史快照,宣告 low / medium / high 風險可在 allowlist、Ansible check-mode、"
|
||||
"controlled apply、post-apply verifier、KM 與 Telegram Gateway receipt 下受控自動處理。"
|
||||
"它不讀 secret、不呼叫 Bot API、不暴露 chat id、不執行 runtime 動作;runtime 動作由既有 worker / Gateway 接手。"
|
||||
),
|
||||
)
|
||||
async def get_agent_autonomous_runtime_control() -> dict[str, Any]:
|
||||
"""回傳目前有效 AI Agent 自主化控制層。"""
|
||||
try:
|
||||
return await asyncio.to_thread(build_ai_agent_autonomous_runtime_control)
|
||||
except ValueError as exc:
|
||||
logger.error("ai_agent_autonomous_runtime_control_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="AI Agent 目前有效自主化控制層無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/automation-backlog-snapshot",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -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_owner_export as validate_wazuh_manager_registry_owner_export_payload,
|
||||
)
|
||||
from src.services.iwooos_wazuh_owner_evidence_preflight import (
|
||||
load_latest_iwooos_wazuh_owner_evidence_preflight,
|
||||
@@ -178,6 +179,38 @@ async def get_iwooos_wazuh_manager_registry_reviewer_validation() -> dict[str, A
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
|
||||
response_model=dict[str, Any],
|
||||
summary="驗證 Wazuh manager registry 脫敏 owner export",
|
||||
description=(
|
||||
"針對單次 owner-provided redacted Wazuh manager registry export 進行 no-persist reviewer "
|
||||
"validation,回傳 accepted / needs supplement / quarantined / rejected runtime action 分流。"
|
||||
"此端點不保存 payload、不查 Wazuh API、不讀主機、不重新註冊 agent、不重啟服務、不讀或回傳"
|
||||
"機密明文、不啟用主動回應、不改 Nginx / Docker / K8s / firewall,也不更新 manager registry "
|
||||
"accepted 總帳。"
|
||||
),
|
||||
)
|
||||
async def validate_iwooos_wazuh_manager_registry_owner_export(owner_export: dict[str, Any]) -> dict[str, Any]:
|
||||
"""回傳單次 Wazuh manager registry 脫敏匯出的公開安全驗證結果。"""
|
||||
try:
|
||||
payload = await asyncio.to_thread(
|
||||
validate_wazuh_manager_registry_owner_export_payload,
|
||||
owner_export,
|
||||
)
|
||||
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 owner export 驗證器無效:{exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/iwooos/runtime-security-readback",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -522,14 +522,25 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
except Exception as e:
|
||||
logger.warning("capacity_forecaster_loop_schedule_failed", error=str(e))
|
||||
|
||||
# ADR-076 Task 4: 每日 08:00 台北時間自動日度巡檢報告
|
||||
# 2026-04-14 Claude Haiku 4.5 Asia/Taipei
|
||||
# ADR-076 / P2-416: 日報 08:00、週報週五 10:00、月報每月 1 日 09:00
|
||||
# 透過既有 Telegram Gateway 送 SRE 群組;不暴露 Bot token / chat id。
|
||||
try:
|
||||
from src.services.report_generation_service import run_daily_report_loop
|
||||
from src.services.report_generation_service import (
|
||||
run_daily_report_loop,
|
||||
run_monthly_report_loop,
|
||||
run_weekly_report_loop,
|
||||
)
|
||||
asyncio.create_task(run_daily_report_loop())
|
||||
logger.info("daily_report_loop_scheduled", trigger_hour_taipei=8)
|
||||
asyncio.create_task(run_weekly_report_loop())
|
||||
asyncio.create_task(run_monthly_report_loop())
|
||||
logger.info(
|
||||
"report_delivery_loops_scheduled",
|
||||
daily_hour_taipei=8,
|
||||
weekly="friday_10_taipei",
|
||||
monthly="day1_09_taipei",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("daily_report_loop_schedule_failed", error=str(e))
|
||||
logger.warning("report_delivery_loops_schedule_failed", error=str(e))
|
||||
|
||||
# ADR-073 P2 修復 2026-04-15: 逾期 Approval 自動結案(每小時)
|
||||
# 確保 PENDING approval 超過 48h 後觸發 resolve_incident → KM 學習鏈閉環
|
||||
|
||||
273
apps/api/src/services/ai_agent_autonomous_runtime_control.py
Normal file
273
apps/api/src/services/ai_agent_autonomous_runtime_control.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""Current AI Agent autonomous runtime control plane.
|
||||
|
||||
This read model is the current directive layer. Historical P2 snapshots can
|
||||
still describe earlier no-send / no-live states, but this payload states what
|
||||
the product should enforce now: low, medium, and high risk routes may proceed
|
||||
through controlled automation when allowlist, check-mode, verifier, rollback,
|
||||
KM, and Telegram receipts are present.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from src.core.config import settings
|
||||
from src.services.report_generation_service import (
|
||||
DAILY_REPORT_HOUR_TAIPEI,
|
||||
MONTHLY_REPORT_DAY_TAIPEI,
|
||||
MONTHLY_REPORT_HOUR_TAIPEI,
|
||||
WEEKLY_REPORT_HOUR_TAIPEI,
|
||||
WEEKLY_REPORT_WEEKDAY_TAIPEI,
|
||||
)
|
||||
|
||||
_SCHEMA_VERSION = "ai_agent_autonomous_runtime_control_v1"
|
||||
_RUNTIME_AUTHORITY = "current_owner_directive_controlled_ai_automation"
|
||||
|
||||
|
||||
def _allowed_risk_levels() -> list[str]:
|
||||
raw = str(settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_ALLOWED_RISK_LEVELS or "")
|
||||
return sorted({item.strip().lower() for item in raw.split(",") if item.strip()})
|
||||
|
||||
|
||||
def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
|
||||
"""Build the current AI Agent autonomy control-plane readback."""
|
||||
|
||||
allowed_risks = _allowed_risk_levels()
|
||||
report_cadences = [
|
||||
{
|
||||
"cadence": "daily",
|
||||
"display_name": "日報",
|
||||
"schedule": f"每日 {DAILY_REPORT_HOUR_TAIPEI:02d}:00 台北時間",
|
||||
"worker": "report_generation_service.run_daily_report_loop",
|
||||
"telegram_gateway_delivery_enabled": True,
|
||||
"direct_bot_api_allowed": False,
|
||||
"receipt_source": "daily_report_sent log + Telegram Gateway result",
|
||||
},
|
||||
{
|
||||
"cadence": "weekly",
|
||||
"display_name": "週報",
|
||||
"schedule": (
|
||||
f"每週五 {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間"
|
||||
if WEEKLY_REPORT_WEEKDAY_TAIPEI == 4
|
||||
else f"每週 weekday={WEEKLY_REPORT_WEEKDAY_TAIPEI} {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間"
|
||||
),
|
||||
"worker": "report_generation_service.run_weekly_report_loop",
|
||||
"telegram_gateway_delivery_enabled": True,
|
||||
"direct_bot_api_allowed": False,
|
||||
"receipt_source": "weekly_report_sent log + Telegram Gateway result",
|
||||
},
|
||||
{
|
||||
"cadence": "monthly",
|
||||
"display_name": "月報",
|
||||
"schedule": f"每月 {MONTHLY_REPORT_DAY_TAIPEI} 日 {MONTHLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間",
|
||||
"worker": "report_generation_service.run_monthly_report_loop",
|
||||
"telegram_gateway_delivery_enabled": True,
|
||||
"direct_bot_api_allowed": False,
|
||||
"receipt_source": "monthly_report_sent log + Telegram Gateway result",
|
||||
},
|
||||
]
|
||||
executor_receipts = [
|
||||
{
|
||||
"operation_type": "ansible_candidate_matched",
|
||||
"owner_agent": "Hermes",
|
||||
"purpose": "把修復候選寫入 executor 可認領佇列",
|
||||
"writes_runtime_state": False,
|
||||
},
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"owner_agent": "AwoooP Ansible check-mode worker",
|
||||
"purpose": "執行 ansible-playbook --check --diff 並留下乾跑收據",
|
||||
"writes_runtime_state": False,
|
||||
},
|
||||
{
|
||||
"operation_type": "ansible_apply_executed",
|
||||
"owner_agent": "AwoooP controlled apply worker",
|
||||
"purpose": "check-mode 通過後,對 allowlisted low / medium / high PlayBook 受控 apply",
|
||||
"writes_runtime_state": True,
|
||||
},
|
||||
{
|
||||
"operation_type": "incident_evidence.post_execution_state",
|
||||
"owner_agent": "post_apply_verifier",
|
||||
"purpose": "apply 後寫入 verifier 結果與 post-execution evidence",
|
||||
"writes_runtime_state": True,
|
||||
},
|
||||
{
|
||||
"operation_type": "knowledge_entries",
|
||||
"owner_agent": "Hermes",
|
||||
"purpose": "把已驗證執行沉澱成 KM / PlayBook trust 候選",
|
||||
"writes_runtime_state": True,
|
||||
},
|
||||
]
|
||||
hard_blockers = [
|
||||
"secret_token_private_key_cookie_session_auth_header_cleartext",
|
||||
"drop_truncate_restore_prune_destructive_database_operation",
|
||||
"reboot_node_drain_irreversible_firewall_or_host_lockout",
|
||||
"credentialed_exploit_or_external_active_scan",
|
||||
"new_paid_provider_cost_ceiling_or_provider_switch_without_replay_shadow_canary",
|
||||
"force_push_delete_repo_refs_or_visibility_change",
|
||||
"critical_or_break_glass_route_without_explicit_break_glass_contract",
|
||||
]
|
||||
legacy_overrides = [
|
||||
{
|
||||
"legacy_area": "report_status_board_no_live_send",
|
||||
"current_effect": "overridden",
|
||||
"new_behavior": "日報 / 週報 / 月報透過 Telegram Gateway 排程派送",
|
||||
},
|
||||
{
|
||||
"legacy_area": "report_live_delivery_owner_review_required",
|
||||
"current_effect": "overridden",
|
||||
"new_behavior": "報告派送走低/中/高風險自動化政策;critical 才 break-glass",
|
||||
},
|
||||
{
|
||||
"legacy_area": "high_risk_owner_review_queue",
|
||||
"current_effect": "overridden_for_high_non_critical",
|
||||
"new_behavior": "high 風險允許 controlled apply;critical / hard blocker 仍不自動",
|
||||
},
|
||||
{
|
||||
"legacy_area": "telegram_no_send_preview_only",
|
||||
"current_effect": "overridden",
|
||||
"new_behavior": "用 Telegram Gateway 實送報告與 actionable receipt;不直接暴露 Bot API",
|
||||
},
|
||||
]
|
||||
payload = {
|
||||
"schema_version": _SCHEMA_VERSION,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"program_status": {
|
||||
"current_task_id": "P2-416-D1N",
|
||||
"status": "current_directive_control_plane_active",
|
||||
"runtime_authority": _RUNTIME_AUTHORITY,
|
||||
"legacy_no_send_no_live_rules_overridden": True,
|
||||
"implementation_completion_percent": 82,
|
||||
"status_note": (
|
||||
"目前有效規則:low / medium / high 風險由 AI Agent 在 allowlist、"
|
||||
"Ansible check-mode、verifier、rollback、KM 與 Telegram receipt 下受控自動處理。"
|
||||
),
|
||||
},
|
||||
"current_policy": {
|
||||
"low_risk_controlled_apply_allowed": "low" in allowed_risks,
|
||||
"medium_risk_controlled_apply_allowed": "medium" in allowed_risks,
|
||||
"high_risk_controlled_apply_allowed": "high" in allowed_risks,
|
||||
"critical_break_glass_required": True,
|
||||
"owner_review_required_for_low_medium_high": False,
|
||||
"direct_bot_api_allowed": False,
|
||||
"telegram_gateway_required": True,
|
||||
"post_apply_verifier_required": True,
|
||||
"km_learning_writeback_required": True,
|
||||
},
|
||||
"runtime_switches": {
|
||||
"ansible_check_mode_worker_enabled": bool(settings.ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER),
|
||||
"ansible_controlled_apply_enabled": bool(settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY),
|
||||
"ansible_controlled_apply_allowed_risk_levels": allowed_risks,
|
||||
"ansible_check_mode_interval_seconds": settings.AWOOOP_ANSIBLE_CHECK_MODE_INTERVAL_SECONDS,
|
||||
"ansible_check_mode_batch_limit": settings.AWOOOP_ANSIBLE_CHECK_MODE_BATCH_LIMIT,
|
||||
"ansible_check_mode_timeout_seconds": settings.AWOOOP_ANSIBLE_CHECK_MODE_TIMEOUT_SECONDS,
|
||||
"ansible_controlled_apply_timeout_seconds": settings.AWOOOP_ANSIBLE_CONTROLLED_APPLY_TIMEOUT_SECONDS,
|
||||
},
|
||||
"agent_roles": [
|
||||
{
|
||||
"agent_id": "openclaw",
|
||||
"role": "仲裁 / hard blocker / replay-shadow-canary gate",
|
||||
"current_job": "只阻擋真正 critical 與 hard blocker,不再用身份保護舊架構",
|
||||
},
|
||||
{
|
||||
"agent_id": "hermes",
|
||||
"role": "報告 / Telegram digest / KM 與 PlayBook trust writeback",
|
||||
"current_job": "日週月報、收據摘要與 verifier 後學習沉澱",
|
||||
},
|
||||
{
|
||||
"agent_id": "nemotron",
|
||||
"role": "市場技術雷達 / no-write replay / challenger scorecard",
|
||||
"current_job": "用市場與回放數據挑戰 OpenClaw / provider / Agent 組合",
|
||||
},
|
||||
{
|
||||
"agent_id": "awooop_ansible_worker",
|
||||
"role": "executor",
|
||||
"current_job": "candidate → check-mode → controlled apply → verifier → KM",
|
||||
},
|
||||
{
|
||||
"agent_id": "telegram_ops",
|
||||
"role": "Telegram Gateway receipt",
|
||||
"current_job": "群組報告、actionable receipt、失敗告警;不展示敏感值或未脫敏資料",
|
||||
},
|
||||
],
|
||||
"report_delivery": {
|
||||
"status": "telegram_gateway_delivery_enabled",
|
||||
"cadences": report_cadences,
|
||||
},
|
||||
"controlled_executor": {
|
||||
"status": "check_mode_then_apply_enabled"
|
||||
if settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY
|
||||
else "check_mode_only_by_config",
|
||||
"operation_receipts": executor_receipts,
|
||||
"required_flow": [
|
||||
"allowlisted_candidate",
|
||||
"ansible_check_mode_success",
|
||||
"controlled_apply",
|
||||
"post_apply_verifier",
|
||||
"auto_repair_execution_receipt",
|
||||
"km_learning_writeback",
|
||||
"telegram_receipt_or_alert",
|
||||
],
|
||||
},
|
||||
"legacy_policy_overrides": legacy_overrides,
|
||||
"hard_blockers": hard_blockers,
|
||||
"visibility_contract": {
|
||||
"frontend_displays_runtime_truth": True,
|
||||
"work_window_transcript_display_allowed": False,
|
||||
"prompt_body_display_allowed": False,
|
||||
"internal_reasoning_display_allowed": False,
|
||||
"sensitive_value_display_allowed": False,
|
||||
"telegram_unredacted_payload_display_allowed": False,
|
||||
"lan_topology_redaction_required": True,
|
||||
},
|
||||
"rollups": {
|
||||
"automated_risk_tier_count": sum(1 for risk in ("low", "medium", "high") if risk in allowed_risks),
|
||||
"hard_blocker_count": len(hard_blockers),
|
||||
"report_cadence_enabled_count": len(report_cadences),
|
||||
"telegram_gateway_delivery_enabled_count": sum(
|
||||
1 for item in report_cadences if item["telegram_gateway_delivery_enabled"]
|
||||
),
|
||||
"direct_bot_api_allowed_count": 0,
|
||||
"controlled_executor_operation_receipt_count": len(executor_receipts),
|
||||
"runtime_write_receipt_type_count": sum(
|
||||
1 for item in executor_receipts if item["writes_runtime_state"]
|
||||
),
|
||||
"legacy_policy_overridden_count": len(legacy_overrides),
|
||||
},
|
||||
}
|
||||
_validate_payload(payload)
|
||||
return payload
|
||||
|
||||
|
||||
def _validate_payload(payload: dict[str, Any]) -> None:
|
||||
if payload.get("schema_version") != _SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version must be {_SCHEMA_VERSION}")
|
||||
status = payload.get("program_status") or {}
|
||||
if status.get("runtime_authority") != _RUNTIME_AUTHORITY:
|
||||
raise ValueError(f"runtime_authority must be {_RUNTIME_AUTHORITY}")
|
||||
policy = payload.get("current_policy") or {}
|
||||
for key in (
|
||||
"low_risk_controlled_apply_allowed",
|
||||
"medium_risk_controlled_apply_allowed",
|
||||
"high_risk_controlled_apply_allowed",
|
||||
"telegram_gateway_required",
|
||||
"post_apply_verifier_required",
|
||||
"km_learning_writeback_required",
|
||||
):
|
||||
if policy.get(key) is not True:
|
||||
raise ValueError(f"current_policy.{key} must be true")
|
||||
if policy.get("owner_review_required_for_low_medium_high") is not False:
|
||||
raise ValueError("owner_review_required_for_low_medium_high must be false")
|
||||
if policy.get("direct_bot_api_allowed") is not False:
|
||||
raise ValueError("direct_bot_api_allowed must be false")
|
||||
visibility = payload.get("visibility_contract") or {}
|
||||
for key in (
|
||||
"work_window_transcript_display_allowed",
|
||||
"prompt_body_display_allowed",
|
||||
"internal_reasoning_display_allowed",
|
||||
"sensitive_value_display_allowed",
|
||||
"telegram_unredacted_payload_display_allowed",
|
||||
):
|
||||
if visibility.get(key) is not False:
|
||||
raise ValueError(f"visibility_contract.{key} must remain false")
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
IwoooS Wazuh manager registry reviewer validation readback.
|
||||
|
||||
This service exposes a committed reviewer-validation contract for future
|
||||
owner-provided redacted Wazuh manager registry exports. It never receives raw
|
||||
payloads, queries Wazuh, reads host data, reads secrets, or authorizes runtime
|
||||
actions.
|
||||
This service exposes a committed reviewer-validation contract and a no-persist
|
||||
validator for owner-provided redacted Wazuh manager registry exports. It never
|
||||
queries Wazuh, reads host data, reads secrets, persists raw payloads, or
|
||||
authorizes runtime actions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -34,6 +35,82 @@ _REQUIRED_FALSE_BOUNDARIES = {
|
||||
"wazuh_manager_restart_authorized",
|
||||
}
|
||||
|
||||
_SENSITIVE_TEXT_PATTERNS = {
|
||||
"internal_ip": re.compile(r"\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b"),
|
||||
"authorization_header": re.compile(r"Authorization\s*:", re.IGNORECASE),
|
||||
"bearer_token": re.compile(r"Bearer\s+[A-Za-z0-9._-]{10,}", re.IGNORECASE),
|
||||
"basic_auth": re.compile(r"Basic\s+[A-Za-z0-9+/=]{10,}", re.IGNORECASE),
|
||||
"password_assignment": re.compile(r"password\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
|
||||
"token_assignment": re.compile(r"token\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
|
||||
"cookie_assignment": re.compile(r"cookie\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
|
||||
"client_keys": re.compile(r"client\.keys", re.IGNORECASE),
|
||||
"private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
|
||||
"work_window_text": re.compile(r"(工作視窗|批准!繼續|source_thread_id|owenhytsai/)", re.IGNORECASE),
|
||||
}
|
||||
|
||||
_FORBIDDEN_KEY_FRAGMENTS = {
|
||||
"authorization_header",
|
||||
"basic_auth",
|
||||
"bearer_token",
|
||||
"client_keys",
|
||||
"cookie",
|
||||
"dashboard_api_secret",
|
||||
"firewall_change",
|
||||
"full_cli_output",
|
||||
"full_journal",
|
||||
"host_write",
|
||||
"hostname",
|
||||
"internal_ip",
|
||||
"nginx_reload",
|
||||
"password",
|
||||
"private_key",
|
||||
"raw_dashboard_request",
|
||||
"raw_log",
|
||||
"raw_wazuh_payload",
|
||||
"stored_api_password",
|
||||
"unredacted_screenshot",
|
||||
}
|
||||
|
||||
_RUNTIME_ACTION_KEYS = {
|
||||
"active_response_enable",
|
||||
"agent_reenroll",
|
||||
"agent_restart",
|
||||
"argocd_sync",
|
||||
"firewall_change",
|
||||
"host_write",
|
||||
"k8s_or_argocd_change",
|
||||
"kali_active_scan",
|
||||
"nginx_reload",
|
||||
"runtime_execution_authorized",
|
||||
"secret_rotation",
|
||||
"wazuh_active_response",
|
||||
"wazuh_agent_reenroll",
|
||||
"wazuh_agent_restart",
|
||||
"wazuh_api_live_query",
|
||||
"wazuh_dashboard_secret_patch",
|
||||
"wazuh_manager_restart",
|
||||
}
|
||||
|
||||
_DASHBOARD_REQUIRED_FIELDS = {
|
||||
"dashboard_api_connection_check_status",
|
||||
"dashboard_api_version_check_status",
|
||||
"dashboard_index_pattern_statuses",
|
||||
"dashboard_api_degradation_root_cause",
|
||||
"dashboard_api_repair_postcheck_ref",
|
||||
}
|
||||
|
||||
_READONLY_CREDENTIAL_REQUIRED_FIELDS = {
|
||||
"collection_method",
|
||||
"manager_health_ref",
|
||||
"redacted_evidence_refs",
|
||||
}
|
||||
|
||||
_ACCOUNTABILITY_REQUIRED_FIELDS = {
|
||||
"followup_owner",
|
||||
"rollback_owner",
|
||||
"postcheck_plan",
|
||||
}
|
||||
|
||||
|
||||
def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
|
||||
security_dir: Path | None = None,
|
||||
@@ -76,6 +153,10 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation(
|
||||
f"docs/security/{_SNAPSHOT_FILE}",
|
||||
"scripts/security/wazuh-manager-registry-reviewer-validation.py",
|
||||
],
|
||||
"owner_export_validation_endpoint": (
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export"
|
||||
),
|
||||
"owner_export_validation_mode": "no_persist_validation_no_runtime_action",
|
||||
"summary": merged_summary,
|
||||
"expected_scope_aliases": _strings(snapshot.get("expected_scope_aliases")),
|
||||
"reviewer_validation_checks": _checks(snapshot.get("reviewer_validation_checks")),
|
||||
@@ -173,6 +254,7 @@ def _evidence_slots(value: Any) -> list[dict[str, Any]]:
|
||||
def _boundary_markers(summary: dict[str, int]) -> list[str]:
|
||||
return [
|
||||
"wazuh_manager_registry_reviewer_validation_visible=true",
|
||||
"wazuh_manager_registry_owner_export_validation_api_available=true",
|
||||
f"wazuh_manager_registry_reviewer_validation_expected_scope_alias_count={summary['expected_scope_alias_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_required_owner_field_count={summary['required_owner_field_count']}",
|
||||
f"wazuh_manager_registry_reviewer_validation_per_host_required_field_count={summary['per_host_required_field_count']}",
|
||||
@@ -225,3 +307,394 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
|
||||
raise ValueError(f"Wazuh manager registry reviewer validation execution_boundaries.{key} 必須維持 false")
|
||||
if boundaries.get("not_authorization") is not True:
|
||||
raise ValueError("Wazuh manager registry reviewer validation not_authorization 必須維持 true")
|
||||
|
||||
|
||||
def validate_iwooos_wazuh_manager_registry_owner_export(
|
||||
owner_export: dict[str, Any],
|
||||
security_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate one redacted owner export without persisting it or 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")))
|
||||
required_owner_fields = _strings(snapshot.get("required_owner_fields"))
|
||||
per_host_required_fields = _strings(snapshot.get("per_host_required_fields"))
|
||||
|
||||
findings: list[dict[str, Any]] = []
|
||||
evidence_status = _initial_evidence_status(snapshot)
|
||||
|
||||
if not isinstance(owner_export, dict):
|
||||
findings.append(_finding("RV-01", "blocker", "request_missing_fields", "owner export 必須是 JSON object", []))
|
||||
return _validation_result(contract, "request_missing_fields", findings, evidence_status)
|
||||
|
||||
sensitive_hits = _collect_sensitive_hits(owner_export)
|
||||
if sensitive_hits:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-07",
|
||||
"critical",
|
||||
"quarantine_sensitive_payload",
|
||||
"owner export 含禁止內容或疑似未脫敏內容,已進隔離分流;回應不回傳原始值。",
|
||||
[hit["path"] for hit in sensitive_hits[:12]],
|
||||
{"categories": sorted({hit["category"] for hit in sensitive_hits})},
|
||||
)
|
||||
)
|
||||
return _validation_result(contract, "quarantine_sensitive_payload", findings, evidence_status)
|
||||
|
||||
runtime_hits = _collect_runtime_action_hits(owner_export)
|
||||
if runtime_hits:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-09",
|
||||
"critical",
|
||||
"reject_runtime_action_request",
|
||||
"owner export 夾帶 runtime action request;收件驗證只允許脫敏證據,不授權 active response、restart、host write 或掃描。",
|
||||
runtime_hits[:12],
|
||||
)
|
||||
)
|
||||
return _validation_result(contract, "reject_runtime_action_request", findings, evidence_status)
|
||||
|
||||
missing_owner_fields = [field for field in required_owner_fields if not _present(owner_export.get(field))]
|
||||
if missing_owner_fields:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-01",
|
||||
"blocker",
|
||||
"request_missing_fields",
|
||||
"owner export envelope 欄位不足,需要補齊後再驗證。",
|
||||
missing_owner_fields,
|
||||
)
|
||||
)
|
||||
|
||||
count_issue = _validate_counts(owner_export)
|
||||
if count_issue:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-02",
|
||||
"blocker",
|
||||
"request_counts_arithmetic_fix",
|
||||
count_issue,
|
||||
["agent_total", "agent_active", "agent_disconnected", "agent_never_connected"],
|
||||
)
|
||||
)
|
||||
|
||||
alias_issue = _validate_aliases(owner_export.get("registry_export_scope_aliases"), expected_aliases)
|
||||
if alias_issue:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-03",
|
||||
"blocker",
|
||||
"request_alias_scope_parity_fix",
|
||||
alias_issue,
|
||||
["registry_export_scope_aliases"],
|
||||
)
|
||||
)
|
||||
|
||||
matrix_issue_paths = _validate_per_host_matrix(
|
||||
owner_export.get("per_host_registry_matrix"),
|
||||
expected_aliases,
|
||||
per_host_required_fields,
|
||||
)
|
||||
if matrix_issue_paths:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-04",
|
||||
"blocker",
|
||||
"request_per_host_matrix_supplement",
|
||||
"逐主機矩陣未完整覆蓋 6 個公開別名或缺少必要欄位。",
|
||||
matrix_issue_paths[:30],
|
||||
)
|
||||
)
|
||||
|
||||
dashboard_missing = [field for field in sorted(_DASHBOARD_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
|
||||
if dashboard_missing:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-05",
|
||||
"blocker",
|
||||
"request_dashboard_api_repair_postcheck",
|
||||
"Dashboard API connection / version / index pattern / root cause / repair postcheck 必須分欄。",
|
||||
dashboard_missing,
|
||||
)
|
||||
)
|
||||
|
||||
readonly_missing = [field for field in sorted(_READONLY_CREDENTIAL_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
|
||||
if readonly_missing:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-06",
|
||||
"blocker",
|
||||
"request_readonly_credential_metadata",
|
||||
"唯讀 credential metadata 與 manager health ref 必須可追溯,且不得含 secret value。",
|
||||
readonly_missing,
|
||||
)
|
||||
)
|
||||
|
||||
accountability_missing = [field for field in sorted(_ACCOUNTABILITY_REQUIRED_FIELDS) if not _present(owner_export.get(field))]
|
||||
if accountability_missing:
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-08",
|
||||
"blocker",
|
||||
"request_owner_accountability_supplement",
|
||||
"followup owner、rollback owner 與 postcheck plan 必須可讀。",
|
||||
accountability_missing,
|
||||
)
|
||||
)
|
||||
|
||||
_mark_evidence_status(evidence_status, owner_export)
|
||||
outcome = _first_blocking_lane(findings) or "accepted_for_readonly_posture_only"
|
||||
if outcome == "accepted_for_readonly_posture_only":
|
||||
evidence_status = [
|
||||
{**slot, "received": True, "accepted": True, "quarantined": False}
|
||||
for slot in evidence_status
|
||||
]
|
||||
findings.append(
|
||||
_finding(
|
||||
"RV-10",
|
||||
"info",
|
||||
"waiting_post_enable_iwooos_readback",
|
||||
"owner export 已通過 no-persist reviewer validation;下一步仍只能進 post-enable IwoooS readback,不開 runtime gate。",
|
||||
["post_enable_iwooos_readback"],
|
||||
)
|
||||
)
|
||||
|
||||
return _validation_result(contract, outcome, findings, evidence_status)
|
||||
|
||||
|
||||
def _validation_result(
|
||||
contract: dict[str, Any],
|
||||
outcome_lane: str,
|
||||
findings: list[dict[str, Any]],
|
||||
evidence_status: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
accepted = outcome_lane == "accepted_for_readonly_posture_only"
|
||||
quarantined = outcome_lane == "quarantine_sensitive_payload"
|
||||
rejected_runtime = outcome_lane == "reject_runtime_action_request"
|
||||
return {
|
||||
"schema_version": "iwooos_wazuh_manager_registry_owner_export_validation_result_v1",
|
||||
"contract_schema_version": contract["schema_version"],
|
||||
"status": outcome_lane,
|
||||
"mode": "no_persist_validation_no_runtime_no_secret_collection",
|
||||
"outcome_lane": outcome_lane,
|
||||
"accepted_for_readonly_posture_only": accepted,
|
||||
"reviewer_validation_passed": accepted,
|
||||
"quarantined": quarantined,
|
||||
"runtime_action_rejected": rejected_runtime,
|
||||
"summary": {
|
||||
"owner_registry_export_received_count": 1,
|
||||
"owner_registry_export_accepted_count": 1 if accepted else 0,
|
||||
"reviewer_validation_passed_count": 1 if accepted else 0,
|
||||
"reviewer_validation_quarantined_count": 1 if quarantined else 0,
|
||||
"manager_registry_accepted_count": 0,
|
||||
"post_enable_readback_passed_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,
|
||||
"evidence_slots": evidence_status,
|
||||
"boundary_markers": [
|
||||
"wazuh_manager_registry_owner_export_validation_received_count=1",
|
||||
f"wazuh_manager_registry_owner_export_validation_accepted_count={1 if accepted else 0}",
|
||||
f"wazuh_manager_registry_owner_export_validation_quarantined_count={1 if quarantined else 0}",
|
||||
"wazuh_manager_registry_owner_export_validation_manager_registry_accepted_count=0",
|
||||
"wazuh_manager_registry_owner_export_validation_runtime_gate_count=0",
|
||||
"wazuh_manager_registry_owner_export_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": "post_enable_iwooos_readback" if accepted else "owner_export_fix_and_resubmit",
|
||||
}
|
||||
|
||||
|
||||
def _finding(
|
||||
check_id: str,
|
||||
severity: str,
|
||||
lane: str,
|
||||
message: str,
|
||||
field_paths: list[str],
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"check_id": check_id,
|
||||
"severity": severity,
|
||||
"lane": lane,
|
||||
"message": message,
|
||||
"field_paths": field_paths,
|
||||
}
|
||||
if extra:
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _initial_evidence_status(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
slots = _evidence_slots(snapshot.get("evidence_slots"))
|
||||
return [{**slot, "received": False, "accepted": False, "quarantined": False} for slot in slots]
|
||||
|
||||
|
||||
def _mark_evidence_status(evidence_status: list[dict[str, Any]], owner_export: dict[str, Any]) -> None:
|
||||
for slot in evidence_status:
|
||||
required_fields = slot.get("required_fields", [])
|
||||
slot["received"] = bool(required_fields) and all(_present(owner_export.get(field)) for field in required_fields)
|
||||
slot["accepted"] = False
|
||||
slot["quarantined"] = False
|
||||
|
||||
|
||||
def _present(value: Any) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
if isinstance(value, str):
|
||||
return bool(value.strip())
|
||||
if isinstance(value, (list, dict, tuple, set)):
|
||||
return bool(value)
|
||||
return True
|
||||
|
||||
|
||||
def _int_or_none(value: Any) -> int | None:
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip().isdigit():
|
||||
return int(value.strip())
|
||||
return None
|
||||
|
||||
|
||||
def _validate_counts(owner_export: dict[str, Any]) -> str | None:
|
||||
fields = ["agent_total", "agent_active", "agent_disconnected", "agent_never_connected"]
|
||||
counts = {field: _int_or_none(owner_export.get(field)) for field in fields}
|
||||
if any(value is None for value in counts.values()):
|
||||
return "agent count 欄位必須是非負整數。"
|
||||
if any(value is not None and value < 0 for value in counts.values()):
|
||||
return "agent count 欄位不得為負數。"
|
||||
total = counts["agent_total"]
|
||||
active = counts["agent_active"]
|
||||
disconnected = counts["agent_disconnected"]
|
||||
never_connected = counts["agent_never_connected"]
|
||||
if total is None or active is None or disconnected is None or never_connected is None:
|
||||
return "agent count 欄位必須是非負整數。"
|
||||
if total < active + disconnected + never_connected:
|
||||
return "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):
|
||||
return "registry_export_scope_aliases 必須是公開別名字串陣列。"
|
||||
alias_set = set(aliases)
|
||||
if len(aliases) != len(alias_set):
|
||||
return "registry_export_scope_aliases 不得重複。"
|
||||
if alias_set != expected_aliases:
|
||||
missing = sorted(expected_aliases - alias_set)
|
||||
extra = sorted(alias_set - expected_aliases)
|
||||
return f"registry_export_scope_aliases 必須剛好等於 6 個公開別名;missing={missing} extra={extra}"
|
||||
return None
|
||||
|
||||
|
||||
def _validate_per_host_matrix(value: Any, expected_aliases: set[str], required_fields: list[str]) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
return ["per_host_registry_matrix"]
|
||||
paths: list[str] = []
|
||||
seen_aliases: list[str] = []
|
||||
for index, item in enumerate(value):
|
||||
if not isinstance(item, dict):
|
||||
paths.append(f"per_host_registry_matrix[{index}]")
|
||||
continue
|
||||
alias = item.get("node_alias")
|
||||
if not isinstance(alias, str) or alias not in expected_aliases:
|
||||
paths.append(f"per_host_registry_matrix[{index}].node_alias")
|
||||
else:
|
||||
seen_aliases.append(alias)
|
||||
for field in required_fields:
|
||||
if not _present(item.get(field)):
|
||||
paths.append(f"per_host_registry_matrix[{index}].{field}")
|
||||
seen_set = set(seen_aliases)
|
||||
if len(seen_aliases) != len(seen_set):
|
||||
paths.append("per_host_registry_matrix.duplicate_node_alias")
|
||||
for missing_alias in sorted(expected_aliases - seen_set):
|
||||
paths.append(f"per_host_registry_matrix.missing.{missing_alias}")
|
||||
for extra_alias in sorted(seen_set - expected_aliases):
|
||||
paths.append(f"per_host_registry_matrix.extra.{extra_alias}")
|
||||
return paths
|
||||
|
||||
|
||||
def _collect_sensitive_hits(value: Any, path: str = "$") -> list[dict[str, str]]:
|
||||
hits: list[dict[str, str]] = []
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
key_text = str(key)
|
||||
key_lower = key_text.lower()
|
||||
for fragment in _FORBIDDEN_KEY_FRAGMENTS:
|
||||
if fragment in key_lower:
|
||||
hits.append({"path": f"{path}.{key_text}", "category": f"forbidden_key:{fragment}"})
|
||||
hits.extend(_collect_sensitive_hits(item, f"{path}.{key_text}"))
|
||||
return hits
|
||||
if isinstance(value, list):
|
||||
for index, item in enumerate(value):
|
||||
hits.extend(_collect_sensitive_hits(item, f"{path}[{index}]"))
|
||||
return hits
|
||||
if isinstance(value, str):
|
||||
for category, pattern in _SENSITIVE_TEXT_PATTERNS.items():
|
||||
if pattern.search(value):
|
||||
hits.append({"path": path, "category": category})
|
||||
return hits
|
||||
|
||||
|
||||
def _collect_runtime_action_hits(value: Any, path: str = "$") -> list[str]:
|
||||
hits: list[str] = []
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
key_text = str(key)
|
||||
normalized_key = key_text.lower().replace("-", "_").replace(" ", "_")
|
||||
if normalized_key in _RUNTIME_ACTION_KEYS and item not in (False, None, "", [], {}):
|
||||
hits.append(f"{path}.{key_text}")
|
||||
hits.extend(_collect_runtime_action_hits(item, f"{path}.{key_text}"))
|
||||
return hits
|
||||
if isinstance(value, list):
|
||||
for index, item in enumerate(value):
|
||||
hits.extend(_collect_runtime_action_hits(item, f"{path}[{index}]"))
|
||||
return hits
|
||||
if isinstance(value, str):
|
||||
normalized = value.lower().replace("-", "_").replace(" ", "_")
|
||||
if normalized in _RUNTIME_ACTION_KEYS:
|
||||
hits.append(path)
|
||||
return hits
|
||||
|
||||
|
||||
def _first_blocking_lane(findings: list[dict[str, Any]]) -> str | None:
|
||||
for lane in (
|
||||
"quarantine_sensitive_payload",
|
||||
"reject_runtime_action_request",
|
||||
"request_missing_fields",
|
||||
"request_counts_arithmetic_fix",
|
||||
"request_alias_scope_parity_fix",
|
||||
"request_per_host_matrix_supplement",
|
||||
"request_dashboard_api_repair_postcheck",
|
||||
"request_readonly_credential_metadata",
|
||||
"request_owner_accountability_supplement",
|
||||
):
|
||||
if any(item.get("lane") == lane for item in findings):
|
||||
return lane
|
||||
return None
|
||||
|
||||
@@ -40,8 +40,12 @@ logger = structlog.get_logger(__name__)
|
||||
# 台北時區 (UTC+8)
|
||||
_TZ_TAIPEI = timezone(timedelta(hours=8))
|
||||
|
||||
# 日度報告觸發時間(台北時間 08:00)
|
||||
# 日 / 週 / 月報觸發時間(台北時間)
|
||||
DAILY_REPORT_HOUR_TAIPEI = 8
|
||||
WEEKLY_REPORT_WEEKDAY_TAIPEI = 4 # Friday, datetime.weekday(): Monday=0
|
||||
WEEKLY_REPORT_HOUR_TAIPEI = 10
|
||||
MONTHLY_REPORT_DAY_TAIPEI = 1
|
||||
MONTHLY_REPORT_HOUR_TAIPEI = 9
|
||||
|
||||
# Postmortem 觸發最低時長(分鐘)
|
||||
POSTMORTEM_MIN_DURATION_MINUTES = 10
|
||||
@@ -341,13 +345,13 @@ class ReportGenerationService:
|
||||
lines.append(" 只讀判讀:不自動改排程、不直接發修復、不取代人工批准。")
|
||||
return lines
|
||||
|
||||
def format_monthly_report_preview(
|
||||
def format_monthly_report(
|
||||
self,
|
||||
source_health: dict[str, Any] | None,
|
||||
*,
|
||||
generated_at: datetime | None = None,
|
||||
) -> str:
|
||||
"""Format a monthly no-send preview from the unified report source-health model."""
|
||||
"""Format the monthly report from the unified report source-health model."""
|
||||
now = generated_at or now_taipei()
|
||||
source_health = source_health or {}
|
||||
previews = source_health.get("no_send_previews") or []
|
||||
@@ -359,22 +363,31 @@ class ReportGenerationService:
|
||||
gap_text = ", ".join(str(gap_id) for gap_id in gap_ids[:5]) if gap_ids else "無"
|
||||
|
||||
lines = [
|
||||
"<b>📊 AWOOOI 月報 no-send preview</b>",
|
||||
"<b>📊 AWOOOI 月報</b>",
|
||||
f"<b>{now.strftime('%Y-%m')}</b> | {now.strftime('%Y-%m-%d %H:%M')} 台北時間",
|
||||
"",
|
||||
"<b>🧭 月報交付狀態</b>",
|
||||
f" 狀態: {html.escape(str(monthly_preview.get('delivery_state') or 'no_send_preview'))}",
|
||||
f" 狀態: {html.escape(str(monthly_preview.get('delivery_state') or 'telegram_gateway_delivery'))}",
|
||||
f" Owner: {html.escape(str(monthly_preview.get('owner_agent') or '未指定'))}",
|
||||
f" 缺口來源: {html.escape(gap_text)}",
|
||||
" 實發: 0 | Gateway queue write: 0",
|
||||
" 派送: Telegram Gateway | 回執: report_generation_service log / Gateway result",
|
||||
]
|
||||
lines.extend(self._format_report_source_health_block(source_health))
|
||||
lines += [
|
||||
"",
|
||||
"<i>🤖 AWOOOI 月報草案 | no-send preview,不代表已授權發送或自動修復</i>",
|
||||
"<i>🤖 AWOOOI 月報自動派送 | 低/中/高風險改善建議由 AI Agent 受控接手,critical / secrets / destructive 仍維持硬阻擋</i>",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def format_monthly_report_preview(
|
||||
self,
|
||||
source_health: dict[str, Any] | None,
|
||||
*,
|
||||
generated_at: datetime | None = None,
|
||||
) -> str:
|
||||
"""Backward-compatible alias for callers that still ask for preview text."""
|
||||
return self.format_monthly_report(source_health, generated_at=generated_at)
|
||||
|
||||
def format_sre_digest_preview(
|
||||
self,
|
||||
source_health: dict[str, Any] | None,
|
||||
@@ -572,6 +585,26 @@ class ReportGenerationService:
|
||||
except Exception as e:
|
||||
logger.error("daily_report_failed", error=str(e))
|
||||
|
||||
async def send_monthly_report(self) -> bool:
|
||||
"""收集月報資料 → 組裝 → 推送 Telegram SRE 群組。"""
|
||||
try:
|
||||
source_health = await self.collect_report_source_health(days=30)
|
||||
report_text = self.format_monthly_report(source_health)
|
||||
|
||||
from src.services.telegram_gateway import get_telegram_gateway
|
||||
gateway = get_telegram_gateway()
|
||||
await gateway.send_to_group(report_text, parse_mode="HTML")
|
||||
|
||||
logger.info(
|
||||
"monthly_report_sent",
|
||||
source_ok=(source_health.get("rollups") or {}).get("source_ok_count"),
|
||||
source_total=(source_health.get("rollups") or {}).get("source_count"),
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("monthly_report_failed", error=str(e))
|
||||
return False
|
||||
|
||||
async def trigger_postmortem(
|
||||
self,
|
||||
incident_id: str,
|
||||
@@ -767,6 +800,40 @@ def _seconds_until_next_report() -> float:
|
||||
return (target - now).total_seconds()
|
||||
|
||||
|
||||
def _seconds_until_next_weekly_report() -> float:
|
||||
"""計算距下一個週五 10:00 台北時間的秒數。"""
|
||||
now = now_taipei()
|
||||
target = now.replace(
|
||||
hour=WEEKLY_REPORT_HOUR_TAIPEI,
|
||||
minute=0,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
days_until = (WEEKLY_REPORT_WEEKDAY_TAIPEI - now.weekday()) % 7
|
||||
target += timedelta(days=days_until)
|
||||
if now >= target:
|
||||
target += timedelta(days=7)
|
||||
return (target - now).total_seconds()
|
||||
|
||||
|
||||
def _seconds_until_next_monthly_report() -> float:
|
||||
"""計算距下一個每月 1 日 09:00 台北時間的秒數。"""
|
||||
now = now_taipei()
|
||||
target = now.replace(
|
||||
day=MONTHLY_REPORT_DAY_TAIPEI,
|
||||
hour=MONTHLY_REPORT_HOUR_TAIPEI,
|
||||
minute=0,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
if now >= target:
|
||||
if now.month == 12:
|
||||
target = target.replace(year=now.year + 1, month=1)
|
||||
else:
|
||||
target = target.replace(month=now.month + 1)
|
||||
return (target - now).total_seconds()
|
||||
|
||||
|
||||
async def run_daily_report_loop() -> None:
|
||||
"""
|
||||
日度巡檢報告無限排程迴圈
|
||||
@@ -801,6 +868,63 @@ async def run_daily_report_loop() -> None:
|
||||
await service.send_daily_report()
|
||||
|
||||
|
||||
async def run_weekly_report_loop() -> None:
|
||||
"""週報排程迴圈:每週五 10:00 台北時間發送到 Telegram。"""
|
||||
logger.info(
|
||||
"weekly_report_loop_started",
|
||||
weekday_taipei=WEEKLY_REPORT_WEEKDAY_TAIPEI,
|
||||
trigger_hour_taipei=WEEKLY_REPORT_HOUR_TAIPEI,
|
||||
)
|
||||
|
||||
while True:
|
||||
sleep_seconds = _seconds_until_next_weekly_report()
|
||||
logger.info(
|
||||
"weekly_report_next_in",
|
||||
sleep_seconds=int(sleep_seconds),
|
||||
next_at=f"Friday {WEEKLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間",
|
||||
)
|
||||
await asyncio.sleep(sleep_seconds)
|
||||
|
||||
from src.services.ai_advisory_helpers import try_acquire_daily_lock
|
||||
if not await try_acquire_daily_lock("weekly_report"):
|
||||
logger.info("weekly_report_skipped_other_pod")
|
||||
continue
|
||||
|
||||
logger.info("weekly_report_triggered")
|
||||
try:
|
||||
from src.services.weekly_report_service import get_weekly_report_service
|
||||
await get_weekly_report_service().send_weekly_report()
|
||||
except Exception as exc:
|
||||
logger.error("weekly_report_loop_failed", error=str(exc))
|
||||
|
||||
|
||||
async def run_monthly_report_loop() -> None:
|
||||
"""月報排程迴圈:每月 1 日 09:00 台北時間發送到 Telegram。"""
|
||||
service = ReportGenerationService()
|
||||
logger.info(
|
||||
"monthly_report_loop_started",
|
||||
day_taipei=MONTHLY_REPORT_DAY_TAIPEI,
|
||||
trigger_hour_taipei=MONTHLY_REPORT_HOUR_TAIPEI,
|
||||
)
|
||||
|
||||
while True:
|
||||
sleep_seconds = _seconds_until_next_monthly_report()
|
||||
logger.info(
|
||||
"monthly_report_next_in",
|
||||
sleep_seconds=int(sleep_seconds),
|
||||
next_at=f"day {MONTHLY_REPORT_DAY_TAIPEI} {MONTHLY_REPORT_HOUR_TAIPEI:02d}:00 台北時間",
|
||||
)
|
||||
await asyncio.sleep(sleep_seconds)
|
||||
|
||||
from src.services.ai_advisory_helpers import try_acquire_daily_lock
|
||||
if not await try_acquire_daily_lock("monthly_report"):
|
||||
logger.info("monthly_report_skipped_other_pod")
|
||||
continue
|
||||
|
||||
logger.info("monthly_report_triggered")
|
||||
await service.send_monthly_report()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Factory Function
|
||||
# =============================================================================
|
||||
|
||||
63
apps/api/tests/test_ai_agent_autonomous_runtime_control.py
Normal file
63
apps/api/tests/test_ai_agent_autonomous_runtime_control.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from src.services.ai_agent_autonomous_runtime_control import (
|
||||
build_ai_agent_autonomous_runtime_control,
|
||||
)
|
||||
|
||||
|
||||
def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive():
|
||||
data = build_ai_agent_autonomous_runtime_control()
|
||||
|
||||
assert data["schema_version"] == "ai_agent_autonomous_runtime_control_v1"
|
||||
assert data["program_status"]["runtime_authority"] == (
|
||||
"current_owner_directive_controlled_ai_automation"
|
||||
)
|
||||
assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True
|
||||
assert data["current_policy"]["low_risk_controlled_apply_allowed"] is True
|
||||
assert data["current_policy"]["medium_risk_controlled_apply_allowed"] is True
|
||||
assert data["current_policy"]["high_risk_controlled_apply_allowed"] is True
|
||||
assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False
|
||||
assert data["current_policy"]["telegram_gateway_required"] is True
|
||||
assert data["current_policy"]["direct_bot_api_allowed"] is False
|
||||
assert data["current_policy"]["post_apply_verifier_required"] is True
|
||||
assert data["current_policy"]["km_learning_writeback_required"] is True
|
||||
|
||||
|
||||
def test_ai_agent_autonomous_runtime_control_exposes_reports_and_executor_receipts():
|
||||
data = build_ai_agent_autonomous_runtime_control()
|
||||
|
||||
cadences = {item["cadence"]: item for item in data["report_delivery"]["cadences"]}
|
||||
assert set(cadences) == {"daily", "weekly", "monthly"}
|
||||
assert {item["telegram_gateway_delivery_enabled"] for item in cadences.values()} == {True}
|
||||
assert {item["direct_bot_api_allowed"] for item in cadences.values()} == {False}
|
||||
assert "run_daily_report_loop" in cadences["daily"]["worker"]
|
||||
assert "run_weekly_report_loop" in cadences["weekly"]["worker"]
|
||||
assert "run_monthly_report_loop" in cadences["monthly"]["worker"]
|
||||
|
||||
operation_types = {
|
||||
item["operation_type"]
|
||||
for item in data["controlled_executor"]["operation_receipts"]
|
||||
}
|
||||
assert {
|
||||
"ansible_candidate_matched",
|
||||
"ansible_check_mode_executed",
|
||||
"ansible_apply_executed",
|
||||
"incident_evidence.post_execution_state",
|
||||
"knowledge_entries",
|
||||
}.issubset(operation_types)
|
||||
assert data["rollups"]["automated_risk_tier_count"] == 3
|
||||
assert data["rollups"]["report_cadence_enabled_count"] == 3
|
||||
assert data["rollups"]["direct_bot_api_allowed_count"] == 0
|
||||
assert data["rollups"]["legacy_policy_overridden_count"] >= 4
|
||||
|
||||
|
||||
def test_ai_agent_autonomous_runtime_control_keeps_hard_blockers_and_redaction():
|
||||
data = build_ai_agent_autonomous_runtime_control()
|
||||
|
||||
assert "secret_token_private_key_cookie_session_auth_header_cleartext" in data["hard_blockers"]
|
||||
assert "drop_truncate_restore_prune_destructive_database_operation" in data["hard_blockers"]
|
||||
assert "force_push_delete_repo_refs_or_visibility_change" in data["hard_blockers"]
|
||||
visibility = data["visibility_contract"]
|
||||
assert visibility["work_window_transcript_display_allowed"] is False
|
||||
assert visibility["prompt_body_display_allowed"] is False
|
||||
assert visibility["internal_reasoning_display_allowed"] is False
|
||||
assert visibility["sensitive_value_display_allowed"] is False
|
||||
assert visibility["telegram_unredacted_payload_display_allowed"] is False
|
||||
@@ -0,0 +1,66 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.main import app
|
||||
|
||||
|
||||
_PUBLIC_FORBIDDEN_TERMS = [
|
||||
"工作視窗",
|
||||
"對話內容",
|
||||
"批准!繼續",
|
||||
"In app browser",
|
||||
"My request for Codex",
|
||||
"browser_context",
|
||||
"codex_user_message",
|
||||
"prompt_text",
|
||||
"raw prompt",
|
||||
"raw_prompt",
|
||||
"raw payload",
|
||||
"raw_payload",
|
||||
"private reasoning",
|
||||
"chain_of_thought",
|
||||
"authorization header",
|
||||
"authorization_header",
|
||||
"secret value",
|
||||
"secret_value",
|
||||
]
|
||||
|
||||
|
||||
def _collect_strings(value):
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
if isinstance(value, list):
|
||||
strings = []
|
||||
for item in value:
|
||||
strings.extend(_collect_strings(item))
|
||||
return strings
|
||||
if isinstance(value, dict):
|
||||
strings = []
|
||||
for item in value.values():
|
||||
strings.extend(_collect_strings(item))
|
||||
return strings
|
||||
return []
|
||||
|
||||
|
||||
def test_get_ai_agent_autonomous_runtime_control_api():
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/v1/agents/agent-autonomous-runtime-control")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "ai_agent_autonomous_runtime_control_v1"
|
||||
assert data["program_status"]["runtime_authority"] == (
|
||||
"current_owner_directive_controlled_ai_automation"
|
||||
)
|
||||
assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False
|
||||
assert data["report_delivery"]["status"] == "telegram_gateway_delivery_enabled"
|
||||
assert data["rollups"]["report_cadence_enabled_count"] == 3
|
||||
|
||||
|
||||
def test_get_ai_agent_autonomous_runtime_control_api_redacts_public_terms():
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/v1/agents/agent-autonomous-runtime-control")
|
||||
|
||||
assert response.status_code == 200
|
||||
all_text = "\n".join(_collect_strings(response.json()))
|
||||
for term in _PUBLIC_FORBIDDEN_TERMS:
|
||||
assert term not in all_text
|
||||
@@ -6,15 +6,78 @@ 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_owner_export,
|
||||
)
|
||||
|
||||
|
||||
EXPECTED_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",
|
||||
]
|
||||
|
||||
|
||||
def _client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _valid_owner_export() -> dict:
|
||||
return {
|
||||
"owner_role": "資安負責人",
|
||||
"team": "IwoooS",
|
||||
"decision": "accept_readonly_registry_export_for_reviewer_validation",
|
||||
"decision_reason": "Wazuh manager registry 已脫敏,供 reviewer 驗證主機覆蓋與 Dashboard API 修復狀態。",
|
||||
"affected_scope": "iwooos_wazuh_expected_scope_aliases",
|
||||
"collection_method": "owner_export_redacted_summary",
|
||||
"agent_total": 6,
|
||||
"agent_active": 2,
|
||||
"agent_disconnected": 3,
|
||||
"agent_never_connected": 1,
|
||||
"last_seen_window_start": "2026-06-27T15:00:00+08:00",
|
||||
"last_seen_window_end": "2026-06-27T16:00:00+08:00",
|
||||
"registry_collected_at": "2026-06-27T16:05:00+08:00",
|
||||
"registry_export_scope_aliases": EXPECTED_ALIASES,
|
||||
"per_host_registry_matrix": [
|
||||
{
|
||||
"node_alias": alias,
|
||||
"scope_role": "managed_scope",
|
||||
"registry_presence": "present",
|
||||
"agent_status_bucket": "active" if index < 2 else "disconnected",
|
||||
"last_seen_state": "within_owner_window" if index < 2 else "outside_owner_window",
|
||||
"manager_group_ref": f"group-ref-{index}",
|
||||
"agent_id_redacted_ref": f"agent-redacted-ref-{index}",
|
||||
"gap_reason": "none" if index < 2 else "owner_followup_required",
|
||||
"redacted_evidence_ref": f"evidence-ref-{index}",
|
||||
}
|
||||
for index, alias in enumerate(EXPECTED_ALIASES)
|
||||
],
|
||||
"registry_gap_reason_by_alias": {
|
||||
alias: "none" if index < 2 else "owner_followup_required"
|
||||
for index, alias in enumerate(EXPECTED_ALIASES)
|
||||
},
|
||||
"registry_export_summary_ref": "evidence-ref-registry-summary",
|
||||
"manager_health_ref": "evidence-ref-manager-health",
|
||||
"dashboard_api_status_ref": "evidence-ref-dashboard-api",
|
||||
"dashboard_api_connection_check_status": "ok",
|
||||
"dashboard_api_version_check_status": "ok",
|
||||
"dashboard_index_pattern_statuses": ["alerts_ok", "monitoring_ok", "statistics_ok"],
|
||||
"dashboard_api_degradation_root_cause": "stored_api_connection_repaired_by_owner",
|
||||
"dashboard_api_repair_postcheck_ref": "evidence-ref-dashboard-postcheck",
|
||||
"redacted_evidence_refs": [
|
||||
"evidence-ref-registry-summary",
|
||||
"evidence-ref-dashboard-postcheck",
|
||||
],
|
||||
"followup_owner": "IwoooS reviewer",
|
||||
"rollback_owner": "IwoooS runtime owner",
|
||||
"postcheck_plan": "post_enable_iwooos_readback_no_raw_payload",
|
||||
}
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_is_waiting_only() -> None:
|
||||
payload = load_latest_iwooos_wazuh_manager_registry_reviewer_validation()
|
||||
|
||||
@@ -94,3 +157,82 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
|
||||
assert "source_thread_id" not in response.text
|
||||
assert "owenhytsai/" not in response.text
|
||||
assert "WAZUH_API_PASSWORD" not in response.text
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_owner_export_validation_accepts_redacted_payload() -> None:
|
||||
payload = validate_iwooos_wazuh_manager_registry_owner_export(_valid_owner_export())
|
||||
|
||||
assert payload["schema_version"] == "iwooos_wazuh_manager_registry_owner_export_validation_result_v1"
|
||||
assert payload["status"] == "accepted_for_readonly_posture_only"
|
||||
assert payload["accepted_for_readonly_posture_only"] is True
|
||||
assert payload["reviewer_validation_passed"] is True
|
||||
assert payload["summary"]["owner_registry_export_received_count"] == 1
|
||||
assert payload["summary"]["owner_registry_export_accepted_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"]["runtime_execution_authorized"] is False
|
||||
assert payload["boundaries"]["manager_registry_accepted_updated"] is False
|
||||
assert all(slot["received"] is True for slot in payload["evidence_slots"])
|
||||
assert all(slot["accepted"] is True for slot in payload["evidence_slots"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_owner_export_validation_api_does_not_update_global_counters() -> None:
|
||||
client = _client()
|
||||
response = client.post(
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export",
|
||||
json=_valid_owner_export(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["status"] == "accepted_for_readonly_posture_only"
|
||||
assert result["summary"]["owner_registry_export_accepted_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"]["owner_registry_export_received_count"] == 0
|
||||
assert readback["summary"]["owner_registry_export_accepted_count"] == 0
|
||||
assert readback["summary"]["manager_registry_accepted_count"] == 0
|
||||
assert readback["summary"]["runtime_gate_count"] == 0
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_owner_export_validation_requests_missing_fields() -> None:
|
||||
candidate = _valid_owner_export()
|
||||
candidate.pop("decision_reason")
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
|
||||
|
||||
assert payload["status"] == "request_missing_fields"
|
||||
assert payload["accepted_for_readonly_posture_only"] is False
|
||||
assert payload["summary"]["owner_registry_export_received_count"] == 1
|
||||
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
|
||||
assert any("decision_reason" in finding["field_paths"] for finding in payload["validation_findings"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_owner_export_validation_quarantines_sensitive_payload() -> None:
|
||||
candidate = _valid_owner_export()
|
||||
candidate["manager_health_ref"] = "health evidence mentioned 10.250.250.250 by mistake"
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
|
||||
|
||||
assert payload["status"] == "quarantine_sensitive_payload"
|
||||
assert payload["quarantined"] is True
|
||||
assert payload["summary"]["reviewer_validation_quarantined_count"] == 1
|
||||
assert payload["summary"]["owner_registry_export_accepted_count"] == 0
|
||||
assert "10.250.250.250" not in str(payload)
|
||||
assert any(finding["check_id"] == "RV-07" for finding in payload["validation_findings"])
|
||||
|
||||
|
||||
def test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_action_request() -> None:
|
||||
candidate = _valid_owner_export()
|
||||
candidate["requested_actions"] = ["wazuh_active_response"]
|
||||
|
||||
payload = validate_iwooos_wazuh_manager_registry_owner_export(candidate)
|
||||
|
||||
assert payload["status"] == "reject_runtime_action_request"
|
||||
assert payload["runtime_action_rejected"] is True
|
||||
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"])
|
||||
|
||||
@@ -31,6 +31,8 @@ from src.services.report_generation_service import (
|
||||
PostmortemData,
|
||||
ReportGenerationService,
|
||||
_seconds_until_next_report,
|
||||
_seconds_until_next_monthly_report,
|
||||
_seconds_until_next_weekly_report,
|
||||
)
|
||||
from src.services.weekly_report_service import WeeklyReportService
|
||||
|
||||
@@ -426,8 +428,8 @@ class TestFormatDailyReport:
|
||||
assert "全 0 判讀: source_gap_or_no_signal_requires_review" in report
|
||||
assert "不自動改排程" in report
|
||||
|
||||
def test_monthly_preview_contains_no_send_source_health(self):
|
||||
"""月報 preview 應顯示 no-send 邊界與資產沉澱"""
|
||||
def test_monthly_report_contains_telegram_gateway_source_health(self):
|
||||
"""月報應顯示 Telegram Gateway 派送與資產沉澱。"""
|
||||
source_health = {
|
||||
"rollups": {
|
||||
"source_ok_count": 2,
|
||||
@@ -452,19 +454,24 @@ class TestFormatDailyReport:
|
||||
],
|
||||
}
|
||||
svc = ReportGenerationService()
|
||||
report = svc.format_monthly_report_preview(
|
||||
report = svc.format_monthly_report(
|
||||
source_health,
|
||||
generated_at=datetime(2026, 6, 18, 10, 0, tzinfo=_TZ_TAIPEI),
|
||||
)
|
||||
|
||||
assert "月報 no-send preview" in report
|
||||
assert "AWOOOI 月報" in report
|
||||
assert "Owner: Hermes" in report
|
||||
assert "實發: 0" in report
|
||||
assert "Telegram Gateway" in report
|
||||
assert "來源: <code>2/5</code>" in report
|
||||
assert "resolution_stats" in report
|
||||
assert "KM: draft_ready 3/4" in report
|
||||
assert "Verifier: source_health_ready 1/2" in report
|
||||
assert "不代表已授權發送或自動修復" in report
|
||||
assert "AI Agent 受控接手" in report
|
||||
|
||||
def test_weekly_and_monthly_report_schedule_helpers_return_positive_seconds(self):
|
||||
assert _seconds_until_next_report() > 0
|
||||
assert _seconds_until_next_weekly_report() > 0
|
||||
assert _seconds_until_next_monthly_report() > 0
|
||||
|
||||
def test_sre_digest_preview_contains_assets_and_boundaries(self):
|
||||
"""SRE 戰情室 digest 應收斂缺口、資產與 no-send 邊界"""
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_weekly_report_preview_exposes_source_health_no_send_preview():
|
||||
assert "不自動改排程" in preview
|
||||
|
||||
|
||||
def test_monthly_report_preview_exposes_source_health_no_send_preview():
|
||||
def test_monthly_report_preview_exposes_source_health_and_gateway_delivery():
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/v1/stats/monthly/preview")
|
||||
|
||||
@@ -65,11 +65,11 @@ def test_monthly_report_preview_exposes_source_health_no_send_preview():
|
||||
assert "formatted_preview" in data
|
||||
|
||||
preview = data["formatted_preview"]
|
||||
assert "月報 no-send preview" in preview
|
||||
assert "AWOOOI 月報" in preview
|
||||
assert "報表資料源 / 沉澱" in preview
|
||||
assert f"來源: <code>{data['source_ok_count']}/{data['source_total_count']}</code>" in preview
|
||||
assert "實發: 0" in preview
|
||||
assert "不代表已授權發送或自動修復" in preview
|
||||
assert "Telegram Gateway" in preview
|
||||
assert "AI Agent 受控接手" in preview
|
||||
|
||||
|
||||
def test_sre_digest_preview_exposes_source_health_no_send_preview():
|
||||
|
||||
@@ -1866,10 +1866,16 @@
|
||||
"needsHumanYes": "需要",
|
||||
"needsHumanNo": "不需要",
|
||||
"stateLabels": {
|
||||
"verificationDegradedManualRequired": "驗證退化,需人工確認"
|
||||
"verificationDegradedManualRequired": "驗證退化,AI 進入 verifier / rollback",
|
||||
"verificationDegradedAiVerifierRequired": "驗證退化,AI 進入 verifier / rollback",
|
||||
"diagnosticOnlyAiRepairRequired": "只完成診斷,AI 補修復候選",
|
||||
"noActionAiCandidateRequired": "未找到修復候選,AI 補 PlayBook"
|
||||
},
|
||||
"nextActionLabels": {
|
||||
"manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復"
|
||||
"manualVerifyOrRepair": "AI 執行 verifier 或 rollback 判定",
|
||||
"runVerifierOrRollbackCandidate": "AI 執行 verifier 或 rollback 判定",
|
||||
"autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選",
|
||||
"autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試"
|
||||
},
|
||||
"reasonLabels": {
|
||||
"incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident仍開啟"
|
||||
@@ -3681,6 +3687,41 @@
|
||||
"runwayTitle": "AI Agents 專業執行跑道",
|
||||
"runwayBadge": "只讀 / 乾跑 / 審核 / 回寫",
|
||||
"runwaySummary": "可無寫入推進 {noWrite} 類;正式執行 {runtime}、Telegram 發送 {sends}、production 寫入 {writes} 仍依 gate。",
|
||||
"currentAutonomy": {
|
||||
"title": "目前有效自主化控制層",
|
||||
"policyTitle": "目前執行政策",
|
||||
"runtimeTitle": "Worker / Gateway 開關",
|
||||
"executorTitle": "Executor 收據鏈",
|
||||
"overrideTitle": "舊規範覆寫",
|
||||
"hardBlockerTitle": "仍維持硬阻擋",
|
||||
"hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。",
|
||||
"badges": {
|
||||
"override": "舊 no-send / no-live 已覆寫",
|
||||
"gateway": "Telegram Gateway"
|
||||
},
|
||||
"metrics": {
|
||||
"completion": "目前完成度",
|
||||
"riskTiers": "自動風險層",
|
||||
"reports": "日週月報",
|
||||
"gateway": "Gateway 派送",
|
||||
"executor": "Executor 收據",
|
||||
"hardBlockers": "硬阻擋"
|
||||
},
|
||||
"policy": {
|
||||
"low": "低風險",
|
||||
"medium": "中風險",
|
||||
"high": "高風險",
|
||||
"noOwnerReview": "低/中/高人工 gate={value}",
|
||||
"verifier": "post-apply verifier={value}",
|
||||
"km": "KM / PlayBook 回寫={value}"
|
||||
},
|
||||
"runtime": {
|
||||
"checkMode": "Ansible check-mode worker",
|
||||
"apply": "Controlled apply",
|
||||
"botApi": "Direct Bot API",
|
||||
"gatewayOnly": "只允許既有 Telegram Gateway,不暴露 token / chat id。"
|
||||
}
|
||||
},
|
||||
"runway": {
|
||||
"readOnlyInvestigation": {
|
||||
"label": "主動巡檢與證據蒐集",
|
||||
@@ -9755,12 +9796,12 @@
|
||||
"verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納",
|
||||
"operator_review_handoff_and_execute_manual_plan": "Operator review交接並執行人工方案",
|
||||
"run_verification_scan_then_record_result": "執行驗證掃描並記錄結果",
|
||||
"open_manual_investigation_with_failed_verification": "建立人工調查並附上失敗驗證",
|
||||
"open_manual_investigation_with_failed_verification": "建立 AI verifier / rollback 調查並附上失敗驗證",
|
||||
"verify_k8s_matches_git_baseline": "驗證 K8s與Git baseline 一致",
|
||||
"confirm_no_repeat_after_rollback": "確認回滾後不再重複",
|
||||
"monitor_for_recurrence": "監控是否復發",
|
||||
"retry_pr_lookup_then_review_drift": "重試 PR 查詢後 review drift",
|
||||
"manual_investigation_or_ansible_check_mode": "人工調查或Ansible check-mode",
|
||||
"manual_investigation_or_ansible_check_mode": "AI 調查或 Ansible check-mode",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"pr": {
|
||||
@@ -10669,11 +10710,15 @@
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
|
||||
"manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
|
||||
"manualInvestigation": "人工調查卡點並補證據",
|
||||
"reviewVerifier": "執行或審查修復後 verifier",
|
||||
"manualInvestigation": "AI 調查卡點並補證據",
|
||||
"autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選",
|
||||
"autoVerifyOrRollback": "AI 執行 verifier 或 rollback 判定",
|
||||
"autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試",
|
||||
"autoGenerateRepairOrBreakGlass": "AI 產生修復包;硬阻擋才 break-glass",
|
||||
"reviewVerifier": "執行修復後 verifier",
|
||||
"monitorRegression": "持續觀察是否復發",
|
||||
"collectEvidenceOrWait": "補收證據或等待下一筆 recurrence",
|
||||
"collectMaintenanceRollback": "補齊維護窗口與 rollback owner",
|
||||
@@ -11272,7 +11317,7 @@
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件"
|
||||
"manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件"
|
||||
},
|
||||
"items": {
|
||||
"km": "KM",
|
||||
@@ -20793,6 +20838,8 @@
|
||||
"title": "Owner export 進來後,先由 reviewer 驗收脫敏清單",
|
||||
"subtitle": "這張卡固定 Wazuh manager registry owner export 的驗收規則:欄位、計數、公開別名矩陣、Dashboard API 修復讀回、唯讀 credential metadata、拒收內容與下一個 Gate 都先可視化;目前尚未收到、尚未接受,也不開 runtime。",
|
||||
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
|
||||
"validationEndpointLabel": "脫敏 owner export 驗證端點",
|
||||
"validationModeLabel": "驗證模式",
|
||||
"slotReceivedLabel": "已收件",
|
||||
"slotAcceptedLabel": "已接受",
|
||||
"slotNextGateLabel": "下一關",
|
||||
|
||||
@@ -1866,10 +1866,16 @@
|
||||
"needsHumanYes": "需要",
|
||||
"needsHumanNo": "不需要",
|
||||
"stateLabels": {
|
||||
"verificationDegradedManualRequired": "驗證退化,需人工確認"
|
||||
"verificationDegradedManualRequired": "驗證退化,AI 進入 verifier / rollback",
|
||||
"verificationDegradedAiVerifierRequired": "驗證退化,AI 進入 verifier / rollback",
|
||||
"diagnosticOnlyAiRepairRequired": "只完成診斷,AI 補修復候選",
|
||||
"noActionAiCandidateRequired": "未找到修復候選,AI 補 PlayBook"
|
||||
},
|
||||
"nextActionLabels": {
|
||||
"manualVerifyOrRepair": "人工確認修復狀態;需要時重新送審修復"
|
||||
"manualVerifyOrRepair": "AI 執行 verifier 或 rollback 判定",
|
||||
"runVerifierOrRollbackCandidate": "AI 執行 verifier 或 rollback 判定",
|
||||
"autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選",
|
||||
"autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試"
|
||||
},
|
||||
"reasonLabels": {
|
||||
"incidentOpenAfterSuccessfulExecution": "自動執行已完成,但 Incident仍開啟"
|
||||
@@ -3681,6 +3687,41 @@
|
||||
"runwayTitle": "AI Agents 專業執行跑道",
|
||||
"runwayBadge": "只讀 / 乾跑 / 審核 / 回寫",
|
||||
"runwaySummary": "可無寫入推進 {noWrite} 類;正式執行 {runtime}、Telegram 發送 {sends}、production 寫入 {writes} 仍依 gate。",
|
||||
"currentAutonomy": {
|
||||
"title": "目前有效自主化控制層",
|
||||
"policyTitle": "目前執行政策",
|
||||
"runtimeTitle": "Worker / Gateway 開關",
|
||||
"executorTitle": "Executor 收據鏈",
|
||||
"overrideTitle": "舊規範覆寫",
|
||||
"hardBlockerTitle": "仍維持硬阻擋",
|
||||
"hardBlockerDetail": "需 break-glass 或專案級合約,不由一般自動化靜默執行。",
|
||||
"badges": {
|
||||
"override": "舊 no-send / no-live 已覆寫",
|
||||
"gateway": "Telegram Gateway"
|
||||
},
|
||||
"metrics": {
|
||||
"completion": "目前完成度",
|
||||
"riskTiers": "自動風險層",
|
||||
"reports": "日週月報",
|
||||
"gateway": "Gateway 派送",
|
||||
"executor": "Executor 收據",
|
||||
"hardBlockers": "硬阻擋"
|
||||
},
|
||||
"policy": {
|
||||
"low": "低風險",
|
||||
"medium": "中風險",
|
||||
"high": "高風險",
|
||||
"noOwnerReview": "低/中/高人工 gate={value}",
|
||||
"verifier": "post-apply verifier={value}",
|
||||
"km": "KM / PlayBook 回寫={value}"
|
||||
},
|
||||
"runtime": {
|
||||
"checkMode": "Ansible check-mode worker",
|
||||
"apply": "Controlled apply",
|
||||
"botApi": "Direct Bot API",
|
||||
"gatewayOnly": "只允許既有 Telegram Gateway,不暴露 token / chat id。"
|
||||
}
|
||||
},
|
||||
"runway": {
|
||||
"readOnlyInvestigation": {
|
||||
"label": "主動巡檢與證據蒐集",
|
||||
@@ -9755,12 +9796,12 @@
|
||||
"verify_git_baseline_then_mark_adopted": "驗證 Git baseline 後標記採納",
|
||||
"operator_review_handoff_and_execute_manual_plan": "Operator review交接並執行人工方案",
|
||||
"run_verification_scan_then_record_result": "執行驗證掃描並記錄結果",
|
||||
"open_manual_investigation_with_failed_verification": "建立人工調查並附上失敗驗證",
|
||||
"open_manual_investigation_with_failed_verification": "建立 AI verifier / rollback 調查並附上失敗驗證",
|
||||
"verify_k8s_matches_git_baseline": "驗證 K8s與Git baseline 一致",
|
||||
"confirm_no_repeat_after_rollback": "確認回滾後不再重複",
|
||||
"monitor_for_recurrence": "監控是否復發",
|
||||
"retry_pr_lookup_then_review_drift": "重試 PR 查詢後 review drift",
|
||||
"manual_investigation_or_ansible_check_mode": "人工調查或Ansible check-mode",
|
||||
"manual_investigation_or_ansible_check_mode": "AI 調查或 Ansible check-mode",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"pr": {
|
||||
@@ -10669,11 +10710,15 @@
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"reviewOwnerReleasePacket": "審查 owner 放行包、執行窗口、rollback 與回寫責任",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件",
|
||||
"manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"collectRepairEvidence": "補齊修復證據或建立專屬 PlayBook",
|
||||
"manualInvestigation": "人工調查卡點並補證據",
|
||||
"reviewVerifier": "執行或審查修復後 verifier",
|
||||
"manualInvestigation": "AI 調查卡點並補證據",
|
||||
"autoGenerateRepairCandidate": "AI 從診斷證據產生修復候選",
|
||||
"autoVerifyOrRollback": "AI 執行 verifier 或 rollback 判定",
|
||||
"autoRepairBlockerConnector": "AI 修復 blocker / connector 後重試",
|
||||
"autoGenerateRepairOrBreakGlass": "AI 產生修復包;硬阻擋才 break-glass",
|
||||
"reviewVerifier": "執行修復後 verifier",
|
||||
"monitorRegression": "持續觀察是否復發",
|
||||
"collectEvidenceOrWait": "補收證據或等待下一筆 recurrence",
|
||||
"collectMaintenanceRollback": "補齊維護窗口與 rollback owner",
|
||||
@@ -11272,7 +11317,7 @@
|
||||
"nextActions": {
|
||||
"openApplyGateWorkItem": "開啟 apply gate 工作項,審查 Verifier 與 KM 回寫",
|
||||
"ownerReviewRepairCandidateDraft": "審查修復候選草案、rollback 與 verifier",
|
||||
"manualReviewNoActionDecision": "人工判斷是否接手或關閉事件"
|
||||
"manualReviewNoActionDecision": "AI 補齊 evidence / PlayBook 後判定是否關閉事件"
|
||||
},
|
||||
"items": {
|
||||
"km": "KM",
|
||||
@@ -20793,6 +20838,8 @@
|
||||
"title": "Owner export 進來後,先由 reviewer 驗收脫敏清單",
|
||||
"subtitle": "這張卡固定 Wazuh manager registry owner export 的驗收規則:欄位、計數、公開別名矩陣、Dashboard API 修復讀回、唯讀 credential metadata、拒收內容與下一個 Gate 都先可視化;目前尚未收到、尚未接受,也不開 runtime。",
|
||||
"loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API",
|
||||
"validationEndpointLabel": "脫敏 owner export 驗證端點",
|
||||
"validationModeLabel": "驗證模式",
|
||||
"slotReceivedLabel": "已收件",
|
||||
"slotAcceptedLabel": "已接受",
|
||||
"slotNextGateLabel": "下一關",
|
||||
|
||||
@@ -45,6 +45,7 @@ import { redactPublicIdentifier } from '@/lib/public-security-redaction'
|
||||
import {
|
||||
apiClient,
|
||||
type AiAgent12AgentWarRoomSnapshot,
|
||||
type AiAgentAutonomousRuntimeControlSnapshot,
|
||||
type AiAgentProfessionalTaskExpansionSnapshot,
|
||||
type AiAgentReceiptReadbackOwnerReviewSnapshot,
|
||||
type AiAgentReportNoWriteAnalysisRuntimeSnapshot,
|
||||
@@ -849,6 +850,7 @@ function GateMatrixRow({
|
||||
export function AutomationInventoryTab() {
|
||||
const t = useTranslations('governance.automationInventory')
|
||||
const [snapshot, setSnapshot] = useState<AiAgentAutomationInventorySnapshot | null>(null)
|
||||
const [autonomousRuntimeControl, setAutonomousRuntimeControl] = useState<AiAgentAutonomousRuntimeControlSnapshot | null>(null)
|
||||
const [backlog, setBacklog] = useState<AiAgentAutomationBacklogSnapshot | null>(null)
|
||||
const [backupTargets, setBackupTargets] = useState<BackupDrTargetInventorySnapshot | null>(null)
|
||||
const [backupReadiness, setBackupReadiness] = useState<BackupDrReadinessMatrixSnapshot | null>(null)
|
||||
@@ -948,6 +950,7 @@ export function AutomationInventoryTab() {
|
||||
setLoading(true)
|
||||
const requests = [
|
||||
apiClient.getAiAgentAutomationInventorySnapshot(),
|
||||
apiClient.getAiAgentAutonomousRuntimeControl(),
|
||||
apiClient.getAiAgentAutomationBacklogSnapshot(),
|
||||
apiClient.getBackupDrTargetInventory(),
|
||||
apiClient.getBackupDrReadinessMatrix(),
|
||||
@@ -1040,6 +1043,7 @@ export function AutomationInventoryTab() {
|
||||
.then((results) => {
|
||||
const [
|
||||
inventoryResult,
|
||||
autonomousRuntimeControlResult,
|
||||
backlogResult,
|
||||
targetResult,
|
||||
readinessResult,
|
||||
@@ -1129,6 +1133,7 @@ export function AutomationInventoryTab() {
|
||||
] = results
|
||||
|
||||
setSnapshot(settledPublicValue(inventoryResult))
|
||||
setAutonomousRuntimeControl(settledPublicValue(autonomousRuntimeControlResult))
|
||||
setBacklog(settledPublicValue(backlogResult))
|
||||
setBackupTargets(settledPublicValue(targetResult))
|
||||
setBackupReadiness(settledPublicValue(readinessResult))
|
||||
@@ -5503,6 +5508,12 @@ export function AutomationInventoryTab() {
|
||||
tone: hostRedactionLocked && professionalTaskRedactionLocked && warRoomRedactionLocked && serviceHealthRedactionLocked ? 'ok' as const : 'warn' as const,
|
||||
},
|
||||
]
|
||||
const currentAutonomyCadences = autonomousRuntimeControl?.report_delivery.cadences ?? []
|
||||
const currentAutonomyExecutorReceipts = autonomousRuntimeControl?.controlled_executor.operation_receipts ?? []
|
||||
const currentAutonomyOverrides = autonomousRuntimeControl?.legacy_policy_overrides ?? []
|
||||
const currentAutonomyHardBlockers = autonomousRuntimeControl?.hard_blockers ?? []
|
||||
const currentAutonomyPolicy = autonomousRuntimeControl?.current_policy
|
||||
const currentAutonomySwitches = autonomousRuntimeControl?.runtime_switches
|
||||
const globalControlRunwayRows: Array<{
|
||||
key: string
|
||||
label: string
|
||||
@@ -6311,6 +6322,161 @@ export function AutomationInventoryTab() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{autonomousRuntimeControl ? (
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 0 }}>
|
||||
<div style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 8,
|
||||
border: '0.5px solid #15803d40',
|
||||
background: 'rgba(21,128,61,0.08)',
|
||||
color: '#15803d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<BellRing size={18} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 18, fontWeight: 760, color: '#141413', lineHeight: 1.15, overflowWrap: 'anywhere' }}>
|
||||
{t('globalControl.currentAutonomy.title')}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, color: '#4f6156', lineHeight: 1.55, overflowWrap: 'anywhere' }}>
|
||||
{autonomousRuntimeControl.program_status.status_note}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={autonomousRuntimeControl.program_status.current_task_id} />
|
||||
<Chip value={t('globalControl.currentAutonomy.badges.override')} />
|
||||
<Chip value={t('globalControl.currentAutonomy.badges.gateway')} muted={autonomousRuntimeControl.rollups.telegram_gateway_delivery_enabled_count === 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(128px, 1fr))', gap: 10 }} className="automation-inventory-global-control-kpi-grid">
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.completion')} value={`${autonomousRuntimeControl.program_status.implementation_completion_percent}%`} tone="ok" icon={<Gauge size={16} />} />
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.riskTiers')} value={`${autonomousRuntimeControl.rollups.automated_risk_tier_count}/3`} tone={autonomousRuntimeControl.rollups.automated_risk_tier_count === 3 ? 'ok' : 'warn'} icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.reports')} value={autonomousRuntimeControl.rollups.report_cadence_enabled_count} tone="ok" icon={<CalendarClock size={16} />} />
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.gateway')} value={autonomousRuntimeControl.rollups.telegram_gateway_delivery_enabled_count} tone="ok" icon={<BellRing size={16} />} />
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.executor')} value={autonomousRuntimeControl.rollups.controlled_executor_operation_receipt_count} tone="ok" icon={<ClipboardCheck size={16} />} />
|
||||
<MetricCard label={t('globalControl.currentAutonomy.metrics.hardBlockers')} value={autonomousRuntimeControl.rollups.hard_blocker_count} tone="warn" icon={<ShieldAlert size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(260px, 0.8fr)', gap: 12 }} className="automation-inventory-global-control-grid">
|
||||
<div style={{ padding: 12, border: '0.5px solid #cfe7d7', borderRadius: 7, background: '#f8fffa', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
|
||||
<SmallLabel>{t('globalControl.currentAutonomy.policyTitle')}</SmallLabel>
|
||||
<Chip value={autonomousRuntimeControl.program_status.runtime_authority} muted />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 8 }} className="automation-inventory-global-control-pipeline-grid">
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.policy.low')}
|
||||
value={currentAutonomyPolicy?.low_risk_controlled_apply_allowed ? 'on' : 'off'}
|
||||
detail={t('globalControl.currentAutonomy.policy.noOwnerReview', { value: String(currentAutonomyPolicy?.owner_review_required_for_low_medium_high === false) })}
|
||||
tone={currentAutonomyPolicy?.low_risk_controlled_apply_allowed ? 'ok' : 'warn'}
|
||||
/>
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.policy.medium')}
|
||||
value={currentAutonomyPolicy?.medium_risk_controlled_apply_allowed ? 'on' : 'off'}
|
||||
detail={t('globalControl.currentAutonomy.policy.verifier', { value: String(currentAutonomyPolicy?.post_apply_verifier_required === true) })}
|
||||
tone={currentAutonomyPolicy?.medium_risk_controlled_apply_allowed ? 'ok' : 'warn'}
|
||||
/>
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.policy.high')}
|
||||
value={currentAutonomyPolicy?.high_risk_controlled_apply_allowed ? 'on' : 'off'}
|
||||
detail={t('globalControl.currentAutonomy.policy.km', { value: String(currentAutonomyPolicy?.km_learning_writeback_required === true) })}
|
||||
tone={currentAutonomyPolicy?.high_risk_controlled_apply_allowed ? 'ok' : 'warn'}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 8 }}>
|
||||
{currentAutonomyCadences.map(cadence => (
|
||||
<div key={cadence.cadence} style={{ padding: 10, border: '0.5px solid #d8e8df', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'flex-start' }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 760, color: '#141413', lineHeight: 1.2, overflowWrap: 'anywhere' }}>
|
||||
{cadence.display_name}
|
||||
</span>
|
||||
<Chip value={cadence.telegram_gateway_delivery_enabled ? 'Gateway on' : 'Gateway off'} muted={!cadence.telegram_gateway_delivery_enabled} />
|
||||
</div>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#50665a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{cadence.schedule}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#50665a', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{cadence.worker}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e5dbc7', borderRadius: 7, background: '#fffdf7', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('globalControl.currentAutonomy.runtimeTitle')}</SmallLabel>
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.runtime.checkMode')}
|
||||
value={currentAutonomySwitches?.ansible_check_mode_worker_enabled ? 'on' : 'off'}
|
||||
detail={`interval=${currentAutonomySwitches?.ansible_check_mode_interval_seconds ?? '--'}s batch=${currentAutonomySwitches?.ansible_check_mode_batch_limit ?? '--'}`}
|
||||
tone={currentAutonomySwitches?.ansible_check_mode_worker_enabled ? 'ok' : 'warn'}
|
||||
/>
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.runtime.apply')}
|
||||
value={currentAutonomySwitches?.ansible_controlled_apply_enabled ? 'on' : 'off'}
|
||||
detail={(currentAutonomySwitches?.ansible_controlled_apply_allowed_risk_levels ?? []).join(', ') || '--'}
|
||||
tone={currentAutonomySwitches?.ansible_controlled_apply_enabled ? 'ok' : 'warn'}
|
||||
/>
|
||||
<GateMatrixRow
|
||||
label={t('globalControl.currentAutonomy.runtime.botApi')}
|
||||
value={currentAutonomyPolicy?.direct_bot_api_allowed ? 'on' : 'off'}
|
||||
detail={t('globalControl.currentAutonomy.runtime.gatewayOnly')}
|
||||
tone={currentAutonomyPolicy?.direct_bot_api_allowed ? 'danger' : 'ok'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-global-control-runway-grid">
|
||||
<div style={{ padding: 12, border: '0.5px solid #d7d2f4', borderRadius: 7, background: '#fbfaff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('globalControl.currentAutonomy.executorTitle')}</SmallLabel>
|
||||
{currentAutonomyExecutorReceipts.slice(0, 5).map(receipt => (
|
||||
<GateMatrixRow
|
||||
key={receipt.operation_type}
|
||||
label={receipt.operation_type}
|
||||
value={receipt.writes_runtime_state ? 'write' : 'dry'}
|
||||
detail={`${receipt.owner_agent}: ${receipt.purpose}`}
|
||||
tone={receipt.writes_runtime_state ? 'warn' : 'ok'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: 12, border: '0.5px solid #d7e2ea', borderRadius: 7, background: '#f9fcff', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('globalControl.currentAutonomy.overrideTitle')}</SmallLabel>
|
||||
{currentAutonomyOverrides.slice(0, 4).map(row => (
|
||||
<GateMatrixRow
|
||||
key={row.legacy_area}
|
||||
label={row.legacy_area}
|
||||
value={row.current_effect}
|
||||
detail={row.new_behavior}
|
||||
tone="ok"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: 12, border: '0.5px solid #f1d4d4', borderRadius: 7, background: '#fffafa', display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<SmallLabel>{t('globalControl.currentAutonomy.hardBlockerTitle')}</SmallLabel>
|
||||
{currentAutonomyHardBlockers.slice(0, 5).map(blocker => (
|
||||
<GateMatrixRow
|
||||
key={blocker}
|
||||
label={blocker}
|
||||
value="blocked"
|
||||
detail={t('globalControl.currentAutonomy.hardBlockerDetail')}
|
||||
tone="warn"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
) : null}
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -2474,6 +2474,7 @@ const wazuhManagedHostCoverageBoundaries = [
|
||||
|
||||
const wazuhManagerRegistryReviewerValidationBoundaries = [
|
||||
'wazuh_manager_registry_reviewer_validation_visible=true',
|
||||
'wazuh_manager_registry_owner_export_validation_api_available=true',
|
||||
'wazuh_manager_registry_reviewer_validation_expected_scope_alias_count=6',
|
||||
'wazuh_manager_registry_reviewer_validation_required_owner_field_count=28',
|
||||
'wazuh_manager_registry_reviewer_validation_per_host_required_field_count=9',
|
||||
@@ -9845,6 +9846,9 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
|
||||
: loading
|
||||
? [t('loadingBoundary')]
|
||||
: wazuhManagerRegistryReviewerValidationBoundaries
|
||||
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 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')
|
||||
@@ -9870,6 +9874,10 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
|
||||
<ToneDot tone={statusTone} />
|
||||
{statusText}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(126px, 1fr))', gap: 8 }}>
|
||||
|
||||
@@ -341,6 +341,8 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse {
|
||||
status: string
|
||||
mode: string
|
||||
source_refs: string[]
|
||||
owner_export_validation_endpoint: string
|
||||
owner_export_validation_mode: string
|
||||
summary: {
|
||||
expected_scope_alias_count: number
|
||||
required_owner_field_count: number
|
||||
@@ -843,6 +845,11 @@ export const apiClient = {
|
||||
return handleResponse<AiAgentAutomationInventorySnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgentAutonomousRuntimeControl() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/agent-autonomous-runtime-control`)
|
||||
return handleResponse<AiAgentAutonomousRuntimeControlSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiAgentAutomationBacklogSnapshot() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`)
|
||||
return handleResponse<AiAgentAutomationBacklogSnapshot>(res)
|
||||
@@ -1848,6 +1855,91 @@ export interface AiTechnologyReportCadenceReadback {
|
||||
// AI Agent Automation Inventory Snapshot
|
||||
// =========================================================================
|
||||
|
||||
export interface AiAgentAutonomousRuntimeControlSnapshot {
|
||||
schema_version: 'ai_agent_autonomous_runtime_control_v1'
|
||||
generated_at: string
|
||||
program_status: {
|
||||
current_task_id: 'P2-416-D1N'
|
||||
status: string
|
||||
runtime_authority: 'current_owner_directive_controlled_ai_automation'
|
||||
legacy_no_send_no_live_rules_overridden: true
|
||||
implementation_completion_percent: number
|
||||
status_note: string
|
||||
}
|
||||
current_policy: {
|
||||
low_risk_controlled_apply_allowed: boolean
|
||||
medium_risk_controlled_apply_allowed: boolean
|
||||
high_risk_controlled_apply_allowed: boolean
|
||||
critical_break_glass_required: boolean
|
||||
owner_review_required_for_low_medium_high: boolean
|
||||
direct_bot_api_allowed: boolean
|
||||
telegram_gateway_required: boolean
|
||||
post_apply_verifier_required: boolean
|
||||
km_learning_writeback_required: boolean
|
||||
}
|
||||
runtime_switches: {
|
||||
ansible_check_mode_worker_enabled: boolean
|
||||
ansible_controlled_apply_enabled: boolean
|
||||
ansible_controlled_apply_allowed_risk_levels: string[]
|
||||
ansible_check_mode_interval_seconds: number
|
||||
ansible_check_mode_batch_limit: number
|
||||
ansible_check_mode_timeout_seconds: number
|
||||
ansible_controlled_apply_timeout_seconds: number
|
||||
}
|
||||
agent_roles: Array<{
|
||||
agent_id: string
|
||||
role: string
|
||||
current_job: string
|
||||
}>
|
||||
report_delivery: {
|
||||
status: string
|
||||
cadences: Array<{
|
||||
cadence: 'daily' | 'weekly' | 'monthly'
|
||||
display_name: string
|
||||
schedule: string
|
||||
worker: string
|
||||
telegram_gateway_delivery_enabled: boolean
|
||||
direct_bot_api_allowed: boolean
|
||||
receipt_source: string
|
||||
}>
|
||||
}
|
||||
controlled_executor: {
|
||||
status: string
|
||||
operation_receipts: Array<{
|
||||
operation_type: string
|
||||
owner_agent: string
|
||||
purpose: string
|
||||
writes_runtime_state: boolean
|
||||
}>
|
||||
required_flow: string[]
|
||||
}
|
||||
legacy_policy_overrides: Array<{
|
||||
legacy_area: string
|
||||
current_effect: string
|
||||
new_behavior: string
|
||||
}>
|
||||
hard_blockers: string[]
|
||||
visibility_contract: {
|
||||
frontend_displays_runtime_truth: boolean
|
||||
work_window_transcript_display_allowed: boolean
|
||||
raw_prompt_display_allowed: boolean
|
||||
private_reasoning_display_allowed: boolean
|
||||
secret_value_display_allowed: boolean
|
||||
raw_telegram_payload_display_allowed: boolean
|
||||
lan_topology_redaction_required: boolean
|
||||
}
|
||||
rollups: {
|
||||
automated_risk_tier_count: number
|
||||
hard_blocker_count: number
|
||||
report_cadence_enabled_count: number
|
||||
telegram_gateway_delivery_enabled_count: number
|
||||
direct_bot_api_allowed_count: number
|
||||
controlled_executor_operation_receipt_count: number
|
||||
runtime_write_receipt_type_count: number
|
||||
legacy_policy_overridden_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AiAgentAutomationInventorySnapshot {
|
||||
schema_version: 'ai_agent_automation_inventory_snapshot_v1'
|
||||
generated_at: string
|
||||
|
||||
@@ -1,3 +1,66 @@
|
||||
## 2026-06-27|P2-416 D1N:目前有效 AI Agent 自主化控制層與日週月報 Telegram Gateway 接線
|
||||
|
||||
**背景**:使用者已明確要求不再依舊 no-send / no-live / 高風險預設人工規範推進;目前有效方向是 low / medium / high 風險在 allowlist、Ansible check-mode、controlled apply、post-apply verifier、KM / PlayBook writeback 與 Telegram receipt 下由 AI Agent 受控自動處理。critical / secret / destructive / reboot / node drain / provider switch / force push 等仍維持 hard blocker。
|
||||
|
||||
**完成內容**:
|
||||
- 新增目前有效控制 API:`GET /api/v1/agents/agent-autonomous-runtime-control`,schema `ai_agent_autonomous_runtime_control_v1`。
|
||||
- 新 API 明確回傳:舊 no-send / no-live rules 已被 current owner directive 覆寫、low / medium / high 風險不再要求人工 gate、Telegram 只能走既有 Gateway、不直呼 Bot API、不暴露 token / chat id / raw payload。
|
||||
- `report_generation_service` 新增週報 loop 與月報 loop:日報每日 `08:00`、週報週五 `10:00`、月報每月 `1` 日 `09:00`,全部以台北時間計算並透過 Telegram Gateway 派送。
|
||||
- 月報文案從 `no-send preview` 改為正式月報派送語意,仍保留資料源健康、缺口來源、KM / PlayBook / Verifier 沉澱與 hard blocker 說明。
|
||||
- `main.py` 已在 lifespan 同時掛入日報 / 週報 / 月報三條背景 loop。
|
||||
- `/zh-TW/governance?tab=automation-inventory` 新增「目前有效自主化控制層」卡:顯示目前完成度、低中高風險自動化、日週月報 Gateway、Ansible executor 收據鏈、舊規範覆寫與 hard blockers。
|
||||
- 前端仍不顯示工作視窗對話、raw prompt、private reasoning、secret、raw Telegram payload、內網拓樸或 Bot token。
|
||||
|
||||
**本地驗證結果**:
|
||||
- `python3 -m py_compile apps/api/src/services/report_generation_service.py apps/api/src/services/ai_agent_autonomous_runtime_control.py apps/api/src/api/v1/agents.py apps/api/src/main.py`:通過。
|
||||
- `DATABASE_URL=sqlite:///test.db python3.11 -m pytest apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py apps/api/tests/test_report_generation_service.py apps/api/tests/test_weekly_report_preview_api.py -q`:`51 passed`。
|
||||
- `pnpm --dir apps/web typecheck`:通過。
|
||||
|
||||
**目前完成度 / 邊界**:
|
||||
- P2-416 D1N 本地:`0% -> 88%`。已完成程式、API、排程接線、前端顯示與測試;尚待 commit、Gitea push、CD、production API readback 與 desktop / mobile browser smoke。
|
||||
- AI Agent 自動化整體保守:`72% -> 78%`。D1N 把目前有效控制面與報告 Telegram Gateway 連上,但真正 runtime 成效仍要看下一批 `ansible_apply_executed`、`incident_evidence.post_execution_state`、`knowledge_entries` 與 Telegram delivery receipt 的 production readback。
|
||||
|
||||
**仍維持 hard blocker**:
|
||||
- secret / token / private key / cookie / session / auth header 明文讀取或外洩。
|
||||
- `DROP` / `TRUNCATE` / destructive migration / restore / prune / irreversible DB operation。
|
||||
- reboot / node drain / irreversible firewall / host lockout。
|
||||
- credentialed exploit / external active scan。
|
||||
- 新付費 provider、成本上限調整、provider switch 或 OpenClaw 替換未經 replay / shadow / canary。
|
||||
- force push、刪 repo / refs、visibility change。
|
||||
- critical / break-glass route 未具備專案級 break-glass contract。
|
||||
|
||||
**下一步**:
|
||||
- Commit / push 到 `gitea main`,等待 code-review / CD。
|
||||
- 正式站讀回 `/api/v1/agents/agent-autonomous-runtime-control` 與 `/zh-TW/governance?tab=automation-inventory`,確認新卡可見且 forbidden terms 為 `0`。
|
||||
|
||||
## 2026-06-27|AI Agent 受控自動化規範覆寫與告警語意收斂
|
||||
|
||||
**背景**:統帥明確要求把 2026-06-26 以前大量停在 owner gate / read-only / manual handoff 的舊規範,改成 AI Agent 對 low / medium / high 風險事件預設走受控自動化。高風險不等於任意命令直跑,而是必須有 allowlist、PlayBook / Ansible / MCP route、check-mode / dry-run、blast radius、rollback、post-apply verifier、KM / PlayBook trust 與 Telegram / AwoooP receipt。
|
||||
|
||||
**完成內容**:
|
||||
- `docs/HARD_RULES.md` 升版到 `v2.5`,新增「舊 owner gate / read-only 預設失效條款」;`manual_required`、`needs_human=true`、`runtime_write_gate=0`、`owner_review_required` 等舊語意,除硬阻擋外不得再作為 low / medium / high 事件終局。
|
||||
- `docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md` 新增 `§1.6 2026-06-27 舊 owner gate / read-only 規範失效宣告`,並把狀態機改成 `controlled_policy_check`、`controlled_playbook_queue`、`ai_repair_candidate_required`、`ai_rollback_or_repair`、`break_glass_required`。
|
||||
- `docs/workplans/2026-06-04-iwooos-security-governance-p0.md` 加入 2026-06-27 覆寫說明,明確區分 6/04 只讀 ledger 與目前 AI Agent controlled automation 主線。
|
||||
- 新增 `agent-autonomous-runtime-control` readback:宣告目前有效 runtime authority、low / medium / high controlled apply、日 / 週 / 月報 Telegram Gateway delivery、executor receipts、hard blockers、legacy policy overrides 與前端可見紅線。
|
||||
- 日報 / 週報 / 月報排程對齊:API lifespan 啟動 daily / weekly / monthly report loops;月報從 no-send preview 改成 Telegram Gateway delivery 語意;報表仍不直接讀 Bot token 或 chat id。
|
||||
- `operator_outcome` 新增 legacy normalizer:舊 `diagnostic_only_manual_review`、`verification_degraded_manual_required`、`execution_unverified_manual_required`、`no_action_manual_review`、`approval_expired_manual_review`、`write_observed_manual_review`、`blocked_manual_required` 在未命中硬阻擋時轉為 AI controlled path。
|
||||
- Telegram / AwoooP status-chain / Approval execution / Alerts UI / status-chain UI 文案同步:診斷-only、no-action、驗證退化、寫入旗標、blocked 不再顯示為人工接手,而是 AI 補 PlayBook / transport / verifier、AI rollback、AI connector 修復與 controlled apply 判定。
|
||||
|
||||
**本地驗證**:
|
||||
- `pytest apps/api/tests/test_operator_outcome.py apps/api/tests/test_telegram_ai_automation_block.py apps/api/tests/test_telegram_webhook_execution_handoff.py apps/api/tests/test_telegram_message_templates.py apps/api/tests/test_awooop_operator_timeline_labels.py -q`:`170 passed`。
|
||||
- `pytest apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py apps/api/tests/test_report_generation_service.py apps/api/tests/test_weekly_report_preview_api.py -q`:`51 passed`。
|
||||
- `python3 -m py_compile`:`operator_outcome.py`、`telegram_gateway.py`、`platform_operator_service.py`、`approval_execution.py`、`report_generation_service.py`、`ai_agent_autonomous_runtime_control.py`、`agents.py`、`main.py` 通過。
|
||||
- `python3 -m json.tool apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:通過。
|
||||
- `pnpm --filter @awoooi/web typecheck`:通過。
|
||||
- `git diff --check`:通過。
|
||||
|
||||
**完成度與邊界**:
|
||||
- 規範基線改寫:`100%`。
|
||||
- API / Telegram / AwoooP outcome 語意收斂:`100%`。
|
||||
- 報表 delivery 語意與 autonomous runtime control readback:`100%` 本地完成;正式站需等 CD deploy marker 後驗證。
|
||||
- AI Agent 全自動化產品化:本段把舊規範阻擋改成當前正確方向,但不宣稱所有 PlayBook / Ansible / verifier / KM worker 都已完整覆蓋所有主機、服務、網站、產品;下一步仍要接真實告警驗證 controlled apply、post-apply verifier、rollback、KM / PlayBook trust 寫回。
|
||||
- 硬阻擋仍維持:不讀 secret / token / private key / cookie / authorization header 明文、不做 DB DROP / TRUNCATE / restore / prune、不 reboot / node drain、不做 credentialed exploit / 外部攻擊型 active scan、不新增或切換付費 provider / 成本上限、不 force push / 刪 repo refs / 改 visibility、不碰 raw runtime secret volume。
|
||||
|
||||
## 2026-06-27|IwoooS Wazuh manager registry reviewer validation 正式讀回完成
|
||||
|
||||
**時間與來源**:
|
||||
|
||||
@@ -29573,6 +29573,7 @@ def validate(root: Path) -> None:
|
||||
"getIwoooSWazuhManagerRegistryReviewerValidation",
|
||||
"apiClient.getIwoooSWazuhManagerRegistryReviewerValidation",
|
||||
"Wazuh manager registry reviewer validation 已讀回",
|
||||
"wazuh_manager_registry_owner_export_validation_api_available=true",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
|
||||
@@ -29594,7 +29595,13 @@ def validate(root: Path) -> None:
|
||||
"/api/v1/iwooos/wazuh-manager-registry-reviewer-validation",
|
||||
"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",
|
||||
"iwooos_wazuh_manager_registry_owner_export_validation_result_v1",
|
||||
"validate_iwooos_wazuh_manager_registry_owner_export",
|
||||
"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",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=0",
|
||||
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
|
||||
|
||||
Reference in New Issue
Block a user