from __future__ import annotations import asyncio import json from pathlib import Path from src.jobs import asset_scanner_job class _Result: rowcount = 1 def scalar(self): return "00000000-0000-0000-0000-000000000001" def one(self): return 1, True class _Database: async def execute(self, statement, parameters=None): return _Result() class _Context: async def __aenter__(self): return _Database() async def __aexit__(self, exc_type, exc, traceback): return False def test_background_scanner_scopes_every_database_operation(monkeypatch) -> None: requested_projects: list[str | None] = [] def fake_db_context(project_id: str | None = None): requested_projects.append(project_id) return _Context() monkeypatch.setattr("src.db.base.get_db_context", fake_db_context) async def exercise_scanner_writes() -> None: run_id = await asset_scanner_job._start_discovery_run( "cron", ["k8s", "prometheus"], "shallow" ) assert run_id == "00000000-0000-0000-0000-000000000001" inserted, modified = await asset_scanner_job._upsert_assets( [ { "asset_key": "host/test", "asset_type": "host", "host": "test", "namespace": None, "name": "test", "metadata": {}, "tags": ["source:test"], } ], run_id, ) assert (inserted, modified) == (1, 0) disappeared = await asset_scanner_job._deprecate_missing_assets( [ { "asset_key": "host/test", } ], ("host/",), run_id, ) assert disappeared == 1 written = await asset_scanner_job._upsert_relationships( [ { "from_key": "host/test", "to_key": "monitoring/test", "relationship_type": "monitors", } ] ) assert written == 1 await asset_scanner_job._write_coverage_snapshots(run_id) await asset_scanner_job._finalize_discovery_run( run_id=run_id, triggered_by="cron", status="success", total_assets=1, new_assets=1, modified_assets=0, disappeared_assets=1, duration_ms=10, error=None, scope=["k8s", "prometheus", "database_catalog"], scan_depth="shallow", ) asyncio.run(exercise_scanner_writes()) assert requested_projects == ["awoooi"] * 6 def test_asset_upsert_propagates_database_failure(monkeypatch) -> None: class FailingDatabase: async def execute(self, statement, parameters=None): raise RuntimeError("constraint rejected") class FailingContext: async def __aenter__(self): return FailingDatabase() async def __aexit__(self, exc_type, exc, traceback): return False monkeypatch.setattr( "src.db.base.get_db_context", lambda project_id=None: FailingContext(), ) async def exercise() -> None: try: await asset_scanner_job._upsert_assets( [ { "asset_key": "postgres/database/awoooi", "asset_type": "database", "host": None, "namespace": None, "name": "awoooi", "metadata": {}, "tags": ["source:test"], } ], "00000000-0000-0000-0000-000000000001", ) except RuntimeError as exc: assert str(exc) == "constraint rejected" else: raise AssertionError("asset upsert failure must propagate") asyncio.run(exercise()) def test_owner_resolution_prefers_explicit_labels_and_safe_scoped_fallbacks() -> None: assert asset_scanner_job._resolve_owner_team( { "asset_key": "k8s/deployment/awoooi-prod/api", "namespace": "awoooi-prod", "metadata": { "labels": {"wooo.work/owner-team": "Platform Security"} }, "tags": [], } ) == ("platform_security", "metadata.labels.wooo.work/owner-team") assert asset_scanner_job._resolve_owner_team( { "asset_key": "k8s/service/awoooi-prod/api", "namespace": "awoooi-prod", "metadata": {}, "tags": [], } ) == ("awoooi", "k8s.namespace") assert asset_scanner_job._resolve_owner_team( { "asset_key": "postgres/relation/awoooi/public/incidents", "namespace": "public", "metadata": {}, "tags": ["source:pg_catalog"], } ) == ("awoooi", "project_scoped_collector") assert asset_scanner_job._resolve_owner_team( { "asset_key": "model_registry/model/ollama/model-a", "namespace": None, "metadata": {}, "tags": ["source:model_registry"], } ) == ("awoooi", "project_scoped_collector") assert asset_scanner_job._resolve_owner_team( { "asset_key": "k8s/node/worker-1", "namespace": None, "metadata": {"labels": {}}, "tags": [], } ) == (None, "unresolved") def test_asset_upsert_persists_owner_candidate_without_overwriting_human_owner( monkeypatch, ) -> None: captured: dict[str, object] = {} class CapturingDatabase: async def execute(self, statement, parameters=None): captured["sql"] = str(statement) captured["parameters"] = parameters return _Result() class CapturingContext: async def __aenter__(self): return CapturingDatabase() async def __aexit__(self, exc_type, exc, traceback): return False monkeypatch.setattr( "src.db.base.get_db_context", lambda project_id=None: CapturingContext(), ) inserted, modified = asyncio.run( asset_scanner_job._upsert_assets( [ { "asset_key": "k8s/deployment/awoooi-prod/api", "asset_type": "k8s_workload", "host": None, "namespace": "awoooi-prod", "name": "api", "metadata": {}, "tags": [], } ], "00000000-0000-0000-0000-000000000001", ) ) assert (inserted, modified) == (1, 0) assert captured["parameters"]["owner_team"] == "awoooi" persisted_metadata = json.loads(captured["parameters"]["md"]) assert persisted_metadata["owner_inference"] == { "candidate": "awoooi", "source": "k8s.namespace", "policy": "deterministic_v1", } sql = str(captured["sql"]) assert "BTRIM(asset_inventory.owner_team) = ''" in sql assert "ELSE asset_inventory.owner_team" in sql def test_asset_key_integrity_preflight_accepts_unique_inventory(monkeypatch) -> None: requested_projects: list[str | None] = [] statements: list[str] = [] class IntegrityResult: def one(self): return 0, True class IntegrityDatabase: async def execute(self, statement, parameters=None): statements.append(str(statement)) return IntegrityResult() class IntegrityContext: async def __aenter__(self): return IntegrityDatabase() async def __aexit__(self, exc_type, exc, traceback): return False def fake_db_context(project_id=None): requested_projects.append(project_id) return IntegrityContext() monkeypatch.setattr("src.db.base.get_db_context", fake_db_context) asyncio.run(asset_scanner_job._assert_asset_key_integrity()) assert requested_projects == ["awoooi"] assert any("enable_indexscan" in statement for statement in statements) assert any("enable_indexonlyscan" in statement for statement in statements) assert any("enable_bitmapscan" in statement for statement in statements) assert any("HAVING COUNT(*) > 1" in statement for statement in statements) assert any("asset_inventory_asset_key_key" in statement for statement in statements) def test_asset_key_integrity_preflight_fails_closed_on_duplicate_key( monkeypatch, ) -> None: class IntegrityResult: def one(self): return 1, True class IntegrityDatabase: async def execute(self, statement, parameters=None): return IntegrityResult() class IntegrityContext: async def __aenter__(self): return IntegrityDatabase() async def __aexit__(self, exc_type, exc, traceback): return False monkeypatch.setattr( "src.db.base.get_db_context", lambda project_id=None: IntegrityContext(), ) async def exercise() -> None: try: await asset_scanner_job._assert_asset_key_integrity() except RuntimeError as exc: assert str(exc) == ( "asset_inventory_key_integrity_failed " "duplicate_groups=1 unique_index_healthy=true" ) else: raise AssertionError("duplicate canonical keys must fail closed") asyncio.run(exercise()) def test_deprecate_missing_assets_is_bounded_to_successful_prefixes( monkeypatch, ) -> None: captured: dict[str, object] = {} class CapturingDatabase: async def execute(self, statement, parameters=None): captured["sql"] = str(statement) captured["parameters"] = parameters return _Result() class CapturingContext: async def __aenter__(self): return CapturingDatabase() async def __aexit__(self, exc_type, exc, traceback): return False monkeypatch.setattr( "src.db.base.get_db_context", lambda project_id=None: CapturingContext(), ) disappeared = asyncio.run( asset_scanner_job._deprecate_missing_assets( [ {"asset_key": "k8s/pod/prod/current"}, {"asset_key": "prometheus_target/node/192.168.0.110:9100"}, ], ("k8s/pod/",), "00000000-0000-0000-0000-000000000001", ) ) assert disappeared == 1 assert "last_seen_at <" in str(captured["sql"]) assert captured["parameters"] == { "patterns": ["k8s/pod/%"], "seen_keys": ["k8s/pod/prod/current"], "rid": "00000000-0000-0000-0000-000000000001", } def test_runtime_collection_reports_failed_sources_without_reconciling_them( monkeypatch, ) -> None: async def fake_kubectl(resource: str, all_namespaces: bool = True): if resource == "nodes": raise RuntimeError("nodes unavailable") return {"items": []} async def fake_prometheus(): return [], [] async def fake_database_catalog(): return [], [] async def fake_ai_catalog(): return [], [] async def fake_gitea_bundle(): return [], [] async def fake_gitea_ci(): return [], [] async def fake_domain_tls(): return [], [] monkeypatch.setattr(asset_scanner_job, "_fetch_kubectl_json", fake_kubectl) monkeypatch.setattr( asset_scanner_job, "_collect_prometheus_targets", fake_prometheus, ) monkeypatch.setattr( asset_scanner_job, "_collect_database_catalog_assets", fake_database_catalog, ) monkeypatch.setattr( asset_scanner_job, "_collect_ai_catalog_assets", fake_ai_catalog, ) monkeypatch.setattr( asset_scanner_job, "_collect_gitea_bundle_assets", fake_gitea_bundle, ) monkeypatch.setattr( asset_scanner_job, "_collect_gitea_ci_assets", fake_gitea_ci, ) monkeypatch.setattr( asset_scanner_job, "_collect_domain_tls_inventory_assets", fake_domain_tls, ) assets, relationships, prefixes, errors = asyncio.run( asset_scanner_job._collect_runtime_assets() ) assert assets == [] assert relationships == [] assert "k8s/node/" not in prefixes assert "k8s/pod/" in prefixes assert "prometheus_target/" in prefixes assert errors == ("k8s_nodes",) def test_ingress_collector_builds_website_api_and_certificate_without_key_data() -> ( None ): assets, relationships = asset_scanner_job._collect_ingress_assets( { "items": [ { "metadata": {"namespace": "awoooi-prod", "name": "awoooi"}, "spec": { "rules": [ { "host": "awoooi.example.test", "http": { "paths": [ { "path": "/api", "pathType": "Prefix", "backend": { "service": {"name": "awoooi-api"} }, } ] }, } ], "tls": [ { "hosts": ["awoooi.example.test"], "secretName": "awoooi-tls", } ], }, } ] } ) by_type = {asset["asset_type"]: asset for asset in assets} assert set(by_type) == {"website", "api_endpoint", "certificate"} assert by_type["api_endpoint"]["metadata"]["request_or_response_body_read"] is False assert by_type["certificate"]["metadata"]["certificate_material_read"] is False assert by_type["certificate"]["metadata"]["secret_object_queried"] is False route_key = by_type["api_endpoint"]["asset_key"] assert relationships == [ { "from_key": "k8s/website/awoooi.example.test", "to_key": route_key, "relationship_type": "routes_to", }, { "from_key": route_key, "to_key": "k8s/service/awoooi-prod/awoooi-api", "relationship_type": "routes_to", }, { "from_key": "k8s/website/awoooi.example.test", "to_key": "k8s/certificate-ref/awoooi-prod/awoooi-tls", "relationship_type": "authenticates_via", }, ] def test_ingress_route_identity_is_stable_when_manifest_order_changes() -> None: def payload(paths: list[dict[str, object]]) -> dict[str, object]: return { "items": [ { "metadata": {"namespace": "prod", "name": "public"}, "spec": { "rules": [ { "host": "service.example.test", "http": {"paths": paths}, } ] }, } ] } api_path = { "path": "/api", "backend": {"service": {"name": "api"}}, } health_path = { "path": "/health", "backend": {"service": {"name": "api"}}, } forward_assets, _ = asset_scanner_job._collect_ingress_assets( payload([api_path, health_path]) ) reversed_assets, _ = asset_scanner_job._collect_ingress_assets( payload([health_path, api_path]) ) forward_keys = { asset["name"]: asset["asset_key"] for asset in forward_assets if asset["asset_type"] == "api_endpoint" } reversed_keys = { asset["name"]: asset["asset_key"] for asset in reversed_assets if asset["asset_type"] == "api_endpoint" } assert forward_keys == reversed_keys assert len(set(forward_keys.values())) == 2 def test_domain_tls_inventory_projects_fingerprints_without_raw_paths() -> None: payload = { "schema_version": "domain_tls_certbot_inventory_v1", "generated_at": "2026-07-14T00:00:00+08:00", "mode": "repo_only_from_nginx_source_of_truth", "execution_boundaries": { "secret_value_collected": False, "live_tls_probe_executed": False, "certbot_renew_executed": False, "nginx_reload_executed": False, "host_write_executed": False, }, "summary": { "managed_domain_count": 2, "unique_certificate_path_count": 1, }, "managed_domains": [ { "domain": "app.example.test", "control_tier": "C0", "tls_certificate_path_present": True, "certificate_owner_confirmation_required": False, "acme_challenge_present": True, "live_tls_probe_status": "not_executed", "dns_resolution_status": "not_executed", "certbot_renewal_status": "not_executed", "owner_review_status": "repo_only_ready_for_owner_review", "certificate_paths": [ "/etc/letsencrypt/live/example/fullchain.pem", ], "certificate_key_paths": [ "/etc/letsencrypt/live/example/privkey.pem", ], }, { "domain": "api.example.test", "control_tier": "C0", "tls_certificate_path_present": True, "certificate_owner_confirmation_required": True, "acme_challenge_present": True, "live_tls_probe_status": "not_executed", "dns_resolution_status": "not_executed", "certbot_renewal_status": "not_executed", "owner_review_status": "repo_only_owner_confirmation_required", "certificate_paths": [ "/etc/letsencrypt/live/example/fullchain.pem", ], "certificate_key_paths": [ "/etc/letsencrypt/live/example/privkey.pem", ], }, ], } assets, relationships = asset_scanner_job._build_domain_tls_inventory_assets( payload ) assert len(assets) == 3 assert {asset["asset_type"] for asset in assets} == {"website", "certificate"} certificate = next( asset for asset in assets if asset["asset_type"] == "certificate" ) assert certificate["metadata"]["raw_certificate_path_stored"] is False assert certificate["metadata"]["certificate_material_read"] is False assert certificate["metadata"]["private_key_material_read"] is False assert len(relationships) == 2 public_text = json.dumps({"assets": assets, "relationships": relationships}) assert "/etc/letsencrypt" not in public_text assert "privkey.pem" not in public_text def test_domain_tls_inventory_candidates_support_shallow_runtime_paths() -> None: candidates = asset_scanner_job._domain_tls_inventory_candidates( cwd=Path("/runtime"), source_path=Path("/app/src/jobs/asset_scanner_job.py"), ) assert Path( "/app/docs/security/domain-tls-certbot-inventory.snapshot.json" ) in candidates def test_public_tls_expiry_classification_is_conservative() -> None: now_epoch = 1_700_000_000.0 assert asset_scanner_job._classify_public_tls_expiry( now_epoch + (45 * 86400), now_epoch=now_epoch ) == ("healthy", 45, "tls_certificate_valid") assert asset_scanner_job._classify_public_tls_expiry( now_epoch + (20 * 86400), now_epoch=now_epoch ) == ("warning", 20, "tls_certificate_expiry_warning") assert asset_scanner_job._classify_public_tls_expiry( now_epoch + (3 * 86400), now_epoch=now_epoch ) == ("critical", 3, "tls_certificate_expiry_critical") assert asset_scanner_job._classify_public_tls_expiry( now_epoch - 1, now_epoch=now_epoch ) == ("critical", -1, "tls_certificate_expired") def test_public_tls_address_policy_allows_only_global_addresses() -> None: assert asset_scanner_job._validated_public_tls_addresses( ["1.1.1.1", "2606:4700:4700::1111", "1.1.1.1"] ) == ["1.1.1.1", "2606:4700:4700::1111"] for rejected in ( "127.0.0.1", "10.0.0.1", "169.254.169.254", "192.168.0.110", "::1", "fc00::1", "not-an-address", ): try: asset_scanner_job._validated_public_tls_addresses([rejected]) except asset_scanner_job._PublicTlsAddressPolicyError: pass else: raise AssertionError(f"non-public TLS address accepted: {rejected}") def test_domain_tls_probe_aggregates_shared_certificate_without_false_green() -> ( None ): payload = { "schema_version": "domain_tls_certbot_inventory_v1", "generated_at": "2026-07-14T00:00:00+08:00", "mode": "repo_only_from_nginx_source_of_truth", "execution_boundaries": { "secret_value_collected": False, "live_tls_probe_executed": False, "certbot_renew_executed": False, "nginx_reload_executed": False, "host_write_executed": False, }, "summary": { "managed_domain_count": 2, "unique_certificate_path_count": 1, }, "managed_domains": [ { "domain": domain, "certificate_paths": ["/redacted/shared/fullchain.pem"], } for domain in ("app.example.test", "api.example.test") ], } assets, relationships = asset_scanner_job._build_domain_tls_inventory_assets( payload ) probe_results = { "app.example.test": asset_scanner_job._public_tls_probe_result( status="healthy", evidenced=True, healthy=True, chain_verified=True, hostname_verified=True, reason_code="tls_certificate_valid", expires_at="2026-09-01T00:00:00+00:00", days_remaining=49, ), "api.example.test": asset_scanner_job._public_tls_probe_result( status="warning", evidenced=True, healthy=False, chain_verified=True, hostname_verified=True, reason_code="tls_certificate_expiry_warning", expires_at="2026-08-01T00:00:00+00:00", days_remaining=18, ), } assets, _ = asset_scanner_job._apply_domain_tls_probe_results( assets, relationships, probe_results ) certificate = next( asset for asset in assets if asset["asset_type"] == "certificate" ) metadata = certificate["metadata"] assert metadata["live_tls_probe_evidenced"] is True assert metadata["live_tls_healthy"] is False assert metadata["live_tls_probe_status"] == "warning" assert metadata["tls_endpoint_count"] == 2 assert metadata["tls_evidenced_endpoint_count"] == 2 assert metadata["tls_healthy_endpoint_count"] == 1 assert metadata["tls_warning_endpoint_count"] == 1 assert metadata["tls_unavailable_endpoint_count"] == 0 assert metadata["tls_min_days_remaining"] == 18 assert metadata["certificate_material_read"] is False assert metadata["private_key_material_read"] is False def test_domain_tls_probe_keeps_shared_certificate_unverified_when_one_is_missing() -> ( None ): payload = { "schema_version": "domain_tls_certbot_inventory_v1", "generated_at": "2026-07-14T00:00:00+08:00", "mode": "repo_only_from_nginx_source_of_truth", "execution_boundaries": { "secret_value_collected": False, "live_tls_probe_executed": False, "certbot_renew_executed": False, "nginx_reload_executed": False, "host_write_executed": False, }, "summary": { "managed_domain_count": 2, "unique_certificate_path_count": 1, }, "managed_domains": [ { "domain": domain, "certificate_paths": ["/redacted/shared/fullchain.pem"], } for domain in ("app.example.test", "api.example.test") ], } assets, relationships = asset_scanner_job._build_domain_tls_inventory_assets( payload ) assets, _ = asset_scanner_job._apply_domain_tls_probe_results( assets, relationships, { "app.example.test": asset_scanner_job._public_tls_probe_result( status="healthy", evidenced=True, healthy=True, chain_verified=True, hostname_verified=True, reason_code="tls_certificate_valid", days_remaining=49, ) }, ) certificate = next(asset for asset in assets if asset["asset_type"] == "certificate") assert certificate["metadata"]["live_tls_probe_evidenced"] is False assert certificate["metadata"]["live_tls_healthy"] is False assert certificate["metadata"]["tls_evidenced_endpoint_count"] == 1 assert certificate["metadata"]["tls_unavailable_endpoint_count"] == 1 def test_domain_tls_inventory_fails_closed_when_secret_boundary_is_not_false() -> ( None ): payload = { "schema_version": "domain_tls_certbot_inventory_v1", "mode": "repo_only_from_nginx_source_of_truth", "execution_boundaries": { "secret_value_collected": True, "live_tls_probe_executed": False, "certbot_renew_executed": False, "nginx_reload_executed": False, "host_write_executed": False, }, "managed_domains": [{"domain": "app.example.test"}], } try: asset_scanner_job._build_domain_tls_inventory_assets(payload) except RuntimeError as exc: assert str(exc) == ( "domain_tls_inventory_boundary_invalid:secret_value_collected" ) else: raise AssertionError("domain TLS inventory must fail closed on secret boundary") def test_k8s_metadata_assets_never_read_volume_secret_or_command_data() -> None: pvc = asset_scanner_job._build_pvc_asset( { "metadata": {"namespace": "prod", "name": "data"}, "spec": { "storageClassName": "local-path", "accessModes": ["ReadWriteOnce"], "resources": {"requests": {"storage": "10Gi"}}, }, "status": {"phase": "Bound"}, } ) cronjob = asset_scanner_job._build_cronjob_asset( { "metadata": {"namespace": "prod", "name": "scan"}, "spec": {"schedule": "0 * * * *", "suspend": False}, "status": {"active": []}, } ) secret = asset_scanner_job._build_secret_reference_asset( "prod", "database-ref", reference_kind="PodVolume", ) assert pvc["asset_type"] == "volume" assert pvc["metadata"]["data_read"] is False assert cronjob["asset_type"] == "scheduled_job" assert cronjob["metadata"]["command_or_env_read"] is False assert secret["asset_type"] == "secret" assert secret["metadata"]["secret_object_queried"] is False assert secret["metadata"]["secret_value_read"] is False def test_workload_network_and_service_registry_projections_are_bounded() -> None: web_assets, web_relationships = ( asset_scanner_job._build_workload_component_assets( { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-web", "labels": { "app": "awoooi-web", "system": "awoooi", }, } }, "Deployment", ) ) log_assets, _ = asset_scanner_job._build_workload_component_assets( { "metadata": { "namespace": "observability", "name": "otel-collector", "labels": { "app.kubernetes.io/component": "log-collector", }, } }, "DaemonSet", ) network = asset_scanner_job._build_service_network_asset( { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-api", "labels": {"system": "awoooi"}, }, "spec": { "type": "ClusterIP", "clusterIP": "10.43.0.10", "ports": [ { "name": "http", "protocol": "TCP", "port": 80, "targetPort": 8000, } ], }, } ) registry_assets, registry_relationships = ( asset_scanner_job._collect_service_registry_assets( { "metadata": { "namespace": "awoooi-prod", "name": "service-registry", }, "data": { "service-registry.yaml": """ services: - name: grafana display_name: Grafana host: 192.168.0.110 stateful_level: STANDARD_HITL containers: [grafana] - name: redis display_name: Redis cache host: 192.168.0.188 stateful_level: CRITICAL_HITL containers: [redis] - name: sentry-redis display_name: Sentry task queue host: 192.168.0.110 stateful_level: CRITICAL_HITL containers: [sentry-redis] """, }, } ) ) assert [asset["asset_type"] for asset in web_assets] == ["frontend"] assert web_relationships[0]["to_key"] == ( "k8s/deployment/awoooi-prod/awoooi-web" ) assert [asset["asset_type"] for asset in log_assets] == ["log_stream"] assert network["asset_type"] == "network" assert network["metadata"]["packet_payload_read"] is False assert {asset["asset_type"] for asset in registry_assets} == { "dashboard", "cache", "message_queue", } assert all( asset["metadata"]["raw_config_persisted"] is False and asset["metadata"]["secret_value_read"] is False for asset in registry_assets ) assert len(registry_relationships) == len(registry_assets) def test_workload_package_projection_never_pulls_images_or_uses_github() -> None: assets, relationships = asset_scanner_job._build_workload_package_assets( { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-api", "labels": {"system": "awoooi"}, }, "spec": { "template": { "spec": { "containers": [ { "name": "api", "image": "192.168.0.110:5000/library/api:abc123", } ], "initContainers": [ { "name": "blocked-source", "image": "ghcr.io/example/tool@sha256:abcd", } ], } } }, }, "Deployment", ) assert len(assets) == 2 assert all(asset["asset_type"] == "package" for asset in assets) assert assets[0]["metadata"]["image_content_pulled_by_scanner"] is False assert assets[0]["metadata"]["registry_api_called_by_scanner"] is False assert assets[0]["metadata"]["forbidden_source_domain_detected"] is None assert assets[1]["metadata"]["digest_pinned"] is True assert assets[1]["metadata"]["forbidden_source_domain_detected"] == "ghcr.io" assert len(relationships) == 2 def test_gitea_ci_projection_requires_gitea_authority_and_github_freeze() -> None: assets, relationships = asset_scanner_job._build_gitea_ci_assets( { "schema_version": "gitea_workflow_runner_health_v1", "workflow_records": [ { "workflow_id": "cd", "display_name": "CD", "file_ref": ".gitea/workflows/cd.yaml", "status": "manifest_mapped", "risk_level": "high", "triggers": ["push", "workflow_dispatch"], "runner_labels": ["awoooi-non110-ubuntu"], "runner_evidence_status": "non110_runner_mapped", "job_count": 2, "notify_bridge_calls": 1, } ], }, { "source_control_authority": "gitea", "status": "gitea_cicd_alert_receipt_chain_ready", "ready": True, "operation_boundaries": {"github_api_allowed": False}, "summary": {"workflow_notify_bridge_count": 6}, }, ) assert [asset["asset_type"] for asset in assets] == ["ci_pipeline"] assert assets[0]["metadata"]["receipt_chain_ready"] is True assert assets[0]["metadata"]["workflow_triggered_by_scanner"] is False assert assets[0]["metadata"]["github_api_used"] is False assert relationships == [ { "from_key": "gitea/workflow/cd", "to_key": "gitea/repo/wooo/awoooi", "relationship_type": "depends_on", } ] try: asset_scanner_job._build_gitea_ci_assets( { "schema_version": "gitea_workflow_runner_health_v1", "workflow_records": [{"workflow_id": "cd"}], }, { "source_control_authority": "gitea", "operation_boundaries": {"github_api_allowed": True}, }, ) except RuntimeError as exc: assert str(exc) == "ci_github_freeze_not_evidenced" else: raise AssertionError("CI projection must fail closed without GitHub freeze") def test_runtime_collection_integrates_new_asset_types_and_reconciliation_prefixes( monkeypatch, ) -> None: async def fake_kubectl(resource: str, all_namespaces: bool = True): if resource == "pods": return { "items": [ { "metadata": {"namespace": "prod", "name": "api"}, "spec": { "volumes": [ { "name": "credential", "secret": {"secretName": "database-ref"}, } ] }, } ] } if resource == "persistentvolumeclaims": return { "items": [ { "metadata": {"namespace": "prod", "name": "data"}, "spec": {}, "status": {}, } ] } if resource == "deployments": return { "items": [ { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-web", "labels": { "app": "awoooi-web", "system": "awoooi", }, }, "spec": { "template": { "spec": { "containers": [ { "name": "web", "image": "192.168.0.110:5000/library/web:test", } ] } } }, "status": {}, }, { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-api", "labels": { "app": "awoooi-api", "system": "awoooi", }, }, "spec": { "template": { "spec": { "containers": [ { "name": "api", "image": "192.168.0.110:5000/library/api:test", } ] } } }, "status": {}, }, ] } if resource == "daemonsets": return { "items": [ { "metadata": { "namespace": "observability", "name": "otel-collector", "labels": { "app.kubernetes.io/component": "log-collector" }, }, "spec": { "template": { "spec": { "containers": [ { "name": "otel-collector", "image": "otel/collector:0.96.0", } ] } } }, "status": {}, } ] } if resource == "services": return { "items": [ { "metadata": { "namespace": "awoooi-prod", "name": "awoooi-api", "labels": {"system": "awoooi"}, }, "spec": { "type": "ClusterIP", "clusterIP": "10.43.0.10", "ports": [{"port": 80, "targetPort": 8000}], }, } ] } if resource == "configmaps": return { "items": [ { "metadata": { "namespace": "awoooi-prod", "name": "service-registry", }, "data": { "service-registry.yaml": """ services: - name: grafana display_name: Grafana host: 192.168.0.110 stateful_level: STANDARD_HITL containers: [grafana] - name: sentry-redis display_name: Sentry task queue host: 192.168.0.110 stateful_level: CRITICAL_HITL containers: [sentry-redis] """, }, } ] } if resource == "ingresses": return { "items": [ { "metadata": {"namespace": "prod", "name": "public"}, "spec": { "rules": [ { "host": "service.example.test", "http": { "paths": [ { "path": "/api", "backend": {"service": {"name": "api"}}, } ] }, } ], "tls": [ { "hosts": ["service.example.test"], "secretName": "public-tls", } ], }, } ] } if resource == "cronjobs": return { "items": [ { "metadata": {"namespace": "prod", "name": "scan"}, "spec": {"schedule": "0 * * * *"}, "status": {}, } ] } return {"items": []} async def empty_collector(): return [], [] monkeypatch.setattr(asset_scanner_job, "_fetch_kubectl_json", fake_kubectl) monkeypatch.setattr( asset_scanner_job, "_collect_prometheus_targets", empty_collector, ) monkeypatch.setattr( asset_scanner_job, "_collect_database_catalog_assets", empty_collector, ) monkeypatch.setattr( asset_scanner_job, "_collect_ai_catalog_assets", empty_collector, ) monkeypatch.setattr( asset_scanner_job, "_collect_gitea_bundle_assets", empty_collector, ) monkeypatch.setattr( asset_scanner_job, "_collect_gitea_ci_assets", empty_collector, ) monkeypatch.setattr( asset_scanner_job, "_collect_domain_tls_inventory_assets", empty_collector, ) assets, relationships, prefixes, errors = asyncio.run( asset_scanner_job._collect_runtime_assets() ) assert errors == () assert { "container", "secret", "volume", "website", "api_endpoint", "certificate", "scheduled_job", "network", "frontend", "backend", "log_stream", "dashboard", "cache", "message_queue", "package", } <= {asset["asset_type"] for asset in assets} assert { "k8s/secret-ref/", "k8s/pvc/", "k8s/website/", "k8s/ingress-route/", "k8s/certificate-ref/", "k8s/cronjob/", "k8s/network/service/", "k8s/component/frontend/deployment/", "k8s/component/backend/deployment/", "k8s/component/log_stream/daemonset/", "service-registry/dashboard/", "service-registry/cache/", "service-registry/message_queue/", "k8s/package/deployment/", "k8s/package/daemonset/", } <= set(prefixes) assert any( relationship["to_key"] == "k8s/secret-ref/prod/database-ref" for relationship in relationships ) def test_ai_catalog_collector_reads_aggregates_and_model_names_only( monkeypatch, tmp_path, ) -> None: class CatalogResult: def __init__(self, rows): self._rows = rows def fetchall(self): return self._rows class CatalogDatabase: async def execute(self, statement, parameters=None): sql = str(statement) if "FROM agent_sessions" in sql: return CatalogResult( [ type( "AgentRow", (), { "_mapping": { "agent_role": "security_reviewer", "session_count": 4, "latest_seen_at": "2026-07-14T00:00:00+00:00", } }, )() ] ) assert parameters == {"project_id": "awoooi"} return CatalogResult( [ type( "KnowledgeRow", (), { "_mapping": { "entry_type": "runbook", "entry_count": 9, "latest_updated_at": "2026-07-14T00:00:00+00:00", } }, )() ] ) class CatalogContext: async def __aenter__(self): return CatalogDatabase() async def __aexit__(self, exc_type, exc, traceback): return False monkeypatch.setattr( "src.db.base.get_db_context", lambda project_id=None: CatalogContext(), ) registry = tmp_path / "models.json" registry.write_text( json.dumps( { "providers": { "ollama": { "models": { "default": "model-a", "summary": "model-a", } } } } ), encoding="utf-8", ) assets, relationships = asyncio.run( asset_scanner_job._collect_ai_catalog_assets(registry) ) by_type: dict[str, list[dict]] = {} for asset in assets: by_type.setdefault(asset["asset_type"], []).append(asset) assert set(by_type) == { "third_party_service", "llm_model", "ai_agent", "km_entry", } assert len(by_type["llm_model"]) == 1 assert by_type["llm_model"][0]["metadata"]["purposes"] == [ "default", "summary", ] assert by_type["ai_agent"][0]["metadata"]["session_input_or_output_read"] is False assert by_type["km_entry"][0]["metadata"]["title_or_content_read"] is False assert relationships == [ { "from_key": "model_registry/model/ollama/model-a", "to_key": "model_registry/provider/ollama", "relationship_type": "depends_on", } ] def test_gitea_bundle_readback_builds_repo_and_backup_assets() -> None: assets, relationships = asset_scanner_job._build_gitea_bundle_assets( { "schema_version": "gitea_repo_bundle_backup_readback_v1", "status": "gitea_repo_bundle_backup_readback_ready", "host": "188", "readback_present": True, "ready": True, "summary": { "expected_repo_count": 2, "repo_row_count": 2, "expected_repo_missing_count": 0, "failed_repo_count": 0, "checksum_missing_count": 0, "bundle_fresh": True, "sample_restore_dry_run_ok": True, }, "repos": [ { "repo": "wooo/awoooi", "present": True, "ok": True, "head_count": 196, "checksum_digest_present": True, }, { "repo": "wooo/tsenyang-website", "present": True, "ok": True, "head_count": 2, "checksum_digest_present": True, }, ], } ) by_type: dict[str, list[dict]] = {} for asset in assets: by_type.setdefault(asset["asset_type"], []).append(asset) assert len(by_type["gitea_repo"]) == 2 assert len(by_type["backup_target"]) == 1 assert by_type["backup_target"][0]["metadata"]["ready"] is True assert all( asset["metadata"]["repository_content_read_by_scanner"] is False for asset in assets ) assert all( asset_scanner_job._resolve_owner_team(asset)[0] == "platform_security" for asset in assets ) assert relationships == [ { "from_key": "gitea/repo/wooo/awoooi", "to_key": "backup/gitea-bundle/188", "relationship_type": "backs_up_to", }, { "from_key": "gitea/repo/wooo/tsenyang-website", "to_key": "backup/gitea-bundle/188", "relationship_type": "backs_up_to", }, ] def test_scan_terminal_is_failed_when_any_collector_is_incomplete(monkeypatch) -> None: terminal: dict[str, object] = {} async def fake_start(*args, **kwargs): return "00000000-0000-0000-0000-000000000001" async def fake_collect(): return [], [], (), ("prometheus_targets",) async def fake_integrity(): return None async def fake_upsert(*args, **kwargs): return 0, 0 async def fake_deprecate(*args, **kwargs): return 0 async def fake_relationships(*args, **kwargs): return 0 async def fake_coverage(*args, **kwargs): return None async def fake_finalize(**kwargs): terminal.update(kwargs) monkeypatch.setattr(asset_scanner_job, "_start_discovery_run", fake_start) monkeypatch.setattr( asset_scanner_job, "_assert_asset_key_integrity", fake_integrity, ) monkeypatch.setattr(asset_scanner_job, "_collect_runtime_assets", fake_collect) monkeypatch.setattr(asset_scanner_job, "_upsert_assets", fake_upsert) monkeypatch.setattr( asset_scanner_job, "_deprecate_missing_assets", fake_deprecate, ) monkeypatch.setattr( asset_scanner_job, "_upsert_relationships", fake_relationships, ) monkeypatch.setattr( asset_scanner_job, "_write_coverage_snapshots", fake_coverage, ) monkeypatch.setattr( asset_scanner_job, "_finalize_discovery_run", fake_finalize, ) run_id = asyncio.run(asset_scanner_job.scan_once()) assert run_id == "00000000-0000-0000-0000-000000000001" assert terminal["status"] == "failed" assert terminal["error"] == ( "RuntimeError: asset_collector_failures=prometheus_targets" ) def test_database_catalog_collector_reads_metadata_only(monkeypatch) -> None: requested_projects: list[str | None] = [] class CatalogResult: def fetchall(self): return [ type( "CatalogRow", (), { "_mapping": { "database_name": "awoooi", "schema_name": "public", "relation_name": "incidents", "relation_kind": "table", "row_security_enabled": True, } }, )() ] class CatalogDatabase: async def execute(self, statement, parameters=None): return CatalogResult() class CatalogContext: async def __aenter__(self): return CatalogDatabase() async def __aexit__(self, exc_type, exc, traceback): return False def fake_db_context(project_id: str | None = None): requested_projects.append(project_id) return CatalogContext() monkeypatch.setattr("src.db.base.get_db_context", fake_db_context) assets, relationships = asyncio.run( asset_scanner_job._collect_database_catalog_assets() ) assert requested_projects == ["awoooi"] assert [asset["asset_type"] for asset in assets] == ["database", "table"] assert assets[0]["metadata"]["row_data_read"] is False assert assets[1]["metadata"] == { "database": "awoooi", "schema": "public", "relation_kind": "table", "row_security_enabled": True, "row_data_read": False, } assert relationships == [ { "from_key": "postgres/relation/awoooi/public/incidents", "to_key": "postgres/database/awoooi", "relationship_type": "depends_on", } ]