diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index fce278e2..f36feacc 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -260,6 +260,8 @@ jobs: ;; apps/api/src/services/gitea_authenticated_inventory_payload_validation.py) ;; + apps/api/src/services/gitea_owner_coverage_attestation_validation.py) + ;; apps/api/src/services/gitea_private_inventory_p0_scorecard.py) ;; apps/api/src/services/reboot_auto_recovery_slo_scorecard.py) @@ -495,6 +497,7 @@ jobs: src/services/heartbeat_report_service.py \ src/services/credential_escrow_evidence_intake_readiness.py \ src/services/gitea_authenticated_inventory_payload_validation.py \ + src/services/gitea_owner_coverage_attestation_validation.py \ src/services/gitea_private_inventory_p0_scorecard.py \ src/services/reboot_auto_recovery_slo_scorecard.py \ src/services/iwooos_security_operating_system.py \ diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 2376a819..01f69742 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -341,6 +341,9 @@ from src.services.docker_build_surface_inventory import ( from src.services.gitea_authenticated_inventory_payload_validation import ( validate_gitea_authenticated_inventory_payload, ) +from src.services.gitea_owner_coverage_attestation_validation import ( + validate_gitea_owner_coverage_attestation, +) from src.services.gitea_private_inventory_p0_scorecard import ( load_latest_gitea_private_inventory_p0_scorecard, ) @@ -1147,6 +1150,42 @@ async def validate_gitea_authenticated_inventory_payload_packet( ) from exc +@router.post( + "/gitea-private-inventory-p0-scorecard/validate-owner-coverage-attestation", + response_model=dict[str, Any], + summary="驗證 P0-003 Gitea owner coverage attestation 脫敏回覆", + description=( + "針對單次 owner-provided redacted Gitea coverage attestation response " + "packet 進行 no-persist validation;此端點只回傳 owner attestation " + "reviewer readiness / needs supplement / quarantined / rejected execution " + "分流,不保存 payload、不呼叫 Gitea/GitHub、不收 token value、" + "不建立 repo、不改 visibility、不同步 refs、不觸發 workflow、" + "不讀 secret、不讀 raw session/SQLite。" + ), +) +async def validate_gitea_owner_coverage_attestation_packet( + owner_response: dict[str, Any], +) -> dict[str, Any]: + """Return no-persist validation for one P0-003 owner attestation packet.""" + try: + payload = await asyncio.to_thread( + validate_gitea_owner_coverage_attestation, + owner_response, + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("gitea_owner_coverage_attestation_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="P0-003 Gitea owner coverage attestation 驗證器無效", + ) from exc + + @router.get( "/github-target-private-backup-evidence-gate", response_model=dict[str, Any], diff --git a/apps/api/src/services/gitea_owner_coverage_attestation_validation.py b/apps/api/src/services/gitea_owner_coverage_attestation_validation.py new file mode 100644 index 00000000..7574acbf --- /dev/null +++ b/apps/api/src/services/gitea_owner_coverage_attestation_validation.py @@ -0,0 +1,388 @@ +"""P0-003 Gitea owner coverage attestation no-persist validation. + +This service validates one owner-provided redacted coverage attestation packet. +It loads the committed response templates, accepts only redacted metadata, and +never writes repos, refs, secrets, workflow state, or runtime state. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any +from urllib.parse import parse_qsl, urlsplit + +from src.services.gitea_private_inventory_p0_scorecard import ( + load_latest_gitea_private_inventory_p0_scorecard, +) +from src.services.snapshot_paths import default_security_dir + +_SCHEMA_VERSION = "gitea_owner_coverage_attestation_validation_v1" +_OWNER_RESPONSE_SCHEMA_VERSION = "gitea_inventory_owner_attestation_response_v1" +_OWNER_RESPONSE_PACKET = "gitea-inventory-owner-attestation-response.snapshot.json" +_LANE_ID = "s4_9_gitea_inventory_owner_attestation_response" +_BLOCKERS_CLEARED_BY_ACCEPTED_ATTESTATION = { + "gitea_owner_coverage_attestation_not_received", +} +_BLOCKERS_CLEARED_AFTER_INVENTORY_RECEIPT_WRITEBACK = { + "gitea_repo_inventory_status_not_ok", + "gitea_visibility_scope_public_only_or_unknown", + "gitea_authenticated_inventory_payload_not_accepted", + "gitea_owner_coverage_attestation_not_received", +} +_FORBIDDEN_TRUE_FIELDS = { + "action_buttons_allowed", + "change_repo_visibility", + "create_gitea_repo", + "create_github_repo", + "delete_or_archive_gitea_repo", + "execution_authorized", + "force_push", + "gitea_repo_write_authorized", + "github_primary_switch_authorized", + "refs_sync_allowed", + "refs_sync_authorized", + "repo_write_allowed", + "runtime_action_requested", + "runtime_execution_authorized", + "secret_value_collection_allowed", + "sync_git_refs", + "token_value_collection_allowed", + "workflow_dispatch_requested", + "write_to_gitea", +} +_SECRET_PATTERNS = { + "authorization_header": re.compile(r"Authorization\s*:", re.IGNORECASE), + "bearer_token": re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{12,}", re.IGNORECASE), + "cookie_header": re.compile(r"\bCookie\s*:", re.IGNORECASE), + "password_assignment": re.compile(r"\bpassword\s*[:=]\s*[^,\s]+", re.IGNORECASE), + "private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), + "token_assignment": re.compile(r"\btoken\s*[:=]\s*[^,\s]+", re.IGNORECASE), +} +_SECRET_QUERY_KEYS = {"access_token", "auth", "key", "password", "secret", "token"} + + +def validate_gitea_owner_coverage_attestation( + owner_response: dict[str, Any], + scorecard: dict[str, Any] | None = None, + security_dir: Path | None = None, +) -> dict[str, Any]: + """Validate one redacted P0-003 owner attestation packet without persisting it.""" + current = scorecard or load_latest_gitea_private_inventory_p0_scorecard() + templates = _load_response_templates(security_dir) + template_by_item = { + str(template.get("attestation_item_id")): template for template in templates + } + required_item_ids = set(template_by_item) + blockers: list[str] = [] + sensitive_hits = _find_sensitive_strings(owner_response) + forbidden_true_fields = _find_forbidden_true_fields(owner_response) + + if owner_response.get("schema_version") != _OWNER_RESPONSE_SCHEMA_VERSION: + blockers.append(f"schema_version_not_{_OWNER_RESPONSE_SCHEMA_VERSION}") + if str(owner_response.get("lane_id") or "") != _LANE_ID: + blockers.append("lane_id_not_s4_9_owner_attestation_response") + + responses = _response_items(owner_response) + response_by_item: dict[str, dict[str, Any]] = {} + duplicate_item_ids: list[str] = [] + for index, response in enumerate(responses): + item_id = str(response.get("attestation_item_id") or "") + if not item_id: + blockers.append(f"responses[{index}].attestation_item_id_missing") + continue + if item_id in response_by_item: + duplicate_item_ids.append(item_id) + response_by_item[item_id] = response + + unknown_item_ids = sorted(set(response_by_item) - required_item_ids) + missing_item_ids = sorted(required_item_ids - set(response_by_item)) + blockers.extend(f"unknown_attestation_item_id:{item_id}" for item_id in unknown_item_ids) + blockers.extend(f"missing_attestation_item_id:{item_id}" for item_id in missing_item_ids) + blockers.extend(f"duplicate_attestation_item_id:{item_id}" for item_id in duplicate_item_ids) + + for item_id in sorted(required_item_ids & set(response_by_item)): + blockers.extend( + _validate_response_item( + item_id=item_id, + response=response_by_item[item_id], + template=template_by_item[item_id], + ) + ) + + if forbidden_true_fields: + status = "rejected_execution_request" + elif sensitive_hits: + status = "quarantined_sensitive_payload" + elif blockers: + status = "needs_supplement" + else: + status = "accepted_for_owner_coverage_attestation_review_only" + + accepted = status == "accepted_for_owner_coverage_attestation_review_only" + current_rollups = _as_dict(current.get("rollups")) + current_blockers = _strings(current.get("active_blockers")) + projected_blockers = ( + [ + blocker + for blocker in current_blockers + if blocker not in _BLOCKERS_CLEARED_BY_ACCEPTED_ATTESTATION + ] + if accepted + else current_blockers + ) + projected_after_inventory_receipt = ( + [ + blocker + for blocker in current_blockers + if blocker not in _BLOCKERS_CLEARED_AFTER_INVENTORY_RECEIPT_WRITEBACK + ] + if accepted + else current_blockers + ) + current_accepted_attestation_count = _as_int( + current_rollups.get("owner_coverage_attestation_accepted_count") + ) + projected_accepted_attestation_count = ( + max(current_accepted_attestation_count, 1) + if accepted + else current_accepted_attestation_count + ) + + return { + "schema_version": _SCHEMA_VERSION, + "status": status, + "priority": "P0-003", + "scope": "gitea_owner_coverage_attestation_validation", + "source_contract": _OWNER_RESPONSE_SCHEMA_VERSION, + "source_scorecard_status": current.get("status"), + "result": { + "accepted_attestation_packet_count": 1 if accepted else 0, + "required_response_item_count": len(required_item_ids), + "provided_response_item_count": len(response_by_item), + "accepted_response_count": len(required_item_ids) if accepted else 0, + "blocker_count": len(blockers), + "sensitive_payload_hit_count": len(sensitive_hits), + "forbidden_true_field_count": len(forbidden_true_fields), + "current_active_blocker_count": len(current_blockers), + "projected_active_blocker_count": len(projected_blockers), + "projected_active_blocker_count_after_redacted_inventory_receipt_writeback": ( + len(projected_after_inventory_receipt) + ), + "current_owner_coverage_attestation_accepted_count": ( + current_accepted_attestation_count + ), + "projected_owner_coverage_attestation_accepted_count": ( + projected_accepted_attestation_count + ), + "token_value_collection_allowed": False, + "secret_value_collection_allowed": False, + "repo_write_allowed": False, + "refs_sync_allowed": False, + "github_primary_switch_authorized": False, + "runtime_gate_count": 0, + }, + "blockers": blockers, + "missing_attestation_item_ids": missing_item_ids, + "unknown_attestation_item_ids": unknown_item_ids, + "duplicate_attestation_item_ids": sorted(duplicate_item_ids), + "sensitive_payload_hits": sensitive_hits, + "forbidden_true_fields": forbidden_true_fields, + "operation_boundaries": { + "payload_persisted": False, + "gitea_api_called": False, + "gitea_write_performed": False, + "repo_write_performed": False, + "refs_sync_performed": False, + "github_api_used": False, + "github_cli_used": False, + "secret_plaintext_read": False, + "token_value_collection_allowed": False, + "secret_value_collection_allowed": False, + "runtime_action_performed": False, + "raw_session_or_sqlite_read_performed": False, + }, + "reviewer_readiness": { + "schema_version": "gitea_owner_coverage_attestation_reviewer_readiness_v1", + "status": ( + "ready_for_private_inventory_closeout_after_inventory_receipt_writeback" + if accepted + else "not_ready_for_private_inventory_closeout" + ), + "redacted_owner_attestation_receipt_writeback_ready_count": ( + 1 if accepted else 0 + ), + "accepted_response_count": len(required_item_ids) if accepted else 0, + "projected_active_blocker_count": len(projected_blockers), + "projected_remaining_blockers": projected_blockers, + "projected_active_blocker_count_after_redacted_inventory_receipt_writeback": ( + len(projected_after_inventory_receipt) + ), + "projected_remaining_blockers_after_redacted_inventory_receipt_writeback": ( + projected_after_inventory_receipt + ), + "repo_write_authorized_count": 0, + "refs_sync_authorized_count": 0, + "github_primary_switch_authorized_count": 0, + "runtime_gate_count": 0, + "token_value_collection_allowed": False, + "secret_value_collection_allowed": False, + "payload_persisted": False, + "safe_next_step": ( + "write_redacted_inventory_receipt_and_owner_attestation_receipt_then_close_p0_003" + if accepted + else "supplement_owner_coverage_attestation_redacted_metadata" + ), + "blocked_operations": [ + "store_token_value", + "store_raw_secret", + "gitea_api_write", + "repo_write", + "refs_sync", + "github_api", + "github_primary_switch", + "workflow_dispatch", + "runtime_action", + "raw_session_or_sqlite_read", + ], + }, + "safe_next_step": ( + "review_redacted_owner_attestation_then_pair_with_inventory_receipt_writeback" + if accepted + else "supplement_owner_coverage_attestation_redacted_metadata" + ), + } + + +def _load_response_templates(security_dir: Path | None) -> list[dict[str, Any]]: + directory = security_dir or default_security_dir(Path(__file__)) + path = directory / _OWNER_RESPONSE_PACKET + with path.open(encoding="utf-8") as handle: + packet = json.load(handle) + if packet.get("schema_version") != _OWNER_RESPONSE_SCHEMA_VERSION: + raise ValueError(f"{path}: owner response packet schema mismatch") + templates = packet.get("response_templates") + if not isinstance(templates, list) or len(templates) != 5: + raise ValueError(f"{path}: expected five response templates") + return [template for template in templates if isinstance(template, dict)] + + +def _response_items(owner_response: dict[str, Any]) -> list[dict[str, Any]]: + for key in ("responses", "owner_responses", "attestation_responses"): + value = owner_response.get(key) + if isinstance(value, list): + return [item for item in value if isinstance(item, dict)] + return [] + + +def _validate_response_item( + *, + item_id: str, + response: dict[str, Any], + template: dict[str, Any], +) -> list[str]: + blockers: list[str] = [] + decision = str(response.get("decision") or "") + acceptable_decisions = set(_strings(template.get("acceptable_decisions"))) + if decision not in acceptable_decisions: + blockers.append(f"{item_id}.decision_not_allowed") + + for field in _strings(template.get("required_owner_fields")): + value = response.get(field) + if field == "evidence_refs": + if not _has_redacted_evidence_refs(value): + blockers.append(f"{item_id}.evidence_refs_missing_or_not_redacted_list") + continue + if _is_placeholder(value): + blockers.append(f"{item_id}.{field}_missing") + + if _is_placeholder(response.get("decision_reason")): + blockers.append(f"{item_id}.decision_reason_missing") + return blockers + + +def _has_redacted_evidence_refs(value: Any) -> bool: + if not isinstance(value, list) or not value: + return False + for item in value: + if not isinstance(item, str) or _is_placeholder(item): + return False + if _url_has_secret(item): + return False + return True + + +def _find_sensitive_strings(value: Any) -> list[str]: + hits: list[str] = [] + + def walk(node: Any, path: str) -> None: + if isinstance(node, dict): + for key, item in node.items(): + walk(item, f"{path}.{key}" if path else str(key)) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]") + elif isinstance(node, str): + for name, pattern in _SECRET_PATTERNS.items(): + if pattern.search(node): + hits.append(f"{path}:{name}") + if _url_has_secret(node): + hits.append(f"{path}:url_contains_secret_material") + + walk(value, "") + return sorted(set(hits)) + + +def _find_forbidden_true_fields(value: Any) -> list[str]: + hits: list[str] = [] + + def walk(node: Any, path: str) -> None: + if isinstance(node, dict): + for key, item in node.items(): + next_path = f"{path}.{key}" if path else str(key) + if key in _FORBIDDEN_TRUE_FIELDS and item is True: + hits.append(next_path) + walk(item, next_path) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]") + + walk(value, "") + return sorted(hits) + + +def _url_has_secret(value: str) -> bool: + if "://" not in value: + return False + parsed = urlsplit(value) + if parsed.username or parsed.password: + return True + return any(key.lower() in _SECRET_QUERY_KEYS for key, _ in parse_qsl(parsed.query)) + + +def _is_placeholder(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str): + return value.strip().lower() in {"", "pending", "todo", "tbd", "n/a", "na"} + if isinstance(value, list): + return not value + return False + + +def _strings(value: Any) -> list[str]: + if not isinstance(value, list): + return [] + return [str(item) for item in value if item is not None] + + +def _as_dict(value: Any) -> dict[str, Any]: + return value if isinstance(value, dict) else {} + + +def _as_int(value: Any) -> int: + try: + return int(value) + except (TypeError, ValueError): + return 0 diff --git a/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py index 6b9a3e27..06272e3b 100644 --- a/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py +++ b/apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py @@ -10,6 +10,9 @@ from src.api.v1.agents import router from src.services.gitea_authenticated_inventory_payload_validation import ( validate_gitea_authenticated_inventory_payload, ) +from src.services.gitea_owner_coverage_attestation_validation import ( + validate_gitea_owner_coverage_attestation, +) from src.services.gitea_private_inventory_p0_scorecard import ( load_latest_gitea_private_inventory_p0_scorecard, ) @@ -201,6 +204,105 @@ def test_gitea_authenticated_inventory_payload_endpoint_validates_without_persis assert data["operation_boundaries"]["raw_session_or_sqlite_read_performed"] is False +def test_gitea_owner_coverage_attestation_validator_accepts_redacted_response(): + payload = validate_gitea_owner_coverage_attestation( + _valid_owner_coverage_attestation_payload(), + scorecard=_scorecard_readback(), + ) + + assert payload["schema_version"] == "gitea_owner_coverage_attestation_validation_v1" + assert payload["status"] == "accepted_for_owner_coverage_attestation_review_only" + assert payload["result"]["accepted_attestation_packet_count"] == 1 + assert payload["result"]["required_response_item_count"] == 5 + assert payload["result"]["provided_response_item_count"] == 5 + assert payload["result"]["accepted_response_count"] == 5 + assert payload["result"]["current_active_blocker_count"] == 4 + assert payload["result"]["projected_active_blocker_count"] == 3 + assert ( + payload["result"][ + "projected_active_blocker_count_after_redacted_inventory_receipt_writeback" + ] + == 0 + ) + assert payload["result"]["projected_owner_coverage_attestation_accepted_count"] == 1 + assert payload["result"]["runtime_gate_count"] == 0 + assert payload["operation_boundaries"]["payload_persisted"] is False + assert payload["operation_boundaries"]["gitea_api_called"] is False + assert payload["operation_boundaries"]["repo_write_performed"] is False + assert payload["operation_boundaries"]["refs_sync_performed"] is False + assert payload["operation_boundaries"]["github_api_used"] is False + assert payload["operation_boundaries"]["secret_plaintext_read"] is False + assert ( + payload["reviewer_readiness"]["status"] + == "ready_for_private_inventory_closeout_after_inventory_receipt_writeback" + ) + assert ( + payload["reviewer_readiness"][ + "redacted_owner_attestation_receipt_writeback_ready_count" + ] + == 1 + ) + assert payload["reviewer_readiness"]["repo_write_authorized_count"] == 0 + + +def test_gitea_owner_coverage_attestation_endpoint_validates_without_persisting(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.post( + "/api/v1/agents/gitea-private-inventory-p0-scorecard/validate-owner-coverage-attestation", + json=_valid_owner_coverage_attestation_payload(), + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "accepted_for_owner_coverage_attestation_review_only" + assert data["result"]["accepted_attestation_packet_count"] == 1 + assert data["result"]["projected_active_blocker_count"] == 3 + assert ( + data["reviewer_readiness"][ + "redacted_owner_attestation_receipt_writeback_ready_count" + ] + == 1 + ) + assert data["operation_boundaries"]["payload_persisted"] is False + assert data["operation_boundaries"]["gitea_write_performed"] is False + assert data["operation_boundaries"]["raw_session_or_sqlite_read_performed"] is False + + +def test_gitea_owner_coverage_attestation_validator_quarantines_secret_material(): + payload = _valid_owner_coverage_attestation_payload() + payload["responses"][0]["evidence_refs"] = [ + "https://gitea.example/wooo/awoooi?token=secret-value" + ] + + validation = validate_gitea_owner_coverage_attestation( + payload, + scorecard=_scorecard_readback(), + ) + + assert validation["status"] == "quarantined_sensitive_payload" + assert validation["result"]["accepted_attestation_packet_count"] == 0 + assert validation["result"]["sensitive_payload_hit_count"] >= 1 + assert validation["operation_boundaries"]["payload_persisted"] is False + + +def test_gitea_owner_coverage_attestation_validator_rejects_execution_request(): + payload = _valid_owner_coverage_attestation_payload() + payload["write_to_gitea"] = True + + validation = validate_gitea_owner_coverage_attestation( + payload, + scorecard=_scorecard_readback(), + ) + + assert validation["status"] == "rejected_execution_request" + assert validation["result"]["accepted_attestation_packet_count"] == 0 + assert validation["result"]["forbidden_true_field_count"] == 1 + assert validation["operation_boundaries"]["repo_write_performed"] is False + + def test_gitea_authenticated_inventory_payload_validator_quarantines_secret_material(): payload = _valid_authenticated_inventory_payload() payload["repos"][0]["clone_url_redacted"] = "https://user:password@example.test/repo.git" @@ -417,6 +519,81 @@ def _valid_authenticated_inventory_payload() -> dict: } +def _valid_owner_coverage_attestation_payload() -> dict: + return { + "schema_version": "gitea_inventory_owner_attestation_response_v1", + "lane_id": "s4_9_gitea_inventory_owner_attestation_response", + "runtime_execution_authorized": False, + "token_value_collection_allowed": False, + "secret_value_collection_allowed": False, + "gitea_repo_write_authorized": False, + "refs_sync_authorized": False, + "github_primary_switch_authorized": False, + "action_buttons_allowed": False, + "responses": [ + { + "attestation_item_id": "public_only_vs_local_gitea_gap", + "owner_role_or_team": "platform-security-owner", + "decision": "in_scope", + "decision_reason": "admin export must cover the public-only gap", + "affected_repos": ["wooo/awoooi", "wooo/ewoooc"], + "evidence_refs": [ + "docs/security/gitea-repo-inventory.snapshot.json", + "docs/security/gitea-org-repo-inventory-blocked.snapshot.json", + ], + "followup_owner": "platform-security-owner", + }, + { + "attestation_item_id": "org_user_endpoint_identity", + "owner_role_or_team": "platform-security-owner", + "decision": "in_scope", + "decision_reason": "wooo namespace is the canonical Gitea scope", + "canonical_namespace": "wooo", + "evidence_refs": [ + "docs/security/gitea-org-repo-inventory-blocked.snapshot.json" + ], + "followup_owner": "platform-security-owner", + }, + { + "attestation_item_id": "internal_110_adjacent_scope", + "owner_role_or_team": "platform-security-owner", + "decision": "in_scope", + "decision_reason": "internal adjacent source is covered by redacted metadata", + "affected_sources": ["gitea-ssh-main-redacted", "public-route-redacted"], + "evidence_refs": [ + "docs/security/git-remote-refs-bitan-exposure.snapshot.json" + ], + "followup_owner": "platform-security-owner", + }, + { + "attestation_item_id": "repo_owner_canonical_scope", + "owner_role_or_team": "platform-security-owner", + "decision": "in_scope", + "decision_reason": "repo owner and canonical source remain Gitea-only", + "repo_owner": "wooo", + "canonical_source": "gitea", + "github_target_candidate": "stopped_retired_do_not_use", + "visibility_review_owner": "platform-security-owner", + "evidence_refs": [ + "docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json" + ], + }, + { + "attestation_item_id": "legacy_or_inaccessible_repo_disposition", + "owner_role_or_team": "platform-security-owner", + "decision": "legacy_archived", + "decision_reason": "legacy or inaccessible sources stay outside active P0 apply", + "affected_repos_or_sources": ["legacy-github-mirror-retired"], + "disposition": "stopped_retired_do_not_use", + "evidence_refs": [ + "docs/security/gitea-inventory-owner-attestation-response.snapshot.json" + ], + "followup_owner": "platform-security-owner", + }, + ], + } + + def _repo(full_name: str) -> dict: _, name = full_name.split("/", 1) return { diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9b469e43..302bcb20 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -12815,11 +12815,36 @@ "iwooos": { "eyebrow": "資訊安全網", "title": "IwoooS", - "subtitle": "AWOOOI 的 AI 自動化資安閉環:把 Kali、原始碼、主機、告警、候選、執行閘門、驗證器與 AwoooP 證據串成可視化資安態勢。", + "subtitle": "AI SOC 資安控制台:集中讀取主機、原始碼、告警、候選處置與驗證器狀態。", "boundary": { "label": "目前邊界", - "state": "只讀鏡像 / 先觀測", - "detail": "只顯示態勢與缺口;掃描、修復、更新、阻擋仍未開閘。" + "state": "AI 受控推進 / critical break-glass", + "detail": "低 / 中 / 高風險走 selector、dry-run、rollback 與 verifier;secret、破壞性 DB、重啟、付費 provider 與 refs 破壞維持 break-glass。" + }, + "commandRail": { + "eyebrow": "控制面", + "title": "AI SOC 工作台", + "navLabel": "IwoooS 第一屏控制入口", + "metrics": { + "controlledApply": { + "label": "受控執行" + }, + "automationClosure": { + "label": "自動化" + }, + "securityPosture": { + "label": "資安態勢" + }, + "breakGlass": { + "label": "硬邊界" + } + }, + "links": { + "evidence": "證據", + "decisions": "決策", + "scope": "範圍", + "owners": "Owner" + } }, "progressIntegrityRibbon": { "eyebrow": "進度誠實儀表", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index f80a30c1..484a5069 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -12815,11 +12815,36 @@ "iwooos": { "eyebrow": "資訊安全網", "title": "IwoooS", - "subtitle": "AWOOOI 的 AI 自動化資安閉環:把 Kali、原始碼、主機、告警、候選、執行閘門、驗證器與 AwoooP 證據串成可視化資安態勢。", + "subtitle": "AI SOC 資安控制台:集中讀取主機、原始碼、告警、候選處置與驗證器狀態。", "boundary": { "label": "目前邊界", - "state": "只讀鏡像 / 先觀測", - "detail": "只顯示態勢與缺口;掃描、修復、更新、阻擋仍未開閘。" + "state": "AI 受控推進 / critical break-glass", + "detail": "低 / 中 / 高風險走 selector、dry-run、rollback 與 verifier;secret、破壞性 DB、重啟、付費 provider 與 refs 破壞維持 break-glass。" + }, + "commandRail": { + "eyebrow": "控制面", + "title": "AI SOC 工作台", + "navLabel": "IwoooS 第一屏控制入口", + "metrics": { + "controlledApply": { + "label": "受控執行" + }, + "automationClosure": { + "label": "自動化" + }, + "securityPosture": { + "label": "資安態勢" + }, + "breakGlass": { + "label": "硬邊界" + } + }, + "links": { + "evidence": "證據", + "decisions": "決策", + "scope": "範圍", + "owners": "Owner" + } }, "progressIntegrityRibbon": { "eyebrow": "進度誠實儀表", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index e026a6ba..8924f34a 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -6295,6 +6295,20 @@ const evidenceItems = [ 'kali-integration-status.snapshot.json', ] +const commandRailMetrics = [ + { key: 'controlledApply', value: 'L/M/H', icon: Activity, tone: 'steady' }, + { key: 'automationClosure', value: '21/21', icon: Workflow, tone: 'steady' }, + { key: 'securityPosture', value: '64%', icon: ShieldCheck, tone: 'warn' }, + { key: 'breakGlass', value: 'critical', icon: Lock, tone: 'locked' }, +] as const + +const commandRailLinks = [ + { key: 'evidence', href: '#iwooos-first-layer-evidence', icon: SearchCheck, tone: 'steady' }, + { key: 'decisions', href: '#iwooos-decision-gate-visuals', icon: Radar, tone: 'warn' }, + { key: 'scope', href: '#iwooos-scope-evidence-visuals', icon: Network, tone: 'steady' }, + { key: 'owners', href: '#iwooos-source-control-readiness-board', icon: ClipboardCheck, tone: 'warn' }, +] as const + const toneColors = { steady: '#1f7a4d', warn: '#b66a2d', @@ -15504,6 +15518,128 @@ function IwoooSFirstProgressUnlockPathBoard() { ) } +function IwoooSCommandRail() { + const t = useTranslations('iwooos.commandRail') + + return ( +
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+
+ +
+ {commandRailMetrics.map(item => { + const Icon = item.icon + + return ( +
+
+ + + {t(`metrics.${item.key}.label` as never)} + +
+ {item.value} +
+ ) + })} +
+ + +
+
+ ) +} + function IwoooSProgressIntegrityRibbon() { const t = useTranslations('iwooos.progressIntegrityRibbon') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } @@ -23548,6 +23684,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index f6105240..af84cb19 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -79,7 +79,7 @@ spec: - name: AWOOOI_BUILD_COMMIT_SHA # 2026-06-29 Codex: CD rewrites this to the deployed image tag so # production deploy readback does not rely on a stale static snapshot. - value: "d14a25f93ca95e4fe96fc339c4bf35ceec5bbe56" + value: "b9293b76b56cd327f34b4f2fb723674665f69a1c" - name: USE_AI_ROUTER value: "true" - name: ENABLE_NEMOTRON_COLLABORATION diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index a6eb1a19..c03049fd 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -41,7 +41,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: d14a25f93ca95e4fe96fc339c4bf35ceec5bbe56 + newTag: b9293b76b56cd327f34b4f2fb723674665f69a1c - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: d14a25f93ca95e4fe96fc339c4bf35ceec5bbe56 + newTag: b9293b76b56cd327f34b4f2fb723674665f69a1c diff --git a/ops/runner/test_cd_controlled_runtime_profile.py b/ops/runner/test_cd_controlled_runtime_profile.py index 969a75fb..a81a1bc7 100644 --- a/ops/runner/test_cd_controlled_runtime_profile.py +++ b/ops/runner/test_cd_controlled_runtime_profile.py @@ -121,6 +121,7 @@ def test_gitea_private_inventory_scorecard_stays_on_controlled_runtime_profile() expected_sources = [ "docs/operations/awoooi-gitea-private-inventory-p0-scorecard.snapshot.json)", "apps/api/src/services/gitea_authenticated_inventory_payload_validation.py)", + "apps/api/src/services/gitea_owner_coverage_attestation_validation.py)", "apps/api/src/services/gitea_private_inventory_p0_scorecard.py)", "apps/api/tests/test_gitea_private_inventory_p0_scorecard_api.py)", "docs/operations/awoooi-gitea-authenticated-inventory-payload-validation.snapshot.json)", @@ -131,6 +132,7 @@ def test_gitea_private_inventory_scorecard_stays_on_controlled_runtime_profile() "scripts/security/gitea-authenticated-inventory-payload-validator.py)", "scripts/security/tests/test_gitea_private_inventory_p0_scorecard.py)", "src/services/gitea_authenticated_inventory_payload_validation.py", + "src/services/gitea_owner_coverage_attestation_validation.py", "src/services/gitea_private_inventory_p0_scorecard.py", "tests/test_gitea_private_inventory_p0_scorecard_api.py", "scripts/security/tests/test_gitea_authenticated_inventory_payload_validator.py)",