Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m34s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
409 lines
14 KiB
Python
409 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
# ruff: noqa: E402, I001
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
from datetime import UTC, datetime, timedelta
|
|
from types import SimpleNamespace
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.api.v1.iwooos import router
|
|
from src.services.iwooos_security_asset_control_plane import (
|
|
IwoooSSecurityAssetControlPlaneService,
|
|
_read_public_source,
|
|
build_iwooos_security_asset_control_plane,
|
|
build_unavailable_iwooos_security_asset_control_plane,
|
|
)
|
|
|
|
|
|
def _row(**values):
|
|
return SimpleNamespace(**values)
|
|
|
|
|
|
def _ready_payload(source_health: list[dict] | None = None) -> dict:
|
|
now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC)
|
|
discovery = _row(
|
|
latest_status="success",
|
|
latest_scan_depth="full",
|
|
latest_started_at=now - timedelta(minutes=31),
|
|
latest_ended_at=now - timedelta(minutes=30),
|
|
latest_success_ended_at=now - timedelta(minutes=30),
|
|
latest_total_assets=24,
|
|
latest_new_assets=2,
|
|
latest_modified_assets=1,
|
|
latest_disappeared_assets=0,
|
|
)
|
|
inventory = [
|
|
_row(asset_type="host", cnt=3, unowned_count=1, stale_count=0),
|
|
_row(asset_type="k8s_workload", cnt=5, unowned_count=0, stale_count=1),
|
|
_row(asset_type="website", cnt=2, unowned_count=0, stale_count=0),
|
|
_row(asset_type="api_endpoint", cnt=4, unowned_count=0, stale_count=0),
|
|
_row(asset_type="database", cnt=2, unowned_count=0, stale_count=0),
|
|
_row(asset_type="log_stream", cnt=4, unowned_count=0, stale_count=0),
|
|
_row(asset_type="monitoring_target", cnt=3, unowned_count=0, stale_count=0),
|
|
_row(asset_type="ai_agent", cnt=1, unowned_count=0, stale_count=0),
|
|
]
|
|
coverage = [
|
|
_row(dimension=dimension, status="green", cnt=20)
|
|
for dimension in (
|
|
"auto_monitoring",
|
|
"auto_alerting",
|
|
"auto_rule_creation",
|
|
"auto_rule_matching",
|
|
"auto_playbook",
|
|
"auto_remediation",
|
|
"auto_km_creation",
|
|
)
|
|
]
|
|
compliance = [
|
|
_row(dimension=dimension, status="compliant", cnt=20)
|
|
for dimension in (
|
|
"ssl_cert_valid",
|
|
"cve_scan",
|
|
"secret_rotated",
|
|
"backup_tested",
|
|
"audit_log_enabled",
|
|
"access_reviewed",
|
|
"encryption_at_rest",
|
|
)
|
|
]
|
|
automation = [
|
|
_row(operation_type="asset_discovered", status="success", cnt=1),
|
|
_row(operation_type="rule_matched", status="success", cnt=3),
|
|
_row(operation_type="rule_created", status="success", cnt=2),
|
|
_row(operation_type="ansible_check_mode_executed", status="dry_run", cnt=2),
|
|
_row(operation_type="ansible_apply_executed", status="success", cnt=1),
|
|
_row(operation_type="remediation_verified", status="success", cnt=1),
|
|
_row(operation_type="km_created", status="success", cnt=1),
|
|
_row(operation_type="ansible_apply_executed", status="failed", cnt=1),
|
|
]
|
|
return build_iwooos_security_asset_control_plane(
|
|
discovery_row=discovery,
|
|
inventory_rows=inventory,
|
|
coverage_rows=coverage,
|
|
relationship_row=_row(relationship_count=21, orphan_asset_count=3),
|
|
compliance_rows=compliance,
|
|
automation_rows=automation,
|
|
ai_trace_row=_row(trace_count=4, accepted_trace_count=3),
|
|
siem_row=_row(
|
|
rule_total=12,
|
|
approved_rule_count=10,
|
|
fired_rule_count=8,
|
|
noisy_rule_count=1,
|
|
ai_generated_rule_count=3,
|
|
incident_total_24h=5,
|
|
open_incident_count_24h=2,
|
|
resolved_incident_count_24h=3,
|
|
decision_chain_count_24h=5,
|
|
normalized_event_count_24h=18,
|
|
mttr_minutes_24h=14.25,
|
|
),
|
|
audit_row=_row(
|
|
k8s_audit_count_24h=2,
|
|
k8s_audit_failure_count_24h=0,
|
|
mcp_audit_count_24h=11,
|
|
mcp_audit_failure_count_24h=1,
|
|
asset_change_count_24h=4,
|
|
asset_change_ai_analysis_count_24h=3,
|
|
),
|
|
source_health=source_health,
|
|
generated_at=now,
|
|
)
|
|
|
|
|
|
def test_builds_live_asset_siem_audit_and_ai_control_plane_without_false_closure() -> (
|
|
None
|
|
):
|
|
payload = _ready_payload()
|
|
|
|
assert payload["schema_version"] == "iwooos_security_asset_control_plane_v1"
|
|
assert payload["status"] == "ready"
|
|
assert payload["source_status"] == "live_database_aggregate"
|
|
assert payload["summary"]["managed_asset_count"] == 24
|
|
assert payload["summary"]["present_asset_type_count"] == 8
|
|
assert payload["summary"]["siem_stage_coverage_percent"] == 100
|
|
assert payload["summary"]["ai_loop_stage_coverage_percent"] == 100
|
|
assert payload["summary"]["verified_remediation_receipt_count_24h"] == 1
|
|
assert payload["discovery"]["fresh"] is True
|
|
assert payload["ai_automation"]["same_run_closed_loop_proven"] is False
|
|
assert payload["ai_automation"]["same_run_closed_loop_count"] == 0
|
|
assert len(payload["nist_csf_functions"]) == 6
|
|
assert payload["summary"]["security_program_domain_count"] == 16
|
|
assert len(payload["security_program_domains"]) == 16
|
|
domain_by_id = {
|
|
domain["domain_id"]: domain for domain in payload["security_program_domains"]
|
|
}
|
|
assert domain_by_id["endpoint_xdr_wazuh"]["status"] == "not_evidenced"
|
|
assert domain_by_id["siem_soc_threat_intel"]["controlled_percent"] == 50
|
|
assert len(payload["siem"]["pipeline"]) == 8
|
|
assert payload["security_audit"]["mcp_tool_audit_count_24h"] == 11
|
|
assert any(
|
|
item["work_item_id"] == "AIA-P0-006-02" for item in payload["work_items"]
|
|
)
|
|
assert any(
|
|
item["work_item_id"] == "AIA-P1-001-01" for item in payload["work_items"]
|
|
)
|
|
|
|
public_text = json.dumps(payload, ensure_ascii=False)
|
|
assert "192.168." not in public_text
|
|
assert "postgresql://" not in public_text
|
|
assert "target_resource" not in public_text
|
|
assert '"event_payload":' not in public_text
|
|
assert 'secret_value_collection_allowed": true' not in public_text.lower()
|
|
|
|
|
|
def test_unknown_coverage_and_compliance_create_p0_work_items() -> None:
|
|
now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC)
|
|
payload = build_iwooos_security_asset_control_plane(
|
|
discovery_row=_row(
|
|
latest_status="success",
|
|
latest_scan_depth="shallow",
|
|
latest_started_at=now - timedelta(minutes=10),
|
|
latest_ended_at=now - timedelta(minutes=9),
|
|
latest_success_ended_at=now - timedelta(minutes=9),
|
|
latest_total_assets=1,
|
|
),
|
|
inventory_rows=[
|
|
_row(asset_type="host", cnt=1, unowned_count=1, stale_count=0),
|
|
],
|
|
coverage_rows=[
|
|
_row(dimension="auto_monitoring", status="unknown", cnt=1),
|
|
],
|
|
relationship_row=_row(relationship_count=0, orphan_asset_count=1),
|
|
compliance_rows=[
|
|
_row(dimension="cve_scan", status="unknown", cnt=1),
|
|
],
|
|
automation_rows=[],
|
|
ai_trace_row=_row(trace_count=0, accepted_trace_count=0),
|
|
siem_row=_row(),
|
|
audit_row=_row(),
|
|
generated_at=now,
|
|
)
|
|
|
|
assert payload["summary"]["automation_coverage_green_percent"] == 0
|
|
assert payload["summary"]["automation_coverage_unknown_count"] == 1
|
|
assert payload["summary"]["compliance_unknown_count"] == 1
|
|
assert payload["summary"]["siem_observed_stage_count"] == 0
|
|
assert payload["summary"]["ai_loop_observed_stage_count"] == 0
|
|
work_item_ids = {item["work_item_id"] for item in payload["work_items"]}
|
|
assert {
|
|
"AIA-P0-006-02",
|
|
"AIA-P0-006-03",
|
|
"AIA-P0-006-04",
|
|
"AIA-P0-006-05",
|
|
"AIA-P0-006-06",
|
|
"AIA-P0-007-01",
|
|
"AIA-P0-008-01",
|
|
} <= work_item_ids
|
|
|
|
|
|
def test_partial_source_failure_is_visible_without_erasing_core_inventory() -> None:
|
|
source_health = [
|
|
{"source_id": "asset_discovery", "status": "ready", "reason_code": None},
|
|
{"source_id": "asset_inventory", "status": "ready", "reason_code": None},
|
|
{
|
|
"source_id": "audit_runtime",
|
|
"status": "unavailable",
|
|
"reason_code": "audit_runtime_query_failed",
|
|
},
|
|
]
|
|
payload = _ready_payload(source_health)
|
|
|
|
assert payload["status"] == "degraded"
|
|
assert payload["source_status"] == "live_database_partial"
|
|
assert payload["summary"]["managed_asset_count"] == 24
|
|
assert payload["summary"]["ready_source_count"] == 2
|
|
assert payload["summary"]["unavailable_source_count"] == 1
|
|
assert payload["source_health"] == source_health
|
|
assert payload["work_items"][0]["work_item_id"] == "AIA-P0-006-00"
|
|
|
|
|
|
def test_read_public_source_returns_redacted_reason_code_on_query_failure(
|
|
monkeypatch,
|
|
) -> None:
|
|
class FailingDb:
|
|
async def execute(self, statement):
|
|
raise RuntimeError("private table detail")
|
|
|
|
class DbContext:
|
|
async def __aenter__(self):
|
|
return FailingDb()
|
|
|
|
async def __aexit__(self, exc_type, exc, traceback):
|
|
return False
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.iwooos_security_asset_control_plane.get_db_context",
|
|
lambda project_id: DbContext(),
|
|
)
|
|
|
|
value, health = asyncio.run(
|
|
_read_public_source(
|
|
project_id="awoooi",
|
|
limiter=asyncio.Semaphore(1),
|
|
source_id="audit_runtime",
|
|
statement="SELECT 1",
|
|
result_mode="one",
|
|
default={},
|
|
)
|
|
)
|
|
|
|
assert value == {}
|
|
assert health == {
|
|
"source_id": "audit_runtime",
|
|
"status": "unavailable",
|
|
"reason_code": "audit_runtime_query_failed",
|
|
}
|
|
|
|
|
|
def test_unavailable_payload_is_fixed_degraded_contract_without_raw_error() -> None:
|
|
payload = build_unavailable_iwooos_security_asset_control_plane(
|
|
"database_query_failed"
|
|
)
|
|
|
|
assert payload["status"] == "degraded"
|
|
assert payload["source_status"] == "live_database_unavailable"
|
|
assert payload["reason_code"] == "database_query_failed"
|
|
assert payload["summary"]["managed_asset_count"] == 0
|
|
assert payload["discovery"]["fresh"] is False
|
|
assert payload["boundaries"]["raw_asset_identity_returned"] is False
|
|
assert payload["boundaries"]["raw_event_payload_returned"] is False
|
|
|
|
|
|
def test_service_redacts_database_exception_details(monkeypatch) -> None:
|
|
service = IwoooSSecurityAssetControlPlaneService()
|
|
|
|
async def fail_load():
|
|
raise RuntimeError("postgresql://operator:raw-secret@private-host/runtime")
|
|
|
|
monkeypatch.setattr(service, "_load_snapshot", fail_load)
|
|
payload = asyncio.run(service.get_snapshot())
|
|
text = json.dumps(payload)
|
|
|
|
assert payload["reason_code"] == "database_query_failed"
|
|
assert "raw-secret" not in text
|
|
assert "private-host" not in text
|
|
|
|
|
|
def test_service_reuses_short_lived_public_aggregate_cache(monkeypatch) -> None:
|
|
service = IwoooSSecurityAssetControlPlaneService()
|
|
calls = 0
|
|
expected = _ready_payload()
|
|
|
|
async def load_once():
|
|
nonlocal calls
|
|
calls += 1
|
|
return expected
|
|
|
|
monkeypatch.setattr(service, "_load_snapshot", load_once)
|
|
|
|
async def read_twice():
|
|
first = await service.get_snapshot()
|
|
second = await service.get_snapshot()
|
|
return first, second
|
|
|
|
first, second = asyncio.run(read_twice())
|
|
|
|
assert calls == 1
|
|
assert first is expected
|
|
assert second is expected
|
|
|
|
|
|
def test_service_uses_canonical_awoooi_project_scope(monkeypatch) -> None:
|
|
service = IwoooSSecurityAssetControlPlaneService()
|
|
requested_projects: list[str] = []
|
|
|
|
class FailingContext:
|
|
async def __aenter__(self):
|
|
raise RuntimeError("stop after project scope capture")
|
|
|
|
async def __aexit__(self, exc_type, exc, traceback):
|
|
return False
|
|
|
|
def fake_db_context(project_id: str):
|
|
requested_projects.append(project_id)
|
|
return FailingContext()
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.iwooos_security_asset_control_plane.get_db_context",
|
|
fake_db_context,
|
|
)
|
|
payload = asyncio.run(service.get_snapshot())
|
|
|
|
assert requested_projects == ["awoooi"] * 9
|
|
assert payload["status"] == "degraded"
|
|
assert payload["source_status"] == "live_database_unavailable"
|
|
assert payload["summary"]["unavailable_source_count"] == 9
|
|
|
|
|
|
def test_service_bounds_source_concurrency_for_database_budget(monkeypatch) -> None:
|
|
service = IwoooSSecurityAssetControlPlaneService()
|
|
concurrency = {"active": 0, "maximum": 0}
|
|
|
|
class EmptyResult:
|
|
def fetchall(self):
|
|
return []
|
|
|
|
def one(self):
|
|
return {}
|
|
|
|
def one_or_none(self):
|
|
return None
|
|
|
|
class SlowDb:
|
|
async def execute(self, statement):
|
|
concurrency["active"] += 1
|
|
concurrency["maximum"] = max(concurrency["maximum"], concurrency["active"])
|
|
await asyncio.sleep(0.02)
|
|
concurrency["active"] -= 1
|
|
return EmptyResult()
|
|
|
|
class DbContext:
|
|
async def __aenter__(self):
|
|
return SlowDb()
|
|
|
|
async def __aexit__(self, exc_type, exc, traceback):
|
|
return False
|
|
|
|
monkeypatch.setattr(
|
|
"src.services.iwooos_security_asset_control_plane.get_db_context",
|
|
lambda project_id: DbContext(),
|
|
)
|
|
payload = asyncio.run(service._load_snapshot())
|
|
|
|
assert concurrency["maximum"] == 3
|
|
assert payload["summary"]["source_count"] == 9
|
|
assert payload["summary"]["ready_source_count"] == 9
|
|
assert payload["summary"]["unavailable_source_count"] == 0
|
|
|
|
|
|
def test_public_api_returns_live_aggregate(monkeypatch) -> None:
|
|
payload = _ready_payload()
|
|
|
|
class FakeService:
|
|
async def get_snapshot(self):
|
|
return payload
|
|
|
|
monkeypatch.setattr(
|
|
"src.api.v1.iwooos.get_iwooos_security_asset_control_plane_service",
|
|
lambda: FakeService(),
|
|
)
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
response = TestClient(app).get("/api/v1/iwooos/security-asset-control-plane")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["schema_version"] == "iwooos_security_asset_control_plane_v1"
|
|
assert data["summary"]["managed_asset_count"] == 24
|
|
assert data["summary"]["siem_stage_coverage_percent"] == 100
|
|
assert data["ai_automation"]["same_run_closed_loop_proven"] is False
|
|
assert "192.168." not in response.text
|
|
assert "raw-secret" not in response.text
|