diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 281047035..c2170fddc 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -15024,6 +15024,58 @@ } } }, + "publicGatewayPreflight": { + "eyebrow": "Public Gateway Preflight", + "title": "Nginx 入口變更前置 Gate 已成只讀表", + "subtitle": "這張卡把 public gateway reload 或 route change 前必備的 owner、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback 欄位固定下來;目前只顯示 repo-only 證據,不讀 live 主機、不 reload、不改 DNS 或憑證。", + "stateLabel": "狀態", + "boundaryTitle": "Public gateway preflight 邊界", + "boundaryIntro": "以下鍵值固定:preflight 可見不代表 Nginx reload、live conf 讀取、route change、nginx -t、route smoke、certbot renew 或主機寫入已授權。", + "summary": { + "sourceConfigs": { + "label": "Source config", + "detail": "三份 Nginx source-of-truth 進入 preflight。" + }, + "routeImpacts": { + "label": "Route impact", + "detail": "14 個 domain / route 影響面需後續 owner review。" + }, + "preflightGates": { + "label": "前置 Gate", + "detail": "12 個 reload 前置 Gate 已固定。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "仍為 0,不產生 reload 或 route change 入口。" + } + }, + "items": { + "sourceHash": { + "title": "Source hash 可重跑", + "body": "repo raw / normalized hash 已可作為後續 drift 比對基準,但尚未代表 live 一致。" + }, + "affectedRoutes": { + "title": "影響 route 已列清", + "body": "route、upstream、TLS、ACME、admin 與 WebSocket 影響面已整理成 preflight 清單。" + }, + "ownerLiveDiff": { + "title": "Owner / live / diff 仍為 0", + "body": "owner response、owner-provided live conf 與 rendered diff 尚未收到或接受。" + }, + "nginxTest": { + "title": "nginx -t 尚未執行", + "body": "語法測試需要維護窗口與負責人證據;目前沒有執行,也不能用 UI 取代。" + }, + "routeSmoke": { + "title": "Route smoke 尚未驗收", + "body": "public、admin、WebSocket / API smoke 仍全部為 0,只能等待後續 owner 提供證據。" + }, + "reloadBoundary": { + "title": "Reload 仍鎖住", + "body": "不 SSH、不讀 live conf、不 reload Nginx、不改 DNS / TLS / ACME,也不新增操作按鈕。" + } + } + }, "domainTlsCertbotInventory": { "eyebrow": "DNS / TLS / certbot 清冊", "title": "公開入口憑證與 ACME 關係已納入只讀控管", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 281047035..c2170fddc 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -15024,6 +15024,58 @@ } } }, + "publicGatewayPreflight": { + "eyebrow": "Public Gateway Preflight", + "title": "Nginx 入口變更前置 Gate 已成只讀表", + "subtitle": "這張卡把 public gateway reload 或 route change 前必備的 owner、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback 欄位固定下來;目前只顯示 repo-only 證據,不讀 live 主機、不 reload、不改 DNS 或憑證。", + "stateLabel": "狀態", + "boundaryTitle": "Public gateway preflight 邊界", + "boundaryIntro": "以下鍵值固定:preflight 可見不代表 Nginx reload、live conf 讀取、route change、nginx -t、route smoke、certbot renew 或主機寫入已授權。", + "summary": { + "sourceConfigs": { + "label": "Source config", + "detail": "三份 Nginx source-of-truth 進入 preflight。" + }, + "routeImpacts": { + "label": "Route impact", + "detail": "14 個 domain / route 影響面需後續 owner review。" + }, + "preflightGates": { + "label": "前置 Gate", + "detail": "12 個 reload 前置 Gate 已固定。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "仍為 0,不產生 reload 或 route change 入口。" + } + }, + "items": { + "sourceHash": { + "title": "Source hash 可重跑", + "body": "repo raw / normalized hash 已可作為後續 drift 比對基準,但尚未代表 live 一致。" + }, + "affectedRoutes": { + "title": "影響 route 已列清", + "body": "route、upstream、TLS、ACME、admin 與 WebSocket 影響面已整理成 preflight 清單。" + }, + "ownerLiveDiff": { + "title": "Owner / live / diff 仍為 0", + "body": "owner response、owner-provided live conf 與 rendered diff 尚未收到或接受。" + }, + "nginxTest": { + "title": "nginx -t 尚未執行", + "body": "語法測試需要維護窗口與負責人證據;目前沒有執行,也不能用 UI 取代。" + }, + "routeSmoke": { + "title": "Route smoke 尚未驗收", + "body": "public、admin、WebSocket / API smoke 仍全部為 0,只能等待後續 owner 提供證據。" + }, + "reloadBoundary": { + "title": "Reload 仍鎖住", + "body": "不 SSH、不讀 live conf、不 reload Nginx、不改 DNS / TLS / ACME,也不新增操作按鈕。" + } + } + }, "domainTlsCertbotInventory": { "eyebrow": "DNS / TLS / certbot 清冊", "title": "公開入口憑證與 ACME 關係已納入只讀控管", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 2a253d46e..97ae3230b 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -287,6 +287,14 @@ type DomainTlsCertbotInventoryItem = { tone: 'steady' | 'warn' | 'locked' } +type PublicGatewayPreflightItem = { + key: string + gate: string + state: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type IwoooSCommandMapItem = { key: IwoooSCommandMapMode metric: string @@ -2089,12 +2097,17 @@ const highValueConfigControlCoverageBoundaries = [ 'high_value_config_control_coverage_c0_category_count=8', 'high_value_config_control_coverage_c1_category_count=4', 'high_value_config_control_coverage_average_percent=66', - 'high_value_config_control_coverage_needs_live_evidence_count=6', + 'high_value_config_control_coverage_needs_live_evidence_count=7', 'high_value_config_control_coverage_owner_response_required_count=14', 'high_value_config_control_coverage_owner_response_received_count=0', 'high_value_config_control_coverage_owner_response_accepted_count=0', 'high_value_config_control_coverage_runtime_gate_count=0', 'high_value_config_control_coverage_action_button_count=0', + 'nginx_public_gateway_coverage_percent=84', + 'public_gateway_preflight_inventory_source_config_count=3', + 'public_gateway_preflight_inventory_route_impact_count=14', + 'public_gateway_preflight_inventory_preflight_gate_count=12', + 'public_gateway_preflight_inventory_runtime_gate_count=0', 'host_service_config_inventory_surface_count=9', 'host_service_config_inventory_write_capable_surface_count=3', 'host_service_config_inventory_runtime_gate_count=0', @@ -2131,6 +2144,15 @@ const highValueConfigControlCoverageBoundaries = [ 'host_keyscan_authorized=false', 'runtime_execution_authorized=false', 'host_write_authorized=false', + 'host_live_conf_read_authorized=false', + 'nginx_test_authorized=false', + 'public_gateway_reload_authorized=false', + 'public_route_change_authorized=false', + 'admin_route_change_authorized=false', + 'websocket_route_change_authorized=false', + 'acme_challenge_change_authorized=false', + 'route_smoke_authorized=false', + 'rollback_executed=false', 'nginx_reload_authorized=false', 'dns_tls_change_authorized=false', 'certbot_renew_authorized=false', @@ -2236,6 +2258,70 @@ const domainTlsCertbotInventoryBoundaries = [ 'secret_value_collection_allowed=false', ] as const +const publicGatewayPreflightSummary = [ + { key: 'sourceConfigs', value: '3', icon: Server, tone: 'steady' }, + { key: 'routeImpacts', value: '14', icon: Route, tone: 'warn' }, + { key: 'preflightGates', value: '12', icon: ListChecks, tone: 'warn' }, + { key: 'runtimeGate', value: '0', icon: Lock, tone: 'locked' }, +] as const + +const publicGatewayPreflightItems: PublicGatewayPreflightItem[] = [ + { key: 'sourceHash', gate: 'PG1', state: 'repo-only ready', icon: FileText, tone: 'steady' }, + { key: 'affectedRoutes', gate: 'PG2', state: '14 route', icon: Route, tone: 'warn' }, + { key: 'ownerLiveDiff', gate: 'PG3', state: '0 accepted', icon: SearchCheck, tone: 'warn' }, + { key: 'nginxTest', gate: 'PG4', state: '0 evidence', icon: ShieldCheck, tone: 'warn' }, + { key: 'routeSmoke', gate: 'PG5', state: '0 evidence', icon: Radar, tone: 'warn' }, + { key: 'reloadBoundary', gate: 'PG6', state: '未開閘', icon: Lock, tone: 'locked' }, +] + +const publicGatewayPreflightBoundaries = [ + 'public_gateway_preflight_frontstage_summary_count=4', + 'public_gateway_preflight_frontstage_item_count=6', + 'public_gateway_preflight_source_config_count=3', + 'public_gateway_preflight_c0_source_config_count=2', + 'public_gateway_preflight_managed_domain_count=14', + 'public_gateway_preflight_route_impact_count=14', + 'public_gateway_preflight_unique_upstream_count=14', + 'public_gateway_preflight_tls_certificate_path_count=10', + 'public_gateway_preflight_certificate_owner_confirmation_required_count=4', + 'public_gateway_preflight_admin_route_domain_count=1', + 'public_gateway_preflight_websocket_route_domain_count=6', + 'public_gateway_preflight_acme_challenge_domain_count=7', + 'public_gateway_preflight_gate_count=12', + 'public_gateway_preflight_repo_only_ready_count=2', + 'public_gateway_preflight_owner_acceptance_required_gate_count=10', + 'public_gateway_preflight_gate_accepted_count=0', + 'public_gateway_preflight_owner_response_received_count=0', + 'public_gateway_preflight_owner_response_accepted_count=0', + 'public_gateway_preflight_owner_provided_live_conf_received_count=0', + 'public_gateway_preflight_rendered_diff_ready_count=0', + 'public_gateway_preflight_nginx_test_evidence_count=0', + 'public_gateway_preflight_route_smoke_evidence_count=0', + 'public_gateway_preflight_maintenance_window_accepted_count=0', + 'public_gateway_preflight_rollback_owner_accepted_count=0', + 'public_gateway_preflight_runtime_gate_count=0', + 'public_gateway_preflight_action_button_count=0', + 'public_gateway_preflight_coverage_percent_before_preflight=78', + 'public_gateway_preflight_coverage_percent_after_preflight=84', + 'runtime_execution_authorized=false', + 'host_live_conf_read_authorized=false', + 'ssh_read_authorized=false', + 'ssh_write_authorized=false', + 'host_write_authorized=false', + 'nginx_test_authorized=false', + 'nginx_reload_authorized=false', + 'public_gateway_reload_authorized=false', + 'public_route_change_authorized=false', + 'admin_route_change_authorized=false', + 'websocket_route_change_authorized=false', + 'acme_challenge_change_authorized=false', + 'route_smoke_authorized=false', + 'rollback_executed=false', + 'secret_value_collection_allowed=false', + 'action_buttons_allowed=false', + 'not_authorization=true', +] as const + const iwooosThreeAxisProductProgressSummary = [ { key: 'headline', value: '64%', icon: Activity, tone: 'warn' }, { key: 'framework', value: '92%', icon: ShieldCheck, tone: 'steady' }, @@ -6904,6 +6990,134 @@ function IwoooSHighValueConfigControlCoverageBoard() { ) } +function IwoooSPublicGatewayPreflightBoard() { + const t = useTranslations('iwooos.publicGatewayPreflight') + const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + + return ( +
+
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+

+ {t('subtitle')} +

+
+ +
+ {publicGatewayPreflightSummary.map(item => { + const Icon = item.icon + return ( +
+
+ {t(`summary.${item.key}.label` as never)} + +
+
+ {item.value} +
+

+ {t(`summary.${item.key}.detail` as never)} +

+
+ ) + })} +
+
+ +
+ {publicGatewayPreflightItems.map(item => { + const Icon = item.icon + return ( +
+
+ + {item.gate} + + +
+
+
+ {t(`items.${item.key}.title` as never)} +
+
+ {t('stateLabel')}:{item.state} +
+
+

+ {t(`items.${item.key}.body` as never)} +

+
+ ) + })} +
+ +
+ + {t('boundaryTitle')} + +

+ {t('boundaryIntro')} +

+
+ {publicGatewayPreflightBoundaries.map(item => ( + + {item} + + ))} +
+
+
+
+ ) +} + function IwoooSDomainTlsCertbotInventoryBoard() { const t = useTranslations('iwooos.domainTlsCertbotInventory') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } @@ -18916,6 +19130,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/docs/schemas/iwooos_posture_projection_v1.schema.json b/docs/schemas/iwooos_posture_projection_v1.schema.json index faee2cdfa..656c258e1 100644 --- a/docs/schemas/iwooos_posture_projection_v1.schema.json +++ b/docs/schemas/iwooos_posture_projection_v1.schema.json @@ -286,6 +286,35 @@ "high_value_config_owner_packet_received_response_count", "high_value_config_owner_packet_accepted_response_count", "high_value_config_owner_packet_runtime_gate_count", + "public_gateway_preflight_first_layer", + "public_gateway_preflight_summary_count", + "public_gateway_preflight_item_count", + "public_gateway_preflight_source_config_count", + "public_gateway_preflight_c0_source_config_count", + "public_gateway_preflight_managed_domain_count", + "public_gateway_preflight_route_impact_count", + "public_gateway_preflight_unique_upstream_count", + "public_gateway_preflight_tls_certificate_path_count", + "public_gateway_preflight_certificate_owner_confirmation_required_count", + "public_gateway_preflight_admin_route_domain_count", + "public_gateway_preflight_websocket_route_domain_count", + "public_gateway_preflight_acme_challenge_domain_count", + "public_gateway_preflight_gate_count", + "public_gateway_preflight_repo_only_ready_count", + "public_gateway_preflight_owner_acceptance_required_gate_count", + "public_gateway_preflight_gate_accepted_count", + "public_gateway_preflight_owner_response_received_count", + "public_gateway_preflight_owner_response_accepted_count", + "public_gateway_preflight_owner_provided_live_conf_received_count", + "public_gateway_preflight_rendered_diff_ready_count", + "public_gateway_preflight_nginx_test_evidence_count", + "public_gateway_preflight_route_smoke_evidence_count", + "public_gateway_preflight_maintenance_window_accepted_count", + "public_gateway_preflight_rollback_owner_accepted_count", + "public_gateway_preflight_runtime_gate_count", + "public_gateway_preflight_action_button_count", + "public_gateway_preflight_coverage_percent_before_preflight", + "public_gateway_preflight_coverage_percent_after_preflight", "domain_tls_certbot_inventory_first_layer", "domain_tls_certbot_inventory_summary_count", "domain_tls_certbot_inventory_item_count", @@ -598,7 +627,7 @@ }, "high_value_config_control_coverage_needs_live_evidence_count": { "type": "integer", - "const": 6 + "const": 7 }, "high_value_config_control_coverage_owner_response_required_count": { "type": "integer", @@ -1256,6 +1285,122 @@ "type": "integer", "minimum": 0, "maximum": 100 + }, + "public_gateway_preflight_first_layer": { + "type": "boolean", + "const": true + }, + "public_gateway_preflight_summary_count": { + "type": "integer", + "const": 4 + }, + "public_gateway_preflight_item_count": { + "type": "integer", + "const": 6 + }, + "public_gateway_preflight_source_config_count": { + "type": "integer", + "const": 3 + }, + "public_gateway_preflight_c0_source_config_count": { + "type": "integer", + "const": 2 + }, + "public_gateway_preflight_managed_domain_count": { + "type": "integer", + "const": 14 + }, + "public_gateway_preflight_route_impact_count": { + "type": "integer", + "const": 14 + }, + "public_gateway_preflight_unique_upstream_count": { + "type": "integer", + "const": 14 + }, + "public_gateway_preflight_tls_certificate_path_count": { + "type": "integer", + "const": 10 + }, + "public_gateway_preflight_certificate_owner_confirmation_required_count": { + "type": "integer", + "const": 4 + }, + "public_gateway_preflight_admin_route_domain_count": { + "type": "integer", + "const": 1 + }, + "public_gateway_preflight_websocket_route_domain_count": { + "type": "integer", + "const": 6 + }, + "public_gateway_preflight_acme_challenge_domain_count": { + "type": "integer", + "const": 7 + }, + "public_gateway_preflight_gate_count": { + "type": "integer", + "const": 12 + }, + "public_gateway_preflight_repo_only_ready_count": { + "type": "integer", + "const": 2 + }, + "public_gateway_preflight_owner_acceptance_required_gate_count": { + "type": "integer", + "const": 10 + }, + "public_gateway_preflight_gate_accepted_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_owner_response_received_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_owner_response_accepted_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_owner_provided_live_conf_received_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_rendered_diff_ready_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_nginx_test_evidence_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_route_smoke_evidence_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_maintenance_window_accepted_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_rollback_owner_accepted_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_runtime_gate_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_action_button_count": { + "type": "integer", + "const": 0 + }, + "public_gateway_preflight_coverage_percent_before_preflight": { + "type": "integer", + "const": 78 + }, + "public_gateway_preflight_coverage_percent_after_preflight": { + "type": "integer", + "const": 84 } }, "additionalProperties": false diff --git a/docs/schemas/public_gateway_preflight_inventory_v1.schema.json b/docs/schemas/public_gateway_preflight_inventory_v1.schema.json new file mode 100644 index 000000000..5a0b23136 --- /dev/null +++ b/docs/schemas/public_gateway_preflight_inventory_v1.schema.json @@ -0,0 +1,477 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://awoooi.wooo.work/schemas/public_gateway_preflight_inventory_v1.schema.json", + "title": "IwoooS public gateway 變更前置 Gate 只讀清冊", + "description": "定義 Nginx public gateway reload / route change 前必備的 owner、diff、nginx -t、route smoke、maintenance window 與 rollback 欄位。本契約不授權 SSH、live conf 讀取、nginx -t、Nginx reload、DNS / TLS probe、certbot renew 或 host write。", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "generated_at", + "status", + "source_scope", + "git_commit", + "source_reports", + "summary", + "execution_boundaries", + "required_preflight_gates", + "config_preflight_rows", + "route_impacts", + "unique_upstreams", + "required_owner_fields", + "next_collection_order", + "operator_interpretation" + ], + "properties": { + "schema_version": { + "const": "public_gateway_preflight_inventory_v1" + }, + "generated_at": { + "type": "string" + }, + "status": { + "const": "repo_only_preflight_contract_ready" + }, + "source_scope": { + "const": "committed_nginx_and_domain_tls_snapshots_only" + }, + "git_commit": { + "type": "string" + }, + "source_reports": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "string" + } + }, + "summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_config_count", + "c0_source_config_count", + "managed_domain_count", + "route_impact_count", + "unique_upstream_count", + "tls_certificate_path_count", + "certificate_owner_confirmation_required_count", + "admin_route_domain_count", + "websocket_route_domain_count", + "acme_challenge_domain_count", + "preflight_gate_count", + "repo_only_preflight_ready_count", + "owner_acceptance_required_gate_count", + "preflight_gate_accepted_count", + "owner_response_received_count", + "owner_response_accepted_count", + "owner_provided_live_conf_received_count", + "rendered_diff_ready_count", + "nginx_test_evidence_count", + "route_smoke_evidence_count", + "maintenance_window_accepted_count", + "rollback_owner_accepted_count", + "runtime_gate_count", + "action_button_count", + "coverage_percent_before_preflight", + "coverage_percent_after_preflight" + ], + "properties": { + "source_config_count": { + "const": 3 + }, + "c0_source_config_count": { + "const": 2 + }, + "managed_domain_count": { + "const": 14 + }, + "route_impact_count": { + "const": 14 + }, + "unique_upstream_count": { + "type": "integer", + "minimum": 1 + }, + "tls_certificate_path_count": { + "const": 10 + }, + "certificate_owner_confirmation_required_count": { + "const": 4 + }, + "admin_route_domain_count": { + "const": 1 + }, + "websocket_route_domain_count": { + "const": 6 + }, + "acme_challenge_domain_count": { + "const": 7 + }, + "preflight_gate_count": { + "const": 12 + }, + "repo_only_preflight_ready_count": { + "const": 2 + }, + "owner_acceptance_required_gate_count": { + "const": 10 + }, + "preflight_gate_accepted_count": { + "const": 0 + }, + "owner_response_received_count": { + "const": 0 + }, + "owner_response_accepted_count": { + "const": 0 + }, + "owner_provided_live_conf_received_count": { + "const": 0 + }, + "rendered_diff_ready_count": { + "const": 0 + }, + "nginx_test_evidence_count": { + "const": 0 + }, + "route_smoke_evidence_count": { + "const": 0 + }, + "maintenance_window_accepted_count": { + "const": 0 + }, + "rollback_owner_accepted_count": { + "const": 0 + }, + "runtime_gate_count": { + "const": 0 + }, + "action_button_count": { + "const": 0 + }, + "coverage_percent_before_preflight": { + "const": 78 + }, + "coverage_percent_after_preflight": { + "const": 84 + } + } + }, + "execution_boundaries": { + "type": "object", + "additionalProperties": { + "const": false + }, + "required": [ + "runtime_execution_authorized", + "host_live_conf_read_authorized", + "ssh_read_authorized", + "ssh_write_authorized", + "host_write_authorized", + "nginx_test_authorized", + "nginx_test_executed", + "nginx_reload_authorized", + "nginx_reload_executed", + "public_gateway_reload_authorized", + "public_route_change_authorized", + "admin_route_change_authorized", + "websocket_route_change_authorized", + "acme_challenge_change_authorized", + "dns_query_executed", + "live_tls_probe_executed", + "certbot_renew_authorized", + "certbot_renew_executed", + "route_smoke_authorized", + "route_smoke_executed", + "rollback_executed", + "secret_value_collection_allowed", + "action_buttons_allowed" + ] + }, + "required_preflight_gates": { + "type": "array", + "minItems": 12, + "maxItems": 12, + "items": { + "$ref": "#/$defs/preflight_gate" + } + }, + "config_preflight_rows": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": { + "$ref": "#/$defs/config_preflight_row" + } + }, + "route_impacts": { + "type": "array", + "minItems": 14, + "maxItems": 14, + "items": { + "$ref": "#/$defs/route_impact" + } + }, + "unique_upstreams": { + "type": "array", + "items": { + "type": "string" + } + }, + "required_owner_fields": { + "type": "array", + "minItems": 11, + "items": { + "type": "string" + } + }, + "next_collection_order": { + "type": "array", + "minItems": 10, + "items": { + "type": "string" + } + }, + "operator_interpretation": { + "type": "array", + "minItems": 4, + "items": { + "type": "string" + } + } + }, + "$defs": { + "preflight_gate": { + "type": "object", + "additionalProperties": false, + "required": [ + "gate_id", + "label", + "required_evidence", + "owner_acceptance_required", + "repo_only_ready" + ], + "properties": { + "gate_id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "required_evidence": { + "type": "string" + }, + "owner_acceptance_required": { + "type": "boolean" + }, + "repo_only_ready": { + "type": "boolean" + } + } + }, + "config_preflight_row": { + "type": "object", + "additionalProperties": false, + "required": [ + "config_id", + "host", + "role", + "control_tier", + "owner_gate", + "repo_source_path", + "live_path", + "server_block_count", + "server_name_count", + "upstream_count", + "tls_certificate_path_count", + "admin_route_count", + "acme_route_count", + "websocket_route_count", + "repo_source_hash_ready", + "owner_response_received", + "owner_response_accepted", + "live_conf_evidence_received", + "rendered_diff_ready", + "nginx_test_evidence_received", + "route_smoke_evidence_received", + "maintenance_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open" + ], + "properties": { + "config_id": { + "type": "string" + }, + "host": { + "type": "string" + }, + "role": { + "type": "string" + }, + "control_tier": { + "enum": [ + "C0", + "C1", + "C2", + "C3" + ] + }, + "owner_gate": { + "type": "string" + }, + "repo_source_path": { + "type": "string" + }, + "live_path": { + "type": "string" + }, + "server_block_count": { + "type": "integer", + "minimum": 0 + }, + "server_name_count": { + "type": "integer", + "minimum": 0 + }, + "upstream_count": { + "type": "integer", + "minimum": 0 + }, + "tls_certificate_path_count": { + "type": "integer", + "minimum": 0 + }, + "admin_route_count": { + "type": "integer", + "minimum": 0 + }, + "acme_route_count": { + "type": "integer", + "minimum": 0 + }, + "websocket_route_count": { + "type": "integer", + "minimum": 0 + }, + "repo_source_hash_ready": { + "const": true + }, + "owner_response_received": { + "const": false + }, + "owner_response_accepted": { + "const": false + }, + "live_conf_evidence_received": { + "const": false + }, + "rendered_diff_ready": { + "const": false + }, + "nginx_test_evidence_received": { + "const": false + }, + "route_smoke_evidence_received": { + "const": false + }, + "maintenance_window_accepted": { + "const": false + }, + "rollback_owner_accepted": { + "const": false + }, + "runtime_gate_open": { + "const": false + } + } + }, + "route_impact": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain", + "hosts", + "config_ids", + "control_tier", + "upstream_count", + "has_tls_certificate_path", + "certificate_owner_confirmation_required", + "acme_challenge_present", + "admin_route_count", + "websocket_route_count", + "public_route_smoke_required", + "admin_route_smoke_required", + "websocket_or_api_smoke_required", + "tls_owner_check_required", + "owner_response_accepted", + "route_smoke_accepted" + ], + "properties": { + "domain": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "config_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "control_tier": { + "enum": [ + "C0", + "C1", + "C2", + "C3" + ] + }, + "upstream_count": { + "type": "integer", + "minimum": 0 + }, + "has_tls_certificate_path": { + "type": "boolean" + }, + "certificate_owner_confirmation_required": { + "type": "boolean" + }, + "acme_challenge_present": { + "type": "boolean" + }, + "admin_route_count": { + "type": "integer", + "minimum": 0 + }, + "websocket_route_count": { + "type": "integer", + "minimum": 0 + }, + "public_route_smoke_required": { + "const": true + }, + "admin_route_smoke_required": { + "type": "boolean" + }, + "websocket_or_api_smoke_required": { + "type": "boolean" + }, + "tls_owner_check_required": { + "type": "boolean" + }, + "owner_response_accepted": { + "const": false + }, + "route_smoke_accepted": { + "const": false + } + } + } + } +} diff --git a/docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md b/docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md index 2d82be083..bf4225c86 100644 --- a/docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md +++ b/docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md @@ -89,3 +89,9 @@ python3 scripts/security/domain-tls-certbot-inventory.py --root . | IwoooS 前台只讀呈現 | `待本輪部署驗證` | 只顯示摘要與邊界,不提供操作按鈕 | | live DNS / TLS validation | `0%` | 尚未批准;不得用本清冊替代 live probe | | certbot renew / Nginx reload | `0%` | 未授權,未執行 | + +## 8. Preflight 銜接 + +`docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md` 會把本清冊中的 domain、TLS certificate path、ACME、admin route 與 WebSocket 影響面,轉成 public gateway reload / route change 前的 route smoke 與 owner check 條件。 + +此銜接仍不代表 DNS 查詢、TLS probe、certbot renew、Nginx reload 或主機寫入已授權。 diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index d5fbf556e..f8d4d5549 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -2,7 +2,7 @@ | 項目 | 內容 | |------|------| -| 日期 | 2026-06-11 | +| 日期 | 2026-06-12 | | 狀態 | `coverage_matrix_ready` | | 工具 | `scripts/security/high-value-config-control-coverage.py` | | Snapshot | `docs/security/high-value-config-control-coverage.snapshot.json` | @@ -25,7 +25,7 @@ | C2 類別 | `1` | 產品 runtime route 與跨產品邊界 | | C3 類別 | `1` | security evidence / snapshot / guard tooling | | 平均只讀控管成熟度 | `66%` | 僅代表框架 / evidence / owner packet 準備度,不代表 runtime 可執行 | -| 需要 live evidence 的類別 | `6` | 只能等 owner-provided redacted evidence 或維護窗口,不主動修改主機 | +| 需要 live evidence 的類別 | `7` | 只能等 owner-provided redacted evidence 或維護窗口,不主動修改主機 | | owner response required | `14` | 每類都需要 owner response 才能往 accepted 前進 | | owner response received / accepted | `0 / 0` | 不得假性提高 | | runtime gate | `0` | 不得產生執行按鈕 | @@ -46,6 +46,15 @@ ```text runtime_execution_authorized=false host_write_authorized=false +host_live_conf_read_authorized=false +nginx_test_authorized=false +public_gateway_reload_authorized=false +public_route_change_authorized=false +admin_route_change_authorized=false +websocket_route_change_authorized=false +acme_challenge_change_authorized=false +route_smoke_authorized=false +rollback_executed=false nginx_reload_authorized=false dns_tls_change_authorized=false certbot_renew_authorized=false @@ -138,3 +147,7 @@ python3 scripts/security/high-value-config-control-coverage.py \ ## 11. P1-4 Monitoring / alerting / observability 清冊更新 `monitoring_alerting_observability_inventory_v1` 已把 Prometheus、Alertmanager、Grafana、SigNoz、Sentry、Langfuse、OTEL、Telegram / notification policy、deploy / reload scripts 與 alert chain smoke scripts 納入 repo-only 清冊,共 `60` 個 surface、`13` 個 alert rule surface、`6` 個 deploy / reload surface、`11` 個 write-capable surface 與 `1` 個 drift guard surface。此更新只讓 `monitoring_alerting_observability` 從 `56%` 推進到 `62%`;owner response、live evidence、reload owner、receiver owner、route smoke、runtime gate 與 action button 仍全部為 `0`。 + +## 12. P0 Public Gateway Preflight 清冊更新 + +`public_gateway_preflight_inventory_v1` 已把 Nginx public gateway reload / route change 前置 Gate 固定成只讀清冊,共 `3` 份 source config、`14` 個 route impact、`14` 個 unique upstream、`12` 個 preflight gate,其中 `2` 個 gate 只代表 repo-only ready,`10` 個 gate 仍需 owner acceptance。此更新只讓 `nginx_public_gateway` 從 `78%` 推進到 `84%`;owner response、owner-provided live conf、rendered diff、`nginx -t` evidence、route smoke、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 51f01dc5b..c50a62be8 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -2,7 +2,7 @@ | 項目 | 內容 | |------|------| -| 日期 | 2026-06-11 | +| 日期 | 2026-06-12 | | 狀態 | `inventory_and_classification_gate_ready` | | 範圍 | AWOOOI / IwoooS 全產品重要配置 | | 本階段模式 | source-control 修補 + 只讀盤點,不做 live reload / restart / sync | @@ -33,7 +33,7 @@ | C0 類別 | `8` | Nginx、DNS / TLS、K8s、secret、workflow / runner、runtime config、backup、agent-bounty runtime | | C1 類別 | `4` | 監控、Docker / systemd、SSH / network、AI provider | | 平均只讀控管成熟度 | `66%` | 只代表框架 / evidence / owner packet 準備度 | -| 需要 live evidence 類別 | `6` | 只能等 owner-provided redacted evidence 或維護窗口,不主動 SSH | +| 需要 live evidence 類別 | `7` | 只能等 owner-provided redacted evidence 或維護窗口,不主動 SSH | | owner response required | `14` | owner response received / accepted 仍 `0 / 0` | | runtime gate | `0` | 不提供執行按鈕 | @@ -63,16 +63,22 @@ 此更新仍不是 live alert chain truth:owner response、live evidence、reload owner、receiver owner、route smoke、maintenance window、rollback owner 與 runtime gate 全部仍為 `0`,也不得執行 Prometheus reload、Alertmanager reload、Grafana import、SigNoz rule apply、Sentry deploy、Langfuse change、OTEL reload、remote write change、silence change、Telegram send、live alert fire、alert chain smoke、SSH 或 kubectl。 +### 0.6 2026-06-12 Public Gateway Preflight repo-only 清冊 + +`public_gateway_preflight_inventory_v1` 已把 Nginx public gateway reload / route change 前置 Gate 固定成只讀 snapshot。清冊目前共有 `3` 份 Nginx source config、`2` 份 C0 source config、`14` 個 route impact、`14` 個 unique upstream、`10` 條 TLS certificate path、`4` 個 certificate owner 確認缺口、`7` 個 ACME challenge domain、`1` 個 admin route domain、`6` 個 WebSocket route domain 與 `12` 個 preflight gate,讓 Nginx public gateway 類別成熟度從 `78%` 推進到 `84%`。 + +此更新仍不是 live gateway truth:owner response、owner-provided live conf、rendered diff、`nginx -t` evidence、route smoke、maintenance window、rollback owner 與 runtime gate 全部仍為 `0`,也不得 SSH、讀 live conf、執行 `nginx -t`、reload Nginx、改 public route、改 admin route、改 WebSocket / API route、改 ACME、做 DNS / TLS probe、執行 certbot renew 或寫入主機。 + ## 1. 目前已不符合新要求的項目 | 優先 | 項目 | 現況 | 風險 | 本階段處置 | |------|------|------|------|------------| -| P0 | Nginx public gateway | 已有 Ansible source-of-truth,但缺少資安等級、owner gate、drift evidence、reload 前後驗證與跨產品通知規範 | 手改 live conf 會讓公開網站、admin route、TLS、API、WebSocket 或 ACME 被改壞,且不易追責 | 已新增高價值配置 Hard Rule 與本清冊;live drift detector 尚未實作 | +| P0 | Nginx public gateway | 已有 Ansible source-of-truth、repo-only drift detector、DNS / TLS 清冊與 public gateway preflight Gate,但尚缺 owner-provided live conf、rendered diff、`nginx -t` evidence、route smoke、maintenance window 與 rollback owner | 手改 live conf 會讓公開網站、admin route、TLS、API、WebSocket 或 ACME 被改壞,且不易追責 | 已新增高價值配置 Hard Rule、drift detector 與 preflight 清冊;仍不得 SSH 或 reload | | P0 | `docs/runbooks/SECRETS-MANAGEMENT.md` Gitea token 範例 | 文件內存在可疑 token 範例 | 可能造成 Gitea API 權限外洩或複製貼上事故 | 已改為 owner-managed token env,不保存 value | | P0 | `k8s/monitoring/docker-compose-110.yml` Grafana admin 密碼 | compose 內有固定密碼常值 | 若被當作 live 密碼或複製使用,會造成監控後台弱控管 | 已改為 `GRAFANA_ADMIN_PASSWORD` owner secret store 注入 | | P0 | `ops/monitoring/discover_docker.py` SSH host key 驗證 | 仍使用關閉 host key 驗證的參數 | MITM 或錯誤主機信任風險 | 已改為 `BatchMode=yes` + `accept-new`;後續升級 pinned known_hosts | | P0 | `apps/api/src/api/v1/monitoring.py` Grafana 探測認證 | 程式碼內有 Grafana Basic Auth 常值 | API 程式碼保存 credential,且會被複製到後續部署 | 已改為 `settings.GRAFANA_API_KEY` Bearer token;未設定時不送 Authorization header | -| P1 | Nginx 188 / 110 live conf drift | repo 有 templates,但尚未自動比較 live hash / rendered hash | 手改後 repo 不知道,下一次 Ansible 可能覆蓋或保留錯誤路由 | 新增為 P0 後續任務:只讀 drift detector | +| P1 | Nginx 188 / 110 live conf drift | repo 有 templates 與 drift detector,比對模式需 owner 提供脫敏 live conf;目前 live evidence 仍為 `0` | 手改後 repo 不知道,下一次 Ansible 可能覆蓋或保留錯誤路由 | 下一步收 owner-provided live conf 與 rendered diff,不主動 SSH | | P1 | 高價值配置變更 Gate | 已有 C0-C3 清冊與 Hard Rule,但原本缺少可重跑 path 分類 | reviewer 只能靠人工記憶判斷 Nginx、workflow、secret、K8s、DNS、AI provider 是否需 owner gate | 已新增 `scripts/security/high-value-config-change-gate.py`;本階段只分類,不接 CI blocking | | P1 | DNS / TLS / certbot | 多產品共用 188 / 110 public gateway,憑證路徑與 renewal 仍分散在 runbook / template | 憑證過期、錯誤 cert path、ACME challenge 被覆蓋會造成公開服務中斷 | 納入 C0,需建立 domain / cert / renewal 清冊 | | P1 | workflow / runner / deploy key / secret name | 已有 Gitea / GitHub readiness 盤點,但尚未把配置變更和 IwoooS 高價值配置共用 gate 合併 | workflow 或 runner 改錯會直接影響部署與 secret 注入 | 納入 C0,維持只讀 owner response,不收 secret value | @@ -156,7 +162,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | 規範 | 目前狀態 | 調整方向 | |------|----------|----------| | IwoooS 初期低摩擦 | 原本偏只讀框架 | 保留只讀框架,但 P0 即時危害可先做 source-control 止血 | -| Nginx DR runbook | 已寫禁止直接手改 live conf | 補 owner gate、drift detector、跨產品通知、post-check | +| Nginx DR runbook | 已寫禁止直接手改 live conf | 補 owner-provided live conf、rendered diff、`nginx -t` evidence、route smoke、跨產品通知、post-check | | Secrets 管理手冊 | 有 secret 來源與 CD 注入說明 | 去除可用 token 範例,補「metadata only」與 owner secret store | | Gitea / GitHub readiness | 已有 repo / workflow / secret name 盤點 | 與高價值配置分級合併,workflow 變更仍需獨立批准 | | Deployment verification | 偏重 Pod / health | 加入 Nginx / DNS / TLS / public route / admin route smoke | @@ -171,6 +177,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | Nginx 控管機制定義 | `100%` | 已定義 source-of-truth、live path、gate、drift 原則 | | source-control P0 止血 | `100%` | 已清掉本波掃到的 token 範例、Grafana 密碼常值與 SSH host key 關閉 | | repo-only Nginx drift detector | `100%` | 已新增 `scripts/security/nginx-config-drift-detector.py` 與 repo source-of-truth snapshot | +| public gateway preflight 清冊 | `100%` | 已新增 `public_gateway_preflight_inventory_v1`,固定 12 個 reload / route change 前置 Gate;成熟度 `78% -> 84%` | | 高價值配置變更分類 Gate | `100%` | 已新增 `scripts/security/high-value-config-change-gate.py`,可用 git diff 或手動檔案分類 C0/C1/C2/C3 並列出 owner / rollback / evidence / 驗證欄位 | | owner response evidence JSON 欄位檢查 | `70%` | Gate 可檢查必要欄位與 false flags;尚未接正式收件 API 或 AwoooP queue | | Gate → owner response packet 草案 | `100%` | 已新增 `scripts/security/high-value-config-owner-packet.py`,可將 impacted category 轉成 canonical owner response packet 草案 | diff --git a/docs/security/IWOOOS-POSTURE-PROJECTION.md b/docs/security/IWOOOS-POSTURE-PROJECTION.md index 1aa5518bf..4e55567c5 100644 --- a/docs/security/IWOOOS-POSTURE-PROJECTION.md +++ b/docs/security/IWOOOS-POSTURE-PROJECTION.md @@ -89,11 +89,12 @@ IwoooS 首版只讀取或對齊以下已提交 evidence: 51. 10 個 frontend surface reverse bridge statuses,顯示既有資安入口目前是 embedded bridge、direct bridge 或 AwoooP read-only candidate;這只是連接狀態,不代表 owner response、runtime authorization、Code Review blocker、Gitea/GitHub action 或任何執行控制。 52. 6 個 source control primary readiness items,顯示 GitHub primary 前置缺口:candidate repo inventory、primary ready counter、owner response validation、refs truth、workflow / secret name inventory、rollback ADR;這只是 readiness,不代表 repo 建立、visibility 變更、refs mutation、secret value collection、primary switch 或 Gitea 停用。 53. 4 個 rollout risk read-only items,顯示風險來源部署 marker、`AWOOOI_ROLLOUT_RISK=1`、ArgoCD `Degraded` / `OutOfSync`、API health / smoke 已通過與執行期閘門仍為 0;這只是部署風險可見性,不代表 ArgoCD sync、kubectl、主機重啟、修復、部署或 runtime gate 已授權。 -54. 14 類 high-value config control coverage statuses,顯示 Nginx、DNS / TLS、K8s、secret、workflow、backup、agent-bounty runtime、monitoring、Docker / systemd、SSH / network、AI provider、產品 route 與 security evidence 的全域配置控管覆蓋矩陣;平均只讀成熟度 `66%`、C0 類別 `8`、需 live evidence 類別 `6`、owner response received / accepted 與 runtime gate 仍為 `0`,不代表 reload、sync、scan、secret rotation、payout 或主機操作授權。 +54. 14 類 high-value config control coverage statuses,顯示 Nginx、DNS / TLS、K8s、secret、workflow、backup、agent-bounty runtime、monitoring、Docker / systemd、SSH / network、AI provider、產品 route 與 security evidence 的全域配置控管覆蓋矩陣;平均只讀成熟度 `66%`、C0 類別 `8`、需 live evidence 類別 `7`、owner response received / accepted 與 runtime gate 仍為 `0`,不代表 reload、sync、scan、secret rotation、payout 或主機操作授權。 55. 9 個 host-service config repo-only inventory surfaces,顯示 Docker Compose、systemd / repair-bot、Ansible service role 與 host config backup capture 的第一層清冊;write-capable surface `3`、repair-bot whitelist `2`、systemd restart surface `1`,owner response、live evidence、restart window、rollback owner、runtime gate 與 action button 仍全部為 `0`,不代表 `docker compose`、`systemctl`、repair-bot 或 Ansible apply 已授權。 56. 16 個 SSH / network access repo-only inventory surfaces,顯示 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 的第一層清冊;write-capable surface `6`、NetworkPolicy `2`、NodePort `2`、sudoers `1`、WireGuard `1`,owner response、live evidence、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0`,不代表 SSH、sudo、firewall、NetworkPolicy、NodePort、WireGuard 或 known_hosts patch 已授權。 57. 38 個 Backup / restore / escrow / retention repo-only inventory surfaces,顯示 backup orchestration、service backup scripts、restic retention、offsite sync、credential escrow、Velero restore drill、backup health alert 與 cold-start / DR runbook 的第一層清冊;write-capable surface `27`、restore drill surface `4`、offsite / escrow surface `8`,owner response、live evidence、restore drill、offsite sync、credential escrow、retention change、runtime gate 與 action button 仍全部為 `0`,不代表 backup、restore、offsite sync、remote delete、restic prune、escrow marker write、rclone config 或 Velero restore 已授權。 58. 60 個 Monitoring / alerting / observability repo-only inventory surfaces,顯示 Prometheus、Alertmanager、Grafana、SigNoz、Sentry、Langfuse、OTEL、Telegram / notification policy、deploy / reload scripts 與 alert chain smoke scripts 的第一層清冊;write-capable surface `11`、alert rule surface `13`、deploy / reload surface `6`,owner response、live evidence、reload owner、receiver owner、route smoke、runtime gate 與 action button 仍全部為 `0`,不代表 Prometheus reload、Alertmanager reload、Grafana import、SigNoz apply、Sentry deploy、Telegram send、live alert fire 或 alert chain smoke 已授權。 +59. 12 個 Public Gateway Preflight repo-only gates,顯示 Nginx public gateway reload / route change 前必備的 owner、live conf、rendered diff、`nginx -t`、public / admin / WebSocket route smoke、ACME / TLS owner check、maintenance window 與 rollback owner;source config `3`、route impact `14`、repo-only ready gate `2`,owner acceptance、live conf、rendered diff、`nginx -t` evidence、route smoke、runtime gate 與 action button 仍全部為 `0`,不代表 SSH、live conf read、Nginx reload、route change、DNS / TLS probe 或 certbot renew 已授權。 ## 3.1 既有前端資安頁面整合 diff --git a/docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md b/docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md index 9bb443b11..e8230538f 100644 --- a/docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md +++ b/docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md @@ -79,3 +79,9 @@ python3 scripts/security/nginx-config-drift-detector.py \ | owner-provided live file 比對格式 | `70%` | 支援手動提供 live conf 檔,不主動取得 live | | live Nginx 證據收集 | `0%` | 本階段不 SSH、不讀主機 | | Nginx reload / restart | `0%` | 未授權且未執行 | + +## 8. Preflight 銜接 + +`docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md` 會消費本工具與 DNS / TLS 清冊的 repo-only 結果,固定 Nginx reload / route change 前必備的 owner、live conf、rendered diff、`nginx -t`、route smoke、maintenance window 與 rollback owner 欄位。 + +此銜接仍不代表 live conf 已收、drift 已判定、`nginx -t` 已執行或 reload 已授權。 diff --git a/docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md b/docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md new file mode 100644 index 000000000..de76b40d7 --- /dev/null +++ b/docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md @@ -0,0 +1,115 @@ +# IwoooS Public Gateway 變更前置 Gate 只讀清冊 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-12 | +| 狀態 | `repo_only_preflight_contract_ready` | +| 工具 | `scripts/security/public-gateway-preflight-inventory.py` | +| Snapshot | `docs/security/public-gateway-preflight-inventory.snapshot.json` | +| Schema | `docs/schemas/public_gateway_preflight_inventory_v1.schema.json` | +| runtime gate | `0` | + +## 1. 目的 + +Public gateway 是所有產品、後台、API、callback、Webhook、ACME 與 WebSocket 的入口。Nginx 或 reverse proxy 若被手動修改,可能造成公開路由錯轉、管理後台暴露、憑證路徑錯置、ACME renew 失敗、WebSocket 中斷或 AI provider proxy 失效。 + +本清冊把 Nginx reload / public route change 前必備證據固定成只讀 Gate:誰負責、影響哪些 route、是否有 live conf evidence、是否有 rendered diff、是否有 `nginx -t`、是否有 route smoke、是否有 rollback owner。它不做任何 live 操作。 + +## 2. repo-only 摘要 + +| 指標 | 值 | +|------|----| +| Nginx source config | `3` | +| C0 source config | `2` | +| route impact | `14` | +| unique upstream | `14` | +| TLS certificate path | `10` | +| certificate owner 確認缺口 | `4` | +| ACME challenge domain | `7` | +| admin route domain | `1` | +| WebSocket route domain | `6` | +| preflight gate | `12` | +| repo-only ready gate | `2` | +| owner acceptance required gate | `10` | +| owner response / accepted | `0 / 0` | +| live conf / rendered diff / nginx -t / route smoke | `0 / 0 / 0 / 0` | +| maintenance window / rollback owner | `0 / 0` | +| runtime gate / action button | `0 / 0` | + +## 3. 已納入的 public gateway source + +| config id | 主機 | 角色 | 等級 | source | +|-----------|------|------|------|--------| +| `host188_all_sites` | `192.168.0.188` | public gateway all sites | C0 | `infra/ansible/roles/nginx/templates/188-all-sites.conf.j2` | +| `host188_internal_tools_https` | `192.168.0.188` | internal tools HTTPS | C0 | `infra/ansible/roles/nginx/templates/188-internal-tools-https.conf.j2` | +| `host110_ollama_proxy` | `192.168.0.110` | Ollama proxy gateway | C1 | `infra/ansible/roles/nginx/templates/110-ollama-proxy.conf.j2` | + +## 4. Reload / route change 前置 Gate + +| Gate | 內容 | 目前狀態 | +|------|------|----------| +| PG1 | source-of-truth hash | repo-only ready | +| PG2 | affected route list | repo-only ready | +| PG3 | owner response | `0` | +| PG4 | owner-provided live config | `0` | +| PG5 | rendered diff | `0` | +| PG6 | `nginx -t` evidence | `0` | +| PG7 | public route smoke | `0` | +| PG8 | admin route smoke | `0` | +| PG9 | WebSocket / API smoke | `0` | +| PG10 | ACME / TLS owner check | `0` | +| PG11 | maintenance window | `0` | +| PG12 | rollback owner and ref | `0` | + +## 5. owner response 欄位 + +任何 public gateway 變更、live drift 判讀或 reload 候選,都至少要具備: + +1. `owner_role_or_team` +2. `decision` +3. `decision_reason` +4. `affected_scope` +5. `redacted_evidence_refs` +6. `followup_owner` +7. `rollback_owner` +8. `maintenance_window` +9. `validation_plan` +10. `nginx_test_evidence_ref` +11. `route_smoke_evidence_ref` + +## 6. 指令 + +更新 snapshot: + +```bash +python3 scripts/security/public-gateway-preflight-inventory.py \ + --root . \ + --generated-at 2026-06-12T10:30:00+08:00 \ + --output docs/security/public-gateway-preflight-inventory.snapshot.json +``` + +只輸出目前清冊: + +```bash +python3 scripts/security/public-gateway-preflight-inventory.py --root . +``` + +## 7. 邊界 + +1. 本清冊不執行 SSH。 +2. 本清冊不讀 live Nginx conf。 +3. 本清冊不執行 `nginx -t`。 +4. 本清冊不 reload / restart Nginx。 +5. 本清冊不做 DNS query、TLS probe 或 certbot renew。 +6. 本清冊不修改主機、不改 DNS、不改憑證、不讀 private key。 +7. IwoooS UI 可顯示 preflight Gate,但不得把卡片可見視為 owner 已確認或 runtime 已授權。 + +## 8. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| public gateway preflight 契約 | `100%` | 已固定 12 個 reload / route change 前置 Gate | +| source-of-truth hash / affected route list | `100%` | 由既有 Nginx / DNS-TLS snapshot 推導 | +| owner response / live conf / rendered diff | `0%` | 尚未收到,不能宣稱 live 無漂移 | +| `nginx -t` / route smoke / rollback owner | `0%` | 尚未批准且未執行 | +| runtime reload / host write | `0%` | 未授權,未執行 | diff --git a/docs/security/high-value-config-control-coverage.snapshot.json b/docs/security/high-value-config-control-coverage.snapshot.json index 2840cabb7..ee4659f9d 100644 --- a/docs/security/high-value-config-control-coverage.snapshot.json +++ b/docs/security/high-value-config-control-coverage.snapshot.json @@ -4,16 +4,19 @@ "action_buttons_allowed": false, "category_id": "nginx_public_gateway", "control_tier": "C0", - "coverage_percent": 78, - "coverage_status": "repo_only_drift_detector_ready", - "current_gap": "缺 owner-provided live conf 匯出與 approved maintenance window;不得 SSH 擷取或 reload。", + "coverage_percent": 84, + "coverage_status": "repo_only_preflight_contract_ready_needs_owner_live_diff", + "current_gap": "已固定 12 個 public gateway preflight gate;owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 仍全部為 0。", "evidence_refs": [ "docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md", "docs/security/nginx-config-drift-repo.snapshot.json", - "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md" + "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md", + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/schemas/public_gateway_preflight_inventory_v1.schema.json" ], "label": "Nginx / reverse proxy / public route", - "next_owner_action": "提供脫敏 live conf 匯出檔或確認維護窗口、rollback owner 與 route smoke plan。", + "next_owner_action": "補 public gateway owner、owner-provided live conf、source-to-live rendered diff、nginx -t evidence、route smoke、maintenance window 與 rollback owner。", "owner_response_accepted": false, "owner_response_received": false, "owner_response_required": true, @@ -509,8 +512,10 @@ } ], "execution_boundaries": { + "acme_challenge_change_authorized": false, "action_buttons_allowed": false, "active_scan_authorized": false, + "admin_route_change_authorized": false, "agent_bounty_runtime_authorized": false, "alert_chain_smoke_authorized": false, "alertmanager_reload_authorized": false, @@ -522,17 +527,21 @@ "exporter_deploy_authorized": false, "force_push_authorized": false, "grafana_dashboard_apply_authorized": false, + "host_live_conf_read_authorized": false, "host_write_authorized": false, "kubectl_action_authorized": false, "langfuse_config_change_authorized": false, "live_alert_fire_authorized": false, "nginx_reload_authorized": false, + "nginx_test_authorized": false, "notification_route_change_authorized": false, "offsite_remote_delete_authorized": false, "offsite_sync_authorized": false, "otel_collector_reload_authorized": false, "payout_or_withdrawal_authorized": false, "prometheus_reload_authorized": false, + "public_gateway_reload_authorized": false, + "public_route_change_authorized": false, "rclone_config_authorized": false, "receiver_route_change_authorized": false, "refs_sync_authorized": false, @@ -541,6 +550,8 @@ "restore_drill_authorized": false, "restore_run_authorized": false, "retention_change_authorized": false, + "rollback_executed": false, + "route_smoke_authorized": false, "runner_change_authorized": false, "runtime_execution_authorized": false, "secret_value_collection_allowed": false, @@ -550,10 +561,11 @@ "telegram_send_authorized": false, "velero_restore_authorized": false, "webhook_receiver_change_authorized": false, + "websocket_route_change_authorized": false, "workflow_modification_authorized": false }, - "generated_at": "2026-06-12T09:05:00+08:00", - "git_commit": "7a7daa33", + "generated_at": "2026-06-12T10:35:00+08:00", + "git_commit": "e4747722", "lowest_coverage_categories": [ { "category_id": "docker_compose_systemd_host_config", @@ -613,7 +625,7 @@ "c3_category_count": 1, "category_count": 14, "lowest_coverage_category_count": 4, - "needs_live_evidence_count": 6, + "needs_live_evidence_count": 7, "owner_response_accepted_count": 0, "owner_response_received_count": 0, "owner_response_required_count": 14, diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index 06c5f6d8f..a977cee8f 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -22,6 +22,9 @@ "docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md", "docs/security/high-value-config-change-gate.snapshot.json", "docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md", + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", "docs/security/domain-tls-certbot-inventory.snapshot.json", "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md", "docs/schemas/domain_tls_certbot_inventory_v1.schema.json", @@ -246,7 +249,7 @@ "high_value_config_control_coverage_c0_category_count": 8, "high_value_config_control_coverage_c1_category_count": 4, "high_value_config_control_coverage_average_percent": 66, - "high_value_config_control_coverage_needs_live_evidence_count": 6, + "high_value_config_control_coverage_needs_live_evidence_count": 7, "high_value_config_control_coverage_owner_response_required_count": 14, "high_value_config_control_coverage_owner_response_received_count": 0, "high_value_config_control_coverage_owner_response_accepted_count": 0, @@ -467,7 +470,36 @@ "monitoring_alerting_observability_inventory_runtime_gate_count": 0, "monitoring_alerting_observability_inventory_action_button_count": 0, "monitoring_alerting_observability_inventory_coverage_percent_before_inventory": 56, - "monitoring_alerting_observability_inventory_coverage_percent_after_inventory": 62 + "monitoring_alerting_observability_inventory_coverage_percent_after_inventory": 62, + "public_gateway_preflight_first_layer": true, + "public_gateway_preflight_summary_count": 4, + "public_gateway_preflight_item_count": 6, + "public_gateway_preflight_source_config_count": 3, + "public_gateway_preflight_c0_source_config_count": 2, + "public_gateway_preflight_managed_domain_count": 14, + "public_gateway_preflight_route_impact_count": 14, + "public_gateway_preflight_unique_upstream_count": 14, + "public_gateway_preflight_tls_certificate_path_count": 10, + "public_gateway_preflight_certificate_owner_confirmation_required_count": 4, + "public_gateway_preflight_admin_route_domain_count": 1, + "public_gateway_preflight_websocket_route_domain_count": 6, + "public_gateway_preflight_acme_challenge_domain_count": 7, + "public_gateway_preflight_gate_count": 12, + "public_gateway_preflight_repo_only_ready_count": 2, + "public_gateway_preflight_owner_acceptance_required_gate_count": 10, + "public_gateway_preflight_gate_accepted_count": 0, + "public_gateway_preflight_owner_response_received_count": 0, + "public_gateway_preflight_owner_response_accepted_count": 0, + "public_gateway_preflight_owner_provided_live_conf_received_count": 0, + "public_gateway_preflight_rendered_diff_ready_count": 0, + "public_gateway_preflight_nginx_test_evidence_count": 0, + "public_gateway_preflight_route_smoke_evidence_count": 0, + "public_gateway_preflight_maintenance_window_accepted_count": 0, + "public_gateway_preflight_rollback_owner_accepted_count": 0, + "public_gateway_preflight_runtime_gate_count": 0, + "public_gateway_preflight_action_button_count": 0, + "public_gateway_preflight_coverage_percent_before_preflight": 78, + "public_gateway_preflight_coverage_percent_after_preflight": 84 }, "progress": { "overall_percent": 64, diff --git a/docs/security/public-gateway-preflight-inventory.snapshot.json b/docs/security/public-gateway-preflight-inventory.snapshot.json new file mode 100644 index 000000000..6bfbe9a07 --- /dev/null +++ b/docs/security/public-gateway-preflight-inventory.snapshot.json @@ -0,0 +1,589 @@ +{ + "config_preflight_rows": [ + { + "acme_route_count": 2, + "admin_route_count": 2, + "config_id": "host188_all_sites", + "control_tier": "C0", + "host": "192.168.0.188", + "live_conf_evidence_received": false, + "live_path": "/etc/nginx/sites-enabled/all-sites.conf", + "maintenance_window_accepted": false, + "nginx_test_evidence_received": false, + "owner_gate": "public_gateway_owner_response_required", + "owner_response_accepted": false, + "owner_response_received": false, + "rendered_diff_ready": false, + "repo_source_hash_ready": true, + "repo_source_path": "infra/ansible/roles/nginx/templates/188-all-sites.conf.j2", + "role": "public_gateway_all_sites", + "rollback_owner_accepted": false, + "route_smoke_evidence_received": false, + "runtime_gate_open": false, + "server_block_count": 15, + "server_name_count": 9, + "tls_certificate_path_count": 7, + "upstream_count": 10, + "websocket_route_count": 5 + }, + { + "acme_route_count": 1, + "admin_route_count": 0, + "config_id": "host188_internal_tools_https", + "control_tier": "C0", + "host": "192.168.0.188", + "live_conf_evidence_received": false, + "live_path": "owner_confirmation_required", + "maintenance_window_accepted": false, + "nginx_test_evidence_received": false, + "owner_gate": "public_tools_owner_response_required", + "owner_response_accepted": false, + "owner_response_received": false, + "rendered_diff_ready": false, + "repo_source_hash_ready": true, + "repo_source_path": "infra/ansible/roles/nginx/templates/188-internal-tools-https.conf.j2", + "role": "public_internal_tools_https", + "rollback_owner_accepted": false, + "route_smoke_evidence_received": false, + "runtime_gate_open": false, + "server_block_count": 8, + "server_name_count": 7, + "tls_certificate_path_count": 4, + "upstream_count": 6, + "websocket_route_count": 2 + }, + { + "acme_route_count": 0, + "admin_route_count": 0, + "config_id": "host110_ollama_proxy", + "control_tier": "C1", + "host": "192.168.0.110", + "live_conf_evidence_received": false, + "live_path": "/etc/nginx/sites-enabled/110-ollama-proxy.conf", + "maintenance_window_accepted": false, + "nginx_test_evidence_received": false, + "owner_gate": "ai_provider_proxy_owner_response_required", + "owner_response_accepted": false, + "owner_response_received": false, + "rendered_diff_ready": false, + "repo_source_hash_ready": true, + "repo_source_path": "infra/ansible/roles/nginx/templates/110-ollama-proxy.conf.j2", + "role": "ollama_proxy_gateway", + "rollback_owner_accepted": false, + "route_smoke_evidence_received": false, + "runtime_gate_open": false, + "server_block_count": 3, + "server_name_count": 0, + "tls_certificate_path_count": 0, + "upstream_count": 3, + "websocket_route_count": 0 + } + ], + "execution_boundaries": { + "acme_challenge_change_authorized": false, + "action_buttons_allowed": false, + "admin_route_change_authorized": false, + "certbot_renew_authorized": false, + "certbot_renew_executed": false, + "dns_query_executed": false, + "host_live_conf_read_authorized": false, + "host_write_authorized": false, + "live_tls_probe_executed": false, + "nginx_reload_authorized": false, + "nginx_reload_executed": false, + "nginx_test_authorized": false, + "nginx_test_executed": false, + "public_gateway_reload_authorized": false, + "public_route_change_authorized": false, + "rollback_executed": false, + "route_smoke_authorized": false, + "route_smoke_executed": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "websocket_route_change_authorized": false + }, + "generated_at": "2026-06-12T10:30:00+08:00", + "git_commit": "e4747722", + "next_collection_order": [ + "public_gateway_owner_response", + "owner_provided_live_conf_export", + "source_to_live_rendered_diff", + "nginx_t_evidence", + "affected_public_route_smoke", + "admin_route_smoke_if_affected", + "websocket_or_api_smoke_if_affected", + "tls_certificate_owner_confirmation", + "maintenance_window", + "rollback_owner_and_ref" + ], + "operator_interpretation": [ + "這是 Nginx public gateway 變更前置控管清冊,不是 reload 授權。", + "repo-only ready 只代表 source hash 與 affected route list 已能重跑產生。", + "owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 全部仍為 0。", + "若未來發現 live drift,只能建立 evidence 與 owner decision,不得自動覆寫 live。" + ], + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan", + "nginx_test_evidence_ref", + "route_smoke_evidence_ref" + ], + "required_preflight_gates": [ + { + "gate_id": "PG1", + "label": "source-of-truth hash", + "owner_acceptance_required": false, + "repo_only_ready": true, + "required_evidence": "repo_source_raw_and_normalized_sha256" + }, + { + "gate_id": "PG2", + "label": "affected route list", + "owner_acceptance_required": false, + "repo_only_ready": true, + "required_evidence": "domain_route_upstream_tls_admin_websocket_acme_inventory" + }, + { + "gate_id": "PG3", + "label": "owner response", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "owner role/team, decision, reason, affected scope, redacted refs, followup owner" + }, + { + "gate_id": "PG4", + "label": "owner-provided live config", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "redacted live conf export or owner statement" + }, + { + "gate_id": "PG5", + "label": "rendered diff", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "source-to-live rendered diff with redaction" + }, + { + "gate_id": "PG6", + "label": "nginx -t evidence", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "syntax test output captured by approved maintainer" + }, + { + "gate_id": "PG7", + "label": "public route smoke", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "public route smoke plan and result for affected domains" + }, + { + "gate_id": "PG8", + "label": "admin route smoke", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "admin/auth route smoke when admin path is affected" + }, + { + "gate_id": "PG9", + "label": "WebSocket / API smoke", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "WebSocket or API smoke when upstream or upgrade header is affected" + }, + { + "gate_id": "PG10", + "label": "ACME / TLS owner check", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "certificate path, SAN/wildcard/shared-cert statement, ACME impact" + }, + { + "gate_id": "PG11", + "label": "maintenance window", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "approved window and notification scope" + }, + { + "gate_id": "PG12", + "label": "rollback owner and ref", + "owner_acceptance_required": true, + "repo_only_ready": false, + "required_evidence": "rollback owner, rollback file/ref, post-rollback smoke" + } + ], + "route_impacts": [ + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "aiops.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 3, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 1 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "bitan.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 1 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": true, + "config_ids": [ + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "gitea.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": true, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 1 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "gitlab.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "harbor.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": true, + "config_ids": [ + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "langfuse.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": true, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "mo.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "registry.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "sentry.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": true, + "config_ids": [ + "host188_all_sites", + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "signoz.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": true, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 1 + }, + { + "acme_challenge_present": true, + "admin_route_count": 2, + "admin_route_smoke_required": true, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites", + "host188_internal_tools_https" + ], + "control_tier": "C0", + "domain": "stock.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 2 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": true, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "tsenyang.com", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": true, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + }, + { + "acme_challenge_present": true, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "vtuber.wooo.work", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 1 + }, + { + "acme_challenge_present": false, + "admin_route_count": 0, + "admin_route_smoke_required": false, + "certificate_owner_confirmation_required": false, + "config_ids": [ + "host188_all_sites" + ], + "control_tier": "C0", + "domain": "www.tsenyang.com", + "has_tls_certificate_path": true, + "hosts": [ + "192.168.0.188" + ], + "owner_response_accepted": false, + "public_route_smoke_required": true, + "route_smoke_accepted": false, + "tls_owner_check_required": false, + "upstream_count": 1, + "websocket_or_api_smoke_required": true, + "websocket_route_count": 0 + } + ], + "schema_version": "public_gateway_preflight_inventory_v1", + "source_reports": [ + "docs/security/nginx-config-drift-repo.snapshot.json", + "docs/security/domain-tls-certbot-inventory.snapshot.json" + ], + "source_scope": "committed_nginx_and_domain_tls_snapshots_only", + "status": "repo_only_preflight_contract_ready", + "summary": { + "acme_challenge_domain_count": 7, + "action_button_count": 0, + "admin_route_domain_count": 1, + "c0_source_config_count": 2, + "certificate_owner_confirmation_required_count": 4, + "coverage_percent_after_preflight": 84, + "coverage_percent_before_preflight": 78, + "maintenance_window_accepted_count": 0, + "managed_domain_count": 14, + "nginx_test_evidence_count": 0, + "owner_acceptance_required_gate_count": 10, + "owner_provided_live_conf_received_count": 0, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "preflight_gate_accepted_count": 0, + "preflight_gate_count": 12, + "rendered_diff_ready_count": 0, + "repo_only_preflight_ready_count": 2, + "rollback_owner_accepted_count": 0, + "route_impact_count": 14, + "route_smoke_evidence_count": 0, + "runtime_gate_count": 0, + "source_config_count": 3, + "tls_certificate_path_count": 10, + "unique_upstream_count": 14, + "websocket_route_domain_count": 6 + }, + "unique_upstreams": [ + "http://127.0.0.1:3000", + "http://127.0.0.1:3301", + "http://127.0.0.1:5003", + "http://192.168.0.110:3001", + "http://192.168.0.110:3003", + "http://192.168.0.110:3100", + "http://192.168.0.110:31235", + "http://192.168.0.110:5000", + "http://192.168.0.110:8929", + "http://192.168.0.110:9000", + "http://192.168.0.125:32334/api/", + "http://192.168.0.125:32334/api/v1/ws", + "http://192.168.0.125:32335", + "https://192.168.0.110" + ] +} diff --git a/scripts/security/high-value-config-control-coverage.py b/scripts/security/high-value-config-control-coverage.py index bbfb001d2..47ea73d39 100644 --- a/scripts/security/high-value-config-control-coverage.py +++ b/scripts/security/high-value-config-control-coverage.py @@ -24,15 +24,18 @@ TAIPEI = timezone(timedelta(hours=8)) CONTROL_STATUS_BY_CATEGORY = { "nginx_public_gateway": { - "coverage_status": "repo_only_drift_detector_ready", - "coverage_percent": 78, + "coverage_status": "repo_only_preflight_contract_ready_needs_owner_live_diff", + "coverage_percent": 84, "evidence_refs": [ "docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md", "docs/security/nginx-config-drift-repo.snapshot.json", "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md", + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", ], - "current_gap": "缺 owner-provided live conf 匯出與 approved maintenance window;不得 SSH 擷取或 reload。", - "next_owner_action": "提供脫敏 live conf 匯出檔或確認維護窗口、rollback owner 與 route smoke plan。", + "current_gap": "已固定 12 個 public gateway preflight gate;owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 仍全部為 0。", + "next_owner_action": "補 public gateway owner、owner-provided live conf、source-to-live rendered diff、nginx -t evidence、route smoke、maintenance window 與 rollback owner。", }, "dns_tls_certbot": { "coverage_status": "repo_only_inventory_ready", @@ -184,6 +187,15 @@ CONTROL_STATUS_BY_CATEGORY = { FALSE_BOUNDARIES = { "runtime_execution_authorized": False, "host_write_authorized": False, + "host_live_conf_read_authorized": False, + "nginx_test_authorized": False, + "public_gateway_reload_authorized": False, + "public_route_change_authorized": False, + "admin_route_change_authorized": False, + "websocket_route_change_authorized": False, + "acme_challenge_change_authorized": False, + "route_smoke_authorized": False, + "rollback_executed": False, "nginx_reload_authorized": False, "dns_tls_change_authorized": False, "certbot_renew_authorized": False, @@ -288,6 +300,7 @@ def build_report(root: Path, generated_at: str | None) -> dict[str, Any]: "policy_defined_needs_restore_drill_owner", "repo_only_inventory_ready_needs_restore_drill_owner", "repo_only_inventory_ready_needs_live_route_evidence", + "repo_only_preflight_contract_ready_needs_owner_live_diff", "policy_ready_needs_drift_evidence", "inventory_needed", "repo_only_inventory_ready_needs_live_owner_evidence", diff --git a/scripts/security/public-gateway-preflight-inventory.py b/scripts/security/public-gateway-preflight-inventory.py new file mode 100644 index 000000000..e22b3c12c --- /dev/null +++ b/scripts/security/public-gateway-preflight-inventory.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +""" +IwoooS public gateway 變更前置 Gate 只讀清冊。 + +本工具只讀取已提交的 Nginx drift snapshot 與 DNS / TLS snapshot,整理 +public gateway reload / route change 前必備的 owner、diff、nginx -t、 +route smoke、rollback 與維護窗口欄位。它不 SSH、不讀 live 主機、不執行 +nginx -t、不 reload、不做 DNS / TLS probe,也不收 secret value。 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) + +REQUIRED_PREFLIGHT_GATES = [ + { + "gate_id": "PG1", + "label": "source-of-truth hash", + "required_evidence": "repo_source_raw_and_normalized_sha256", + "owner_acceptance_required": False, + "repo_only_ready": True, + }, + { + "gate_id": "PG2", + "label": "affected route list", + "required_evidence": "domain_route_upstream_tls_admin_websocket_acme_inventory", + "owner_acceptance_required": False, + "repo_only_ready": True, + }, + { + "gate_id": "PG3", + "label": "owner response", + "required_evidence": "owner role/team, decision, reason, affected scope, redacted refs, followup owner", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG4", + "label": "owner-provided live config", + "required_evidence": "redacted live conf export or owner statement", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG5", + "label": "rendered diff", + "required_evidence": "source-to-live rendered diff with redaction", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG6", + "label": "nginx -t evidence", + "required_evidence": "syntax test output captured by approved maintainer", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG7", + "label": "public route smoke", + "required_evidence": "public route smoke plan and result for affected domains", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG8", + "label": "admin route smoke", + "required_evidence": "admin/auth route smoke when admin path is affected", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG9", + "label": "WebSocket / API smoke", + "required_evidence": "WebSocket or API smoke when upstream or upgrade header is affected", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG10", + "label": "ACME / TLS owner check", + "required_evidence": "certificate path, SAN/wildcard/shared-cert statement, ACME impact", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG11", + "label": "maintenance window", + "required_evidence": "approved window and notification scope", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, + { + "gate_id": "PG12", + "label": "rollback owner and ref", + "required_evidence": "rollback owner, rollback file/ref, post-rollback smoke", + "owner_acceptance_required": True, + "repo_only_ready": False, + }, +] + +EXECUTION_BOUNDARIES = { + "runtime_execution_authorized": False, + "host_live_conf_read_authorized": False, + "ssh_read_authorized": False, + "ssh_write_authorized": False, + "host_write_authorized": False, + "nginx_test_authorized": False, + "nginx_test_executed": False, + "nginx_reload_authorized": False, + "nginx_reload_executed": False, + "public_gateway_reload_authorized": False, + "public_route_change_authorized": False, + "admin_route_change_authorized": False, + "websocket_route_change_authorized": False, + "acme_challenge_change_authorized": False, + "dns_query_executed": False, + "live_tls_probe_executed": False, + "certbot_renew_authorized": False, + "certbot_renew_executed": False, + "route_smoke_authorized": False, + "route_smoke_executed": False, + "rollback_executed": False, + "secret_value_collection_allowed": False, + "action_buttons_allowed": False, +} + + +def git_short_sha(root: Path) -> str: + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + except Exception: + return "unknown" + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def unique_upstreams(domains: list[dict[str, Any]]) -> list[str]: + return sorted({upstream for item in domains for upstream in item.get("upstreams", [])}) + + +def build_config_rows(nginx_report: dict[str, Any]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for config in nginx_report.get("configs", []): + parsed = config.get("repo_source", {}).get("parsed", {}) + rows.append( + { + "config_id": config["config_id"], + "host": config["host"], + "role": config["role"], + "control_tier": config["control_tier"], + "owner_gate": config["owner_gate"], + "repo_source_path": config["repo_source_path"], + "live_path": config["live_path"], + "server_block_count": parsed.get("server_block_count", 0), + "server_name_count": len(parsed.get("server_names", [])), + "upstream_count": len(parsed.get("proxy_passes", [])), + "tls_certificate_path_count": len(parsed.get("ssl_certificates", [])), + "admin_route_count": len(parsed.get("admin_routes", [])), + "acme_route_count": len(parsed.get("acme_routes", [])), + "websocket_route_count": len(parsed.get("websocket_routes", [])), + "repo_source_hash_ready": True, + "owner_response_received": False, + "owner_response_accepted": False, + "live_conf_evidence_received": False, + "rendered_diff_ready": False, + "nginx_test_evidence_received": False, + "route_smoke_evidence_received": False, + "maintenance_window_accepted": False, + "rollback_owner_accepted": False, + "runtime_gate_open": False, + } + ) + return rows + + +def build_route_impacts(domains: list[dict[str, Any]]) -> list[dict[str, Any]]: + impacts: list[dict[str, Any]] = [] + for item in domains: + impacts.append( + { + "domain": item["domain"], + "hosts": item.get("hosts", []), + "config_ids": item.get("config_ids", []), + "control_tier": item.get("control_tier", "C3"), + "upstream_count": len(item.get("upstreams", [])), + "has_tls_certificate_path": item.get("tls_certificate_path_present", False), + "certificate_owner_confirmation_required": item.get( + "certificate_owner_confirmation_required", + False, + ), + "acme_challenge_present": item.get("acme_challenge_present", False), + "admin_route_count": item.get("admin_route_count", 0), + "websocket_route_count": item.get("websocket_route_count", 0), + "public_route_smoke_required": True, + "admin_route_smoke_required": item.get("admin_route_count", 0) > 0, + "websocket_or_api_smoke_required": item.get("websocket_route_count", 0) > 0 + or len(item.get("upstreams", [])) > 0, + "tls_owner_check_required": item.get("certificate_owner_confirmation_required", False), + "owner_response_accepted": False, + "route_smoke_accepted": False, + } + ) + return impacts + + +def build_report( + root: Path, + nginx_report: dict[str, Any], + domain_tls_report: dict[str, Any], + generated_at: str | None, +) -> dict[str, Any]: + report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + managed_domains = domain_tls_report.get("managed_domains", []) + config_rows = build_config_rows(nginx_report) + route_impacts = build_route_impacts(managed_domains) + upstreams = unique_upstreams(managed_domains) + repo_only_ready_count = sum(1 for gate in REQUIRED_PREFLIGHT_GATES if gate["repo_only_ready"]) + owner_required_count = sum(1 for gate in REQUIRED_PREFLIGHT_GATES if gate["owner_acceptance_required"]) + + summary = { + "source_config_count": len(config_rows), + "c0_source_config_count": sum(1 for item in config_rows if item["control_tier"] == "C0"), + "managed_domain_count": len(managed_domains), + "route_impact_count": len(route_impacts), + "unique_upstream_count": len(upstreams), + "tls_certificate_path_count": domain_tls_report.get("summary", {}).get( + "unique_certificate_path_count", + 0, + ), + "certificate_owner_confirmation_required_count": domain_tls_report.get("summary", {}).get( + "certificate_owner_confirmation_required_count", + 0, + ), + "admin_route_domain_count": domain_tls_report.get("summary", {}).get("admin_route_domain_count", 0), + "websocket_route_domain_count": domain_tls_report.get("summary", {}).get( + "websocket_route_domain_count", + 0, + ), + "acme_challenge_domain_count": domain_tls_report.get("summary", {}).get( + "acme_challenge_domain_count", + 0, + ), + "preflight_gate_count": len(REQUIRED_PREFLIGHT_GATES), + "repo_only_preflight_ready_count": repo_only_ready_count, + "owner_acceptance_required_gate_count": owner_required_count, + "preflight_gate_accepted_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "owner_provided_live_conf_received_count": 0, + "rendered_diff_ready_count": 0, + "nginx_test_evidence_count": 0, + "route_smoke_evidence_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_before_preflight": 78, + "coverage_percent_after_preflight": 84, + } + + return { + "schema_version": "public_gateway_preflight_inventory_v1", + "generated_at": report_time, + "status": "repo_only_preflight_contract_ready", + "source_scope": "committed_nginx_and_domain_tls_snapshots_only", + "git_commit": git_short_sha(root), + "source_reports": [ + "docs/security/nginx-config-drift-repo.snapshot.json", + "docs/security/domain-tls-certbot-inventory.snapshot.json", + ], + "summary": summary, + "execution_boundaries": EXECUTION_BOUNDARIES, + "required_preflight_gates": REQUIRED_PREFLIGHT_GATES, + "config_preflight_rows": config_rows, + "route_impacts": route_impacts, + "unique_upstreams": upstreams, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan", + "nginx_test_evidence_ref", + "route_smoke_evidence_ref", + ], + "next_collection_order": [ + "public_gateway_owner_response", + "owner_provided_live_conf_export", + "source_to_live_rendered_diff", + "nginx_t_evidence", + "affected_public_route_smoke", + "admin_route_smoke_if_affected", + "websocket_or_api_smoke_if_affected", + "tls_certificate_owner_confirmation", + "maintenance_window", + "rollback_owner_and_ref", + ], + "operator_interpretation": [ + "這是 Nginx public gateway 變更前置控管清冊,不是 reload 授權。", + "repo-only ready 只代表 source hash 與 affected route list 已能重跑產生。", + "owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 全部仍為 0。", + "若未來發現 live drift,只能建立 evidence 與 owner decision,不得自動覆寫 live。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS public gateway 變更前置 Gate 只讀清冊") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument( + "--nginx-report", + default="docs/security/nginx-config-drift-repo.snapshot.json", + help="Nginx repo-only drift detector snapshot", + ) + parser.add_argument( + "--domain-tls-report", + default="docs/security/domain-tls-certbot-inventory.snapshot.json", + help="DNS / TLS / certbot repo-only inventory snapshot", + ) + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report( + root, + load_json(root / args.nginx_report), + load_json(root / args.domain_tls_report), + args.generated_at, + ) + payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload + "\n", encoding="utf-8") + else: + print(payload) + + summary = report["summary"] + print( + "PUBLIC_GATEWAY_PREFLIGHT_INVENTORY_OK " + f"configs={summary['source_config_count']} " + f"routes={summary['route_impact_count']} " + f"gates={summary['preflight_gate_count']} " + f"runtime_gate={summary['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index c28949f5b..9cf32771b 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -95,6 +95,9 @@ def validate(root: Path) -> None: security_dir / "monitoring-alerting-observability-inventory.snapshot.json" ) domain_tls_inventory = load_json(security_dir / "domain-tls-certbot-inventory.snapshot.json") + public_gateway_preflight_inventory = load_json( + security_dir / "public-gateway-preflight-inventory.snapshot.json" + ) s49_request_draft = load_json(security_dir / "gitea-inventory-owner-attestation-request-draft.snapshot.json") kali_status = load_json(security_dir / "kali-integration-status.snapshot.json") iwooos_projection_page = ( @@ -2428,7 +2431,7 @@ def validate(root: Path) -> None: assert_equal( "high_value_config_coverage.summary.needs_live_evidence_count", high_value_config_coverage["summary"]["needs_live_evidence_count"], - 6, + 7, ) for key in [ "owner_response_received_count", @@ -2453,6 +2456,31 @@ def validate(root: Path) -> None: [item["category_id"] for item in high_value_config_coverage["coverage_categories"]], "agent_bounty_protocol_runtime", ) + nginx_gateway_category = next( + item + for item in high_value_config_coverage["coverage_categories"] + if item["category_id"] == "nginx_public_gateway" + ) + assert_equal( + "high_value_config_coverage.coverage_categories.nginx.coverage_percent", + nginx_gateway_category["coverage_percent"], + 84, + ) + assert_equal( + "high_value_config_coverage.coverage_categories.nginx.coverage_status", + nginx_gateway_category["coverage_status"], + "repo_only_preflight_contract_ready_needs_owner_live_diff", + ) + for evidence_ref in [ + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", + ]: + assert_contains( + "high_value_config_coverage.coverage_categories.nginx.evidence_refs", + nginx_gateway_category["evidence_refs"], + evidence_ref, + ) assert_contains( "high_value_config_coverage.lowest_coverage_categories.docker", [item["category_id"] for item in high_value_config_coverage["lowest_coverage_categories"]], @@ -3045,7 +3073,106 @@ def validate(root: Path) -> None: [item["surface_id"] for item in monitoring_alerting_observability_inventory["write_capable_surfaces"]], surface_id, ) + assert_equal( + "public_gateway_preflight_inventory.schema", + public_gateway_preflight_inventory["schema_version"], + "public_gateway_preflight_inventory_v1", + ) + assert_equal( + "public_gateway_preflight_inventory.status", + public_gateway_preflight_inventory["status"], + "repo_only_preflight_contract_ready", + ) + assert_equal( + "public_gateway_preflight_inventory.source_scope", + public_gateway_preflight_inventory["source_scope"], + "committed_nginx_and_domain_tls_snapshots_only", + ) + expected_public_gateway_preflight_summary = { + "source_config_count": 3, + "c0_source_config_count": 2, + "managed_domain_count": 14, + "route_impact_count": 14, + "unique_upstream_count": 14, + "tls_certificate_path_count": 10, + "certificate_owner_confirmation_required_count": 4, + "admin_route_domain_count": 1, + "websocket_route_domain_count": 6, + "acme_challenge_domain_count": 7, + "preflight_gate_count": 12, + "repo_only_preflight_ready_count": 2, + "owner_acceptance_required_gate_count": 10, + "preflight_gate_accepted_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "owner_provided_live_conf_received_count": 0, + "rendered_diff_ready_count": 0, + "nginx_test_evidence_count": 0, + "route_smoke_evidence_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + "coverage_percent_before_preflight": 78, + "coverage_percent_after_preflight": 84, + } + for key, expected in expected_public_gateway_preflight_summary.items(): + assert_equal( + f"public_gateway_preflight_inventory.summary.{key}", + public_gateway_preflight_inventory["summary"][key], + expected, + ) + for key, value in public_gateway_preflight_inventory["execution_boundaries"].items(): + assert_false(f"public_gateway_preflight_inventory.execution_boundaries.{key}", value) + assert_equal( + "public_gateway_preflight_inventory.required_preflight_gates.count", + len(public_gateway_preflight_inventory["required_preflight_gates"]), + 12, + ) + assert_equal( + "public_gateway_preflight_inventory.config_preflight_rows.count", + len(public_gateway_preflight_inventory["config_preflight_rows"]), + 3, + ) + assert_equal( + "public_gateway_preflight_inventory.route_impacts.count", + len(public_gateway_preflight_inventory["route_impacts"]), + 14, + ) + for row in public_gateway_preflight_inventory["config_preflight_rows"]: + assert_true( + f"public_gateway_preflight_inventory.config_preflight_rows.{row['config_id']}.repo_source_hash_ready", + row["repo_source_hash_ready"], + ) + for key in [ + "owner_response_received", + "owner_response_accepted", + "live_conf_evidence_received", + "rendered_diff_ready", + "nginx_test_evidence_received", + "route_smoke_evidence_received", + "maintenance_window_accepted", + "rollback_owner_accepted", + "runtime_gate_open", + ]: + assert_false(f"public_gateway_preflight_inventory.config_preflight_rows.{row['config_id']}.{key}", row[key]) + for route_impact in public_gateway_preflight_inventory["route_impacts"]: + assert_true( + f"public_gateway_preflight_inventory.route_impacts.{route_impact['domain']}.public_route_smoke_required", + route_impact["public_route_smoke_required"], + ) + assert_false( + f"public_gateway_preflight_inventory.route_impacts.{route_impact['domain']}.owner_response_accepted", + route_impact["owner_response_accepted"], + ) + assert_false( + f"public_gateway_preflight_inventory.route_impacts.{route_impact['domain']}.route_smoke_accepted", + route_impact["route_smoke_accepted"], + ) for source_path in [ + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", "docs/security/host-service-config-inventory.snapshot.json", "docs/security/HOST-SERVICE-CONFIG-INVENTORY.md", "docs/schemas/host_service_config_inventory_v1.schema.json", @@ -3083,7 +3210,7 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_c0_category_count": 8, "high_value_config_control_coverage_c1_category_count": 4, "high_value_config_control_coverage_average_percent": 66, - "high_value_config_control_coverage_needs_live_evidence_count": 6, + "high_value_config_control_coverage_needs_live_evidence_count": 7, "high_value_config_control_coverage_owner_response_required_count": 14, "high_value_config_control_coverage_owner_response_received_count": 0, "high_value_config_control_coverage_owner_response_accepted_count": 0, @@ -3229,6 +3356,43 @@ def validate(root: Path) -> None: iwooos_projection["summary"][key], expected, ) + expected_public_gateway_preflight_projection_summary = { + "public_gateway_preflight_first_layer": True, + "public_gateway_preflight_summary_count": 4, + "public_gateway_preflight_item_count": 6, + "public_gateway_preflight_source_config_count": 3, + "public_gateway_preflight_c0_source_config_count": 2, + "public_gateway_preflight_managed_domain_count": 14, + "public_gateway_preflight_route_impact_count": 14, + "public_gateway_preflight_unique_upstream_count": 14, + "public_gateway_preflight_tls_certificate_path_count": 10, + "public_gateway_preflight_certificate_owner_confirmation_required_count": 4, + "public_gateway_preflight_admin_route_domain_count": 1, + "public_gateway_preflight_websocket_route_domain_count": 6, + "public_gateway_preflight_acme_challenge_domain_count": 7, + "public_gateway_preflight_gate_count": 12, + "public_gateway_preflight_repo_only_ready_count": 2, + "public_gateway_preflight_owner_acceptance_required_gate_count": 10, + "public_gateway_preflight_gate_accepted_count": 0, + "public_gateway_preflight_owner_response_received_count": 0, + "public_gateway_preflight_owner_response_accepted_count": 0, + "public_gateway_preflight_owner_provided_live_conf_received_count": 0, + "public_gateway_preflight_rendered_diff_ready_count": 0, + "public_gateway_preflight_nginx_test_evidence_count": 0, + "public_gateway_preflight_route_smoke_evidence_count": 0, + "public_gateway_preflight_maintenance_window_accepted_count": 0, + "public_gateway_preflight_rollback_owner_accepted_count": 0, + "public_gateway_preflight_runtime_gate_count": 0, + "public_gateway_preflight_action_button_count": 0, + "public_gateway_preflight_coverage_percent_before_preflight": 78, + "public_gateway_preflight_coverage_percent_after_preflight": 84, + } + for key, expected in expected_public_gateway_preflight_projection_summary.items(): + assert_equal( + f"iwooos_projection.summary.{key}", + iwooos_projection["summary"][key], + expected, + ) assert_true( "iwooos_projection.summary.high_value_config_owner_packet_first_layer", iwooos_projection["summary"]["high_value_config_owner_packet_first_layer"], @@ -12371,12 +12535,17 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_c0_category_count=8", "high_value_config_control_coverage_c1_category_count=4", "high_value_config_control_coverage_average_percent=66", - "high_value_config_control_coverage_needs_live_evidence_count=6", + "high_value_config_control_coverage_needs_live_evidence_count=7", "high_value_config_control_coverage_owner_response_required_count=14", "high_value_config_control_coverage_owner_response_received_count=0", "high_value_config_control_coverage_owner_response_accepted_count=0", "high_value_config_control_coverage_runtime_gate_count=0", "high_value_config_control_coverage_action_button_count=0", + "nginx_public_gateway_coverage_percent=84", + "public_gateway_preflight_inventory_source_config_count=3", + "public_gateway_preflight_inventory_route_impact_count=14", + "public_gateway_preflight_inventory_preflight_gate_count=12", + "public_gateway_preflight_inventory_runtime_gate_count=0", "host_service_config_inventory_surface_count=9", "host_service_config_inventory_write_capable_surface_count=3", "host_service_config_inventory_runtime_gate_count=0", @@ -12413,6 +12582,15 @@ def validate(root: Path) -> None: "host_keyscan_authorized=false", "runtime_execution_authorized=false", "host_write_authorized=false", + "host_live_conf_read_authorized=false", + "nginx_test_authorized=false", + "public_gateway_reload_authorized=false", + "public_route_change_authorized=false", + "admin_route_change_authorized=false", + "websocket_route_change_authorized=false", + "acme_challenge_change_authorized=false", + "route_smoke_authorized=false", + "rollback_executed=false", "nginx_reload_authorized=false", "dns_tls_change_authorized=false", "certbot_renew_authorized=false", @@ -12461,6 +12639,127 @@ def validate(root: Path) -> None: list(web_messages_en["iwooos"].keys()), "highValueConfigControlCoverage", ) + assert_text_contains( + "iwooos_page.public_gateway_preflight_testid", + iwooos_projection_page, + 'data-testid="iwooos-public-gateway-preflight-board"', + ) + assert_text_contains( + "iwooos_page.public_gateway_preflight_boundary_testid", + iwooos_projection_page, + 'data-testid="iwooos-public-gateway-preflight-boundaries"', + ) + assert_text_contains( + "iwooos_page.public_gateway_preflight_component", + iwooos_projection_page, + "IwoooSPublicGatewayPreflightBoard", + ) + assert_text_before( + "iwooos_page.public_gateway_preflight_before_owner_packet", + iwooos_projection_page, + "", + "", + ) + for text in [ + "public_gateway_preflight_frontstage_summary_count=4", + "public_gateway_preflight_frontstage_item_count=6", + "public_gateway_preflight_source_config_count=3", + "public_gateway_preflight_c0_source_config_count=2", + "public_gateway_preflight_managed_domain_count=14", + "public_gateway_preflight_route_impact_count=14", + "public_gateway_preflight_unique_upstream_count=14", + "public_gateway_preflight_tls_certificate_path_count=10", + "public_gateway_preflight_certificate_owner_confirmation_required_count=4", + "public_gateway_preflight_admin_route_domain_count=1", + "public_gateway_preflight_websocket_route_domain_count=6", + "public_gateway_preflight_acme_challenge_domain_count=7", + "public_gateway_preflight_gate_count=12", + "public_gateway_preflight_repo_only_ready_count=2", + "public_gateway_preflight_owner_acceptance_required_gate_count=10", + "public_gateway_preflight_gate_accepted_count=0", + "public_gateway_preflight_owner_response_received_count=0", + "public_gateway_preflight_owner_response_accepted_count=0", + "public_gateway_preflight_owner_provided_live_conf_received_count=0", + "public_gateway_preflight_rendered_diff_ready_count=0", + "public_gateway_preflight_nginx_test_evidence_count=0", + "public_gateway_preflight_route_smoke_evidence_count=0", + "public_gateway_preflight_maintenance_window_accepted_count=0", + "public_gateway_preflight_rollback_owner_accepted_count=0", + "public_gateway_preflight_runtime_gate_count=0", + "public_gateway_preflight_action_button_count=0", + "public_gateway_preflight_coverage_percent_before_preflight=78", + "public_gateway_preflight_coverage_percent_after_preflight=84", + "host_live_conf_read_authorized=false", + "nginx_test_authorized=false", + "nginx_reload_authorized=false", + "public_gateway_reload_authorized=false", + "public_route_change_authorized=false", + "admin_route_change_authorized=false", + "websocket_route_change_authorized=false", + "acme_challenge_change_authorized=false", + "route_smoke_authorized=false", + "rollback_executed=false", + "secret_value_collection_allowed=false", + "action_buttons_allowed=false", + "not_authorization=true", + ]: + assert_text_contains( + "iwooos_page.public_gateway_preflight_boundary", + iwooos_projection_page, + text, + ) + assert_contains( + "web_messages.zh-TW.iwooos.publicGatewayPreflight", + list(web_messages_zh["iwooos"].keys()), + "publicGatewayPreflight", + ) + assert_contains( + "web_messages.en.iwooos.publicGatewayPreflight", + list(web_messages_en["iwooos"].keys()), + "publicGatewayPreflight", + ) + for key in [ + "eyebrow", + "title", + "subtitle", + "stateLabel", + "boundaryTitle", + "boundaryIntro", + "summary", + "items", + ]: + assert_contains( + "web_messages.zh-TW.iwooos.publicGatewayPreflight.keys", + list(web_messages_zh["iwooos"]["publicGatewayPreflight"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.publicGatewayPreflight.keys", + list(web_messages_en["iwooos"]["publicGatewayPreflight"].keys()), + key, + ) + for key in ["sourceConfigs", "routeImpacts", "preflightGates", "runtimeGate"]: + assert_contains( + "web_messages.zh-TW.iwooos.publicGatewayPreflight.summary", + list(web_messages_zh["iwooos"]["publicGatewayPreflight"]["summary"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.publicGatewayPreflight.summary", + list(web_messages_en["iwooos"]["publicGatewayPreflight"]["summary"].keys()), + key, + ) + for key in ["sourceHash", "affectedRoutes", "ownerLiveDiff", "nginxTest", "routeSmoke", "reloadBoundary"]: + assert_contains( + "web_messages.zh-TW.iwooos.publicGatewayPreflight.items", + list(web_messages_zh["iwooos"]["publicGatewayPreflight"]["items"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.publicGatewayPreflight.items", + list(web_messages_en["iwooos"]["publicGatewayPreflight"]["items"].keys()), + key, + ) for key in ["categories", "c0", "coverage", "runtimeGate"]: assert_contains( "web_messages.zh-TW.iwooos.highValueConfigControlCoverage.summary", @@ -19638,6 +19937,23 @@ def validate(root: Path) -> None: ]: assert_contains("forbidden_actions", list(forbidden_actions), action) + frontend_product_text = "\n".join( + [ + iwooos_projection_page, + json.dumps(web_messages_zh["iwooos"], ensure_ascii=False), + json.dumps(web_messages_en["iwooos"], ensure_ascii=False), + ] + ) + for forbidden in [ + "工作視窗", + "內部對話", + "對話內容", + "批准!繼續", + "In app browser", + "My request for Codex", + ]: + assert_text_not_contains("iwooos_frontend_product_text.internal_conversation", frontend_product_text, forbidden) + def main() -> None: parser = argparse.ArgumentParser(description=__doc__)