diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 0af22a6b6..e5b933e08 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14605,6 +14605,55 @@ "body": "不開 Nginx reload、ArgoCD sync、kubectl、workflow 修改、agent-bounty runtime、payout 或主機操作。" } } + }, + "domainTlsCertbotInventory": { + "eyebrow": "DNS / TLS / certbot 清冊", + "title": "公開入口憑證與 ACME 關係已納入只讀控管", + "subtitle": "此清冊只從 Nginx source-of-truth 推導 domain、憑證路徑、ACME、admin route 與 WebSocket 影響面;目前不做 DNS 查詢、TLS probe、certbot renew 或 Nginx reload。", + "checkLabel": "檢核", + "stateLabel": "狀態", + "boundaryTitle": "DNS / TLS 只讀邊界", + "boundaryIntro": "以下鍵值固定:這張卡只顯示 repo-only 清冊與 owner 確認缺口,不代表 DNS 修改、TLS renew、Nginx reload、live probe 或主機操作已授權。", + "summary": { + "domains": { + "label": "Domain", + "detail": "從三份 Nginx source config 盤到 14 個 domain。" + }, + "certPaths": { + "label": "憑證路徑", + "detail": "目前 repo-only 清冊包含 10 條 fullchain path。" + }, + "ownerConfirm": { + "label": "待確認", + "detail": "4 個 certificate path 關係需 owner 確認 SAN 或共用憑證。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "仍為 0,不產生 renew 或 reload 入口。" + } + }, + "items": { + "repoSource": { + "title": "來源固定為 repo-only", + "body": "清冊來源是 Nginx drift repo snapshot;沒有 SSH、host read、live hash 或私鑰內容。" + }, + "domainMap": { + "title": "公開 domain 關係已成表", + "body": "每個 domain 都保留 host、config id、source path、listen、upstream 與控制等級。" + }, + "acmeRoutes": { + "title": "ACME challenge 已標記", + "body": "7 個 domain 帶有 ACME challenge route,可供後續 owner review 與 smoke plan 使用。" + }, + "ownerConfirmation": { + "title": "憑證路徑需 owner 確認", + "body": "gitea、langfuse、signoz 與 tsenyang.com 的憑證路徑關係需要 owner 補 SAN / wildcard / 共用憑證證據。" + }, + "runtimeBoundary": { + "title": "不做 live 變更", + "body": "不 DNS 查詢、不 TLS probe、不 certbot renew、不 Nginx reload、不主機寫入,也不提高 IwoooS 進度。" + } + } } } } diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 0af22a6b6..e5b933e08 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14605,6 +14605,55 @@ "body": "不開 Nginx reload、ArgoCD sync、kubectl、workflow 修改、agent-bounty runtime、payout 或主機操作。" } } + }, + "domainTlsCertbotInventory": { + "eyebrow": "DNS / TLS / certbot 清冊", + "title": "公開入口憑證與 ACME 關係已納入只讀控管", + "subtitle": "此清冊只從 Nginx source-of-truth 推導 domain、憑證路徑、ACME、admin route 與 WebSocket 影響面;目前不做 DNS 查詢、TLS probe、certbot renew 或 Nginx reload。", + "checkLabel": "檢核", + "stateLabel": "狀態", + "boundaryTitle": "DNS / TLS 只讀邊界", + "boundaryIntro": "以下鍵值固定:這張卡只顯示 repo-only 清冊與 owner 確認缺口,不代表 DNS 修改、TLS renew、Nginx reload、live probe 或主機操作已授權。", + "summary": { + "domains": { + "label": "Domain", + "detail": "從三份 Nginx source config 盤到 14 個 domain。" + }, + "certPaths": { + "label": "憑證路徑", + "detail": "目前 repo-only 清冊包含 10 條 fullchain path。" + }, + "ownerConfirm": { + "label": "待確認", + "detail": "4 個 certificate path 關係需 owner 確認 SAN 或共用憑證。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "仍為 0,不產生 renew 或 reload 入口。" + } + }, + "items": { + "repoSource": { + "title": "來源固定為 repo-only", + "body": "清冊來源是 Nginx drift repo snapshot;沒有 SSH、host read、live hash 或私鑰內容。" + }, + "domainMap": { + "title": "公開 domain 關係已成表", + "body": "每個 domain 都保留 host、config id、source path、listen、upstream 與控制等級。" + }, + "acmeRoutes": { + "title": "ACME challenge 已標記", + "body": "7 個 domain 帶有 ACME challenge route,可供後續 owner review 與 smoke plan 使用。" + }, + "ownerConfirmation": { + "title": "憑證路徑需 owner 確認", + "body": "gitea、langfuse、signoz 與 tsenyang.com 的憑證路徑關係需要 owner 補 SAN / wildcard / 共用憑證證據。" + }, + "runtimeBoundary": { + "title": "不做 live 變更", + "body": "不 DNS 查詢、不 TLS probe、不 certbot renew、不 Nginx reload、不主機寫入,也不提高 IwoooS 進度。" + } + } } } } diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index a8a6d8458..8e77cf9c3 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -271,6 +271,14 @@ type HighValueConfigOwnerPacketItem = { tone: 'steady' | 'warn' | 'locked' } +type DomainTlsCertbotInventoryItem = { + key: string + check: string + state: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type IwoooSCommandMapItem = { key: IwoooSCommandMapMode metric: string @@ -2082,6 +2090,47 @@ const highValueConfigOwnerPacketBoundaries = [ 'agent_bounty_runtime_authorized=false', ] as const +const domainTlsCertbotInventorySummary = [ + { key: 'domains', value: '14', icon: Route, tone: 'steady' }, + { key: 'certPaths', value: '10', icon: ShieldCheck, tone: 'steady' }, + { key: 'ownerConfirm', value: '4', icon: SearchCheck, tone: 'warn' }, + { key: 'runtimeGate', value: '0', icon: Lock, tone: 'locked' }, +] as const + +const domainTlsCertbotInventoryItems: DomainTlsCertbotInventoryItem[] = [ + { key: 'repoSource', check: 'DT1', state: '3 來源', icon: FileText, tone: 'steady' }, + { key: 'domainMap', check: 'DT2', state: '14 domain', icon: Route, tone: 'steady' }, + { key: 'acmeRoutes', check: 'DT3', state: '7 domain', icon: Network, tone: 'steady' }, + { key: 'ownerConfirmation', check: 'DT4', state: '4 待確認', icon: SearchCheck, tone: 'warn' }, + { key: 'runtimeBoundary', check: 'DT5', state: '未開閘', icon: Lock, tone: 'locked' }, +] + +const domainTlsCertbotInventoryBoundaries = [ + 'domain_tls_certbot_inventory_frontstage_summary_count=4', + 'domain_tls_certbot_inventory_frontstage_item_count=5', + 'domain_tls_certbot_inventory_source_config_count=3', + 'domain_tls_certbot_inventory_managed_domain_count=14', + 'domain_tls_certbot_inventory_unique_certificate_path_count=10', + 'domain_tls_certbot_inventory_acme_challenge_domain_count=7', + 'domain_tls_certbot_inventory_owner_confirmation_required_count=4', + 'domain_tls_certbot_inventory_admin_route_domain_count=1', + 'domain_tls_certbot_inventory_websocket_route_domain_count=6', + 'domain_tls_certbot_inventory_request_sent_count=0', + 'domain_tls_certbot_inventory_received_response_count=0', + 'domain_tls_certbot_inventory_accepted_response_count=0', + 'domain_tls_certbot_inventory_runtime_gate_count=0', + 'domain_tls_certbot_inventory_live_tls_probe_executed=false', + 'domain_tls_certbot_inventory_dns_change_executed=false', + 'domain_tls_certbot_inventory_certbot_renew_executed=false', + 'domain_tls_certbot_inventory_nginx_reload_executed=false', + 'domain_tls_certbot_inventory_action_buttons_allowed=false', + 'runtime_execution_authorized=false', + 'active_runtime_gate_count=0', + 'action_buttons_allowed=false', + 'not_authorization=true', + 'secret_value_collection_allowed=false', +] as const + const iwooosThreeAxisProductProgressSummary = [ { key: 'headline', value: '64%', icon: Activity, tone: 'warn' }, { key: 'framework', value: '92%', icon: ShieldCheck, tone: 'steady' }, @@ -6622,6 +6671,134 @@ function IwoooSHighValueConfigOwnerPacketBoard() { ) } +function IwoooSDomainTlsCertbotInventoryBoard() { + const t = useTranslations('iwooos.domainTlsCertbotInventory') + const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + + return ( +
+
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+

+ {t('subtitle')} +

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

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

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

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

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

+ {t('boundaryIntro')} +

+
+ {domainTlsCertbotInventoryBoundaries.map(item => ( + + {item} + + ))} +
+
+
+
+ ) +} + function ProgressAccelerationLaneCard({ item }: { item: ProgressAccelerationLane }) { const t = useTranslations('iwooos.progressAcceleration') const Icon = item.icon @@ -18506,6 +18683,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + 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 cert_domain(path: str) -> str | None: + match = CERT_DOMAIN_RE.search(path) + if not match: + return None + return match.group(1) + + +def cert_domain_matches(domain: str, candidate: str) -> bool: + if candidate == domain: + return True + if candidate.startswith("*.") and domain.endswith(candidate[1:]): + return True + return False + + +def strongest_tier(tiers: set[str]) -> str: + if not tiers: + return "C3" + return sorted(tiers, key=lambda tier: TIER_ORDER.get(tier, 99))[0] + + +def ensure_domain(domains: dict[str, dict[str, Any]], domain: str) -> dict[str, Any]: + if domain not in domains: + domains[domain] = { + "domain": domain, + "hosts": set(), + "config_ids": set(), + "source_paths": set(), + "live_paths": set(), + "control_tiers": set(), + "server_block_indexes": set(), + "listens": set(), + "certificate_paths": set(), + "certificate_key_paths": set(), + "certificate_path_domains": set(), + "acme_challenge_roots": set(), + "upstreams": set(), + "admin_route_count": 0, + "websocket_route_count": 0, + } + return domains[domain] + + +def add_server_to_domain( + domain_item: dict[str, Any], + config: dict[str, Any], + server: dict[str, Any], +) -> None: + domain_item["hosts"].add(config["host"]) + domain_item["config_ids"].add(config["config_id"]) + domain_item["source_paths"].add(config["repo_source_path"]) + domain_item["live_paths"].add(config["live_path"]) + domain_item["control_tiers"].add(config["control_tier"]) + domain_item["server_block_indexes"].add(f"{config['config_id']}#{server['index']}") + domain_item["listens"].update(server.get("listens", [])) + domain_item["certificate_paths"].update(server.get("ssl_certificates", [])) + domain_item["certificate_key_paths"].update(server.get("ssl_certificate_keys", [])) + domain_item["upstreams"].update(server.get("proxy_passes", [])) + + for path in server.get("ssl_certificates", []): + candidate = cert_domain(path) + if candidate: + domain_item["certificate_path_domains"].add(candidate) + + for location in server.get("locations", []): + path = str(location.get("path", "")) + if ".well-known/acme-challenge" in path: + domain_item["acme_challenge_roots"].update(location.get("roots", [])) + if "/admin" in path: + domain_item["admin_route_count"] += 1 + if location.get("websocket_upgrade"): + domain_item["websocket_route_count"] += 1 + + +def normalize_domain_item(item: dict[str, Any]) -> dict[str, Any]: + domain = item["domain"] + cert_domains = sorted(item["certificate_path_domains"]) + certificate_owner_confirmation_required = bool(cert_domains) and not any( + cert_domain_matches(domain, candidate) for candidate in cert_domains + ) + tls_certificate_path_present = bool(item["certificate_paths"]) + if not tls_certificate_path_present: + status = "repo_only_tls_path_missing" + elif certificate_owner_confirmation_required: + status = "repo_only_owner_confirmation_required" + else: + status = "repo_only_ready_for_owner_review" + + return { + "domain": domain, + "hosts": sorted(item["hosts"]), + "config_ids": sorted(item["config_ids"]), + "source_paths": sorted(item["source_paths"]), + "live_paths": sorted(item["live_paths"]), + "control_tier": strongest_tier(item["control_tiers"]), + "server_block_refs": sorted(item["server_block_indexes"]), + "listens": sorted(item["listens"]), + "tls_certificate_path_present": tls_certificate_path_present, + "certificate_paths": sorted(item["certificate_paths"]), + "certificate_key_paths": sorted(item["certificate_key_paths"]), + "certificate_path_domains": cert_domains, + "certificate_owner_confirmation_required": certificate_owner_confirmation_required, + "acme_challenge_present": bool(item["acme_challenge_roots"]), + "acme_challenge_roots": sorted(item["acme_challenge_roots"]), + "upstreams": sorted(item["upstreams"]), + "admin_route_count": item["admin_route_count"], + "websocket_route_count": item["websocket_route_count"], + "live_tls_probe_status": "not_executed", + "dns_resolution_status": "not_executed", + "certbot_renewal_status": "not_executed", + "owner_review_status": status, + } + + +def build_report(root: Path, nginx_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]: + domains: dict[str, dict[str, Any]] = {} + for config in nginx_report.get("configs", []): + parsed = config.get("repo_source", {}).get("parsed", {}) + for server in parsed.get("servers", []): + for domain in server.get("server_names", []): + if not domain or domain == "_": + continue + item = ensure_domain(domains, domain) + add_server_to_domain(item, config, server) + + managed_domains = [normalize_domain_item(item) for item in domains.values()] + managed_domains.sort(key=lambda item: item["domain"]) + certificate_paths = sorted( + { + path + for item in managed_domains + for path in item.get("certificate_paths", []) + } + ) + owner_confirmation_required = [ + item + for item in managed_domains + if item["certificate_owner_confirmation_required"] or not item["tls_certificate_path_present"] + ] + acme_domains = [item for item in managed_domains if item["acme_challenge_present"]] + admin_domains = [item for item in managed_domains if item["admin_route_count"] > 0] + websocket_domains = [item for item in managed_domains if item["websocket_route_count"] > 0] + + return { + "schema_version": "domain_tls_certbot_inventory_v1", + "generated_at": generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds"), + "mode": "repo_only_from_nginx_source_of_truth", + "git_commit": git_short_sha(root), + "source_nginx_report": "docs/security/nginx-config-drift-repo.snapshot.json", + "execution_boundaries": { + "dns_query_executed": False, + "live_tls_probe_executed": False, + "certbot_renew_executed": False, + "nginx_reload_executed": False, + "host_write_executed": False, + "runtime_gate_opened": False, + "secret_value_collected": False, + "action_buttons_allowed": False, + }, + "summary": { + "source_config_count": nginx_report.get("summary", {}).get("source_config_count", 0), + "managed_domain_count": len(managed_domains), + "unique_certificate_path_count": len(certificate_paths), + "acme_challenge_domain_count": len(acme_domains), + "certificate_owner_confirmation_required_count": len(owner_confirmation_required), + "admin_route_domain_count": len(admin_domains), + "websocket_route_domain_count": len(websocket_domains), + "owner_response_request_sent_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + "live_tls_probe_executed": False, + "dns_change_executed": False, + "certbot_renew_executed": False, + "nginx_reload_executed": False, + "action_buttons_allowed": False, + }, + "certificate_paths": certificate_paths, + "managed_domains": managed_domains, + "owner_confirmation_required_domains": [ + { + "domain": item["domain"], + "certificate_path_domains": item["certificate_path_domains"], + "tls_certificate_path_present": item["tls_certificate_path_present"], + "owner_review_status": item["owner_review_status"], + } + for item in owner_confirmation_required + ], + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan", + ], + "next_steps": [ + "請 owner 確認 certificate path 是否由 SAN 或 wildcard 合法覆蓋;未確認前不得 renew 或 reload。", + "未來若要做 live TLS / DNS probe,需另行 scope approval;本清冊只保留 repo-only 證據。", + "任何 certbot renew、Nginx reload 或 DNS 變更都必須另開維護窗口、rollback owner 與 post-check。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS DNS / TLS / certbot 只讀清冊") + 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("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + nginx_report_path = root / args.nginx_report + nginx_report = load_json(nginx_report_path) + report = build_report(root, nginx_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( + "DOMAIN_TLS_CERTBOT_INVENTORY_OK " + f"domains={summary['managed_domain_count']} " + f"cert_paths={summary['unique_certificate_path_count']} " + f"owner_confirm={summary['certificate_owner_confirmation_required_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 2324c9904..3125df6ab 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -87,6 +87,7 @@ def validate(root: Path) -> None: primary_gate = load_json(security_dir / "source-control-primary-readiness-gate.snapshot.json") rollout_policy = load_json(security_dir / "security-rollout-policy.snapshot.json") iwooos_projection = load_json(security_dir / "iwooos-posture-projection.snapshot.json") + domain_tls_inventory = load_json(security_dir / "domain-tls-certbot-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 = ( @@ -11579,6 +11580,150 @@ def validate(root: Path) -> None: list(web_messages_en["iwooos"]["highValueConfigOwnerPacket"]["items"].keys()), key, ) + domain_tls_summary = domain_tls_inventory["summary"] + assert_equal("domain_tls_inventory.schema", domain_tls_inventory["schema_version"], "domain_tls_certbot_inventory_v1") + assert_equal("domain_tls_inventory.summary.source_config_count", domain_tls_summary["source_config_count"], 3) + assert_equal("domain_tls_inventory.summary.managed_domain_count", domain_tls_summary["managed_domain_count"], 14) + assert_equal( + "domain_tls_inventory.summary.unique_certificate_path_count", + domain_tls_summary["unique_certificate_path_count"], + 10, + ) + assert_equal( + "domain_tls_inventory.summary.acme_challenge_domain_count", + domain_tls_summary["acme_challenge_domain_count"], + 7, + ) + assert_equal( + "domain_tls_inventory.summary.certificate_owner_confirmation_required_count", + domain_tls_summary["certificate_owner_confirmation_required_count"], + 4, + ) + assert_equal("domain_tls_inventory.summary.runtime_gate_count", domain_tls_summary["runtime_gate_count"], 0) + assert_false("domain_tls_inventory.summary.live_tls_probe_executed", domain_tls_summary["live_tls_probe_executed"]) + assert_false("domain_tls_inventory.summary.dns_change_executed", domain_tls_summary["dns_change_executed"]) + assert_false("domain_tls_inventory.summary.certbot_renew_executed", domain_tls_summary["certbot_renew_executed"]) + assert_false("domain_tls_inventory.summary.nginx_reload_executed", domain_tls_summary["nginx_reload_executed"]) + assert_false("domain_tls_inventory.summary.action_buttons_allowed", domain_tls_summary["action_buttons_allowed"]) + assert_equal( + "iwooos_projection.summary.domain_tls_certbot_inventory_managed_domain_count", + iwooos_projection["summary"]["domain_tls_certbot_inventory_managed_domain_count"], + 14, + ) + assert_equal( + "iwooos_projection.summary.domain_tls_certbot_inventory_owner_confirmation_required_count", + iwooos_projection["summary"]["domain_tls_certbot_inventory_owner_confirmation_required_count"], + 4, + ) + assert_equal( + "iwooos_projection.summary.domain_tls_certbot_inventory_runtime_gate_count", + iwooos_projection["summary"]["domain_tls_certbot_inventory_runtime_gate_count"], + 0, + ) + for source_path in [ + "docs/security/domain-tls-certbot-inventory.snapshot.json", + "docs/security/DOMAIN-TLS-CERTBOT-INVENTORY.md", + "docs/schemas/domain_tls_certbot_inventory_v1.schema.json", + ]: + assert_contains("iwooos_projection.source_paths", iwooos_projection["source_paths"], source_path) + assert_text_contains( + "iwooos_page.domain_tls_certbot_inventory_testid", + iwooos_projection_page, + 'data-testid="iwooos-domain-tls-certbot-inventory-board"', + ) + assert_text_contains( + "iwooos_page.domain_tls_certbot_inventory_boundary_testid", + iwooos_projection_page, + 'data-testid="iwooos-domain-tls-certbot-inventory-boundaries"', + ) + assert_text_contains( + "iwooos_page.domain_tls_certbot_inventory_component", + iwooos_projection_page, + "IwoooSDomainTlsCertbotInventoryBoard", + ) + for text in [ + "domain_tls_certbot_inventory_frontstage_summary_count=4", + "domain_tls_certbot_inventory_frontstage_item_count=5", + "domain_tls_certbot_inventory_source_config_count=3", + "domain_tls_certbot_inventory_managed_domain_count=14", + "domain_tls_certbot_inventory_unique_certificate_path_count=10", + "domain_tls_certbot_inventory_acme_challenge_domain_count=7", + "domain_tls_certbot_inventory_owner_confirmation_required_count=4", + "domain_tls_certbot_inventory_admin_route_domain_count=1", + "domain_tls_certbot_inventory_websocket_route_domain_count=6", + "domain_tls_certbot_inventory_request_sent_count=0", + "domain_tls_certbot_inventory_received_response_count=0", + "domain_tls_certbot_inventory_accepted_response_count=0", + "domain_tls_certbot_inventory_runtime_gate_count=0", + "domain_tls_certbot_inventory_live_tls_probe_executed=false", + "domain_tls_certbot_inventory_dns_change_executed=false", + "domain_tls_certbot_inventory_certbot_renew_executed=false", + "domain_tls_certbot_inventory_nginx_reload_executed=false", + "domain_tls_certbot_inventory_action_buttons_allowed=false", + "runtime_execution_authorized=false", + "active_runtime_gate_count=0", + "action_buttons_allowed=false", + "not_authorization=true", + "secret_value_collection_allowed=false", + ]: + assert_text_contains( + "iwooos_page.domain_tls_certbot_inventory_boundary", + iwooos_projection_page, + text, + ) + assert_contains( + "web_messages.zh-TW.iwooos.domainTlsCertbotInventory", + list(web_messages_zh["iwooos"].keys()), + "domainTlsCertbotInventory", + ) + assert_contains( + "web_messages.en.iwooos.domainTlsCertbotInventory", + list(web_messages_en["iwooos"].keys()), + "domainTlsCertbotInventory", + ) + for key in [ + "eyebrow", + "title", + "subtitle", + "checkLabel", + "stateLabel", + "boundaryTitle", + "boundaryIntro", + "summary", + "items", + ]: + assert_contains( + "web_messages.zh-TW.iwooos.domainTlsCertbotInventory.keys", + list(web_messages_zh["iwooos"]["domainTlsCertbotInventory"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.domainTlsCertbotInventory.keys", + list(web_messages_en["iwooos"]["domainTlsCertbotInventory"].keys()), + key, + ) + for key in ["domains", "certPaths", "ownerConfirm", "runtimeGate"]: + assert_contains( + "web_messages.zh-TW.iwooos.domainTlsCertbotInventory.summary", + list(web_messages_zh["iwooos"]["domainTlsCertbotInventory"]["summary"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.domainTlsCertbotInventory.summary", + list(web_messages_en["iwooos"]["domainTlsCertbotInventory"]["summary"].keys()), + key, + ) + for key in ["repoSource", "domainMap", "acmeRoutes", "ownerConfirmation", "runtimeBoundary"]: + assert_contains( + "web_messages.zh-TW.iwooos.domainTlsCertbotInventory.items", + list(web_messages_zh["iwooos"]["domainTlsCertbotInventory"]["items"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.domainTlsCertbotInventory.items", + list(web_messages_en["iwooos"]["domainTlsCertbotInventory"]["items"].keys()), + key, + ) assert_text_contains( "iwooos_page.three_axis_product_progress_testid", iwooos_projection_page,