| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- from __future__ import annotations
- import os
- import uuid
- from datetime import UTC, datetime, timedelta
- import pytest
- from sqlalchemy import text
- pytestmark = pytest.mark.integration
- def test_governance_audit_covers_six_sources_and_detects_tampering(monkeypatch):
- database_url = os.environ.get("TEST_DATABASE_URL")
- if not database_url:
- pytest.skip("TEST_DATABASE_URL is required")
- monkeypatch.setenv("DATABASE_URL", database_url)
- from app import create_app, db
- from app.core.system.governance_audit import (
- AUDIT_CATEGORIES,
- GovernanceAuditService,
- )
- from app.core.system.governance_audit_repository import (
- SqlAlchemyGovernanceAuditRepository,
- )
- app = create_app()
- app.config.update(TESTING=True)
- suffix = uuid.uuid4().hex[:10]
- ids = {name: str(uuid.uuid4()) for name in (
- "user",
- "source",
- "job",
- "asset_left",
- "asset_right",
- "candidate",
- "review",
- "merge",
- "rollback",
- "ontology",
- "ontology_version",
- "ontology_run",
- "quality_profile",
- "quality_version",
- "quality_run",
- "issue",
- "timeline",
- "query",
- "correlation",
- )}
- now = datetime.now(UTC).replace(microsecond=0)
- start = now - timedelta(minutes=1)
- end = now + timedelta(minutes=1)
- try:
- with app.app_context():
- statements = [
- (
- """
- INSERT INTO public.users (
- id, username, display_name, password_hash, status
- ) VALUES (
- CAST(:user AS uuid), :username, :username,
- 'wp12-integration-hash', 'active'
- )
- """,
- {"user": ids["user"], "username": f"wp12-{suffix}"},
- ),
- (
- """
- INSERT INTO public.auth_audit_events (
- user_id, username, event_type, success, detail,
- created_at
- ) VALUES (
- CAST(:user AS uuid), :username, 'login', TRUE,
- 'safe integration fixture', :now
- )
- """,
- {
- "user": ids["user"],
- "username": f"wp12-{suffix}",
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.ingestion_sources (
- uid, source_type, name, config, permission_scope,
- status, created_by
- ) VALUES (
- CAST(:source AS uuid), 'database', :name,
- '{"password":"source-secret"}'::jsonb,
- '{}'::jsonb, 'active', :user
- )
- """,
- {
- "source": ids["source"],
- "name": f"WP12 source {suffix}",
- "user": ids["user"],
- },
- ),
- (
- """
- INSERT INTO public.ingestion_jobs (
- uid, source_uid, job_type, status, idempotency_key,
- parser_version, parameters, statistics, actor_uid,
- attempt_count, created_at
- ) VALUES (
- CAST(:job AS uuid), CAST(:source AS uuid),
- 'database_catalog', 'published', :key, 'wp12-v1',
- '{}'::jsonb, '{}'::jsonb, :user, 1, :now
- )
- """,
- {
- "job": ids["job"],
- "source": ids["source"],
- "key": suffix.ljust(64, "0"),
- "user": ids["user"],
- "now": now,
- },
- ),
- ]
- for asset_key, marker in (
- ("asset_left", "a"),
- ("asset_right", "b"),
- ):
- statements.append(
- (
- """
- INSERT INTO public.device_assets (
- uid, asset_type, name, status, current_version,
- content_hash, attributes, created_by, updated_by
- ) VALUES (
- CAST(:uid AS uuid), 'device', :name, 'active', 1,
- :hash, '{}'::jsonb, :user, :user
- )
- """,
- {
- "uid": ids[asset_key],
- "name": f"WP12 asset {marker} {suffix}",
- "hash": marker * 64,
- "user": ids["user"],
- },
- )
- )
- statements.extend(
- [
- (
- """
- INSERT INTO public.device_entity_match_candidates (
- uid, left_asset_uid, right_asset_uid,
- canonical_asset_uid, status, suggestion_source,
- confidence, explanation, evidence_uids,
- current_version, created_by
- ) VALUES (
- CAST(:candidate AS uuid),
- CAST(:left_asset AS uuid),
- CAST(:right_asset AS uuid),
- CAST(:left_asset AS uuid), 'rolled_back',
- 'manual', 1, '[]'::jsonb, '[]'::jsonb, 2, :user
- )
- """,
- {
- "candidate": ids["candidate"],
- "left_asset": ids["asset_left"],
- "right_asset": ids["asset_right"],
- "user": ids["user"],
- },
- ),
- (
- """
- INSERT INTO public.device_entity_match_reviews (
- uid, candidate_uid, version, decision, reason,
- actor_uid, created_at
- ) VALUES (
- CAST(:review AS uuid), CAST(:candidate AS uuid),
- 1, 'approve', 'safe integration fixture',
- :user, :now
- )
- """,
- {
- "review": ids["review"],
- "candidate": ids["candidate"],
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.device_entity_merge_events (
- uid, candidate_uid, canonical_asset_uid,
- member_asset_uid, review_uid, snapshot,
- actor_uid, created_at
- ) VALUES (
- CAST(:merge AS uuid), CAST(:candidate AS uuid),
- CAST(:left_asset AS uuid),
- CAST(:right_asset AS uuid),
- CAST(:review AS uuid),
- '{"password":"merge-secret"}'::jsonb,
- :user, :now
- )
- """,
- {
- "merge": ids["merge"],
- "candidate": ids["candidate"],
- "left_asset": ids["asset_left"],
- "right_asset": ids["asset_right"],
- "review": ids["review"],
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.device_entity_merge_rollbacks (
- uid, merge_uid, candidate_uid, reason, snapshot,
- actor_uid, created_at
- ) VALUES (
- CAST(:rollback AS uuid), CAST(:merge AS uuid),
- CAST(:candidate AS uuid), 'safe rollback',
- '{"password":"rollback-secret"}'::jsonb,
- :user, :now
- )
- """,
- {
- "rollback": ids["rollback"],
- "merge": ids["merge"],
- "candidate": ids["candidate"],
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.ontologies (
- uid, code, name, owner_uid, status,
- draft_revision, created_by
- ) VALUES (
- CAST(:ontology AS uuid), :code, :name, :user,
- 'published', 1, :user
- )
- """,
- {
- "ontology": ids["ontology"],
- "code": f"WP12-{suffix}",
- "name": f"WP12 ontology {suffix}",
- "user": ids["user"],
- },
- ),
- (
- """
- INSERT INTO public.ontology_versions (
- uid, ontology_uid, version, status,
- graph_document, content_hash, created_by,
- created_at, published_at
- ) VALUES (
- CAST(:version AS uuid), CAST(:ontology AS uuid),
- 1, 'published', '{}'::jsonb, :hash, :user,
- :now, :now
- )
- """,
- {
- "version": ids["ontology_version"],
- "ontology": ids["ontology"],
- "hash": "c" * 64,
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.ontology_publish_runs (
- uid, ontology_uid, version_uid, idempotency_key,
- status, actor_uid, created_at, finished_at
- ) VALUES (
- CAST(:run AS uuid), CAST(:ontology AS uuid),
- CAST(:version AS uuid), :key, 'published',
- :user, :now, :now
- )
- """,
- {
- "run": ids["ontology_run"],
- "ontology": ids["ontology"],
- "version": ids["ontology_version"],
- "key": f"wp12-{suffix}",
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.device_quality_profiles (
- uid, code, name, created_by
- ) VALUES (
- CAST(:profile AS uuid), :code, :name, :user
- )
- """,
- {
- "profile": ids["quality_profile"],
- "code": f"wp12-quality-{suffix}",
- "name": f"WP12 quality {suffix}",
- "user": ids["user"],
- },
- ),
- (
- """
- INSERT INTO public.device_quality_profile_versions (
- uid, profile_uid, version, status, rules,
- content_hash, created_by, published_by,
- created_at, published_at
- ) VALUES (
- CAST(:version AS uuid), CAST(:profile AS uuid),
- 1, 'published', '[]'::jsonb, :hash,
- :user, :user, :now, :now
- )
- """,
- {
- "version": ids["quality_version"],
- "profile": ids["quality_profile"],
- "hash": "d" * 64,
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.device_quality_runs (
- uid, policy_version_uid, policy_hash, source_uid,
- status, total_assets, total_violations, score,
- created_by, created_at
- ) VALUES (
- CAST(:run AS uuid), CAST(:version AS uuid), :hash,
- CAST(:source AS uuid), 'success', 1, 1, 0,
- :user, :now
- )
- """,
- {
- "run": ids["quality_run"],
- "version": ids["quality_version"],
- "hash": "d" * 64,
- "source": ids["source"],
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.device_quality_issues (
- uid, issue_code, source_violation_uid,
- source_run_uid, rule_code, severity, priority,
- asset_uid, field_name, message, evidence,
- recurrence_key, occurrence_number, status,
- current_version, created_by, updated_by
- ) VALUES (
- CAST(:issue AS uuid), :code,
- CAST(:violation AS uuid), CAST(:run AS uuid),
- 'WP12-RULE', 'error', 'high',
- CAST(:asset AS uuid), 'name', 'safe message',
- '{"password":"issue-secret"}'::jsonb, :key,
- 1, 'closed', 1,
- CAST(:user AS uuid), CAST(:user AS uuid)
- )
- """,
- {
- "issue": ids["issue"],
- "code": f"WP12-{suffix}",
- "violation": str(uuid.uuid4()),
- "run": ids["quality_run"],
- "asset": ids["asset_left"],
- "key": "e" * 64,
- "user": ids["user"],
- },
- ),
- (
- """
- INSERT INTO public.device_quality_issue_timeline (
- uid, issue_uid, action, from_status, to_status,
- actor_uid, note, payload, created_at
- ) VALUES (
- CAST(:timeline AS uuid), CAST(:issue AS uuid),
- 'closed', 'pending_review', 'closed',
- CAST(:user AS uuid), 'must not be selected',
- '{"password":"timeline-secret"}'::jsonb, :now
- )
- """,
- {
- "timeline": ids["timeline"],
- "issue": ids["issue"],
- "user": ids["user"],
- "now": now,
- },
- ),
- (
- """
- INSERT INTO public.knowledge_query_audits (
- id, query_hash, user_id, roles,
- business_domain_uids, mode, retriever_counts,
- cited_points, degraded_components, correlation_id,
- latency_ms, created_at
- ) VALUES (
- CAST(:query AS uuid), :hash, CAST(:user AS uuid),
- '["admin"]'::jsonb, '[]'::jsonb, 'hybrid',
- CAST(:retriever_counts AS jsonb),
- '["point-1"]'::jsonb,
- '[]'::jsonb, CAST(:correlation AS uuid), 12, :now
- )
- """,
- {
- "query": ids["query"],
- "hash": "f" * 64,
- "user": ids["user"],
- "correlation": ids["correlation"],
- "retriever_counts": '{"canonical":1}',
- "now": now,
- },
- ),
- ]
- )
- for statement, params in statements:
- db.session.execute(text(statement), params)
- db.session.flush()
- service = GovernanceAuditService(
- SqlAlchemyGovernanceAuditRepository(db.session),
- evidence_secret="wp12-integration-secret-with-32-bytes",
- key_version="integration-v1",
- now_factory=lambda: end,
- )
- coverage = service.coverage(
- period_start=start,
- period_end=end,
- )
- assert [item["category"] for item in coverage["categories"]] == list(
- AUDIT_CATEGORIES
- )
- assert all(item["count"] >= 1 for item in coverage["categories"])
- events = service.list_events(
- period_start=start,
- period_end=end,
- page_size=100,
- )
- serialized = repr(events)
- for secret in (
- "source-secret",
- "merge-secret",
- "rollback-secret",
- "issue-secret",
- "timeline-secret",
- "must not be selected",
- ):
- assert secret not in serialized
- seal = service.create_seal(
- period_start=start,
- period_end=end,
- actor_uid=ids["user"],
- )
- assert service.verify_seal(seal["uid"])["integrity_status"] == (
- "intact"
- )
- nested = db.session.begin_nested()
- db.session.execute(
- text(
- """
- UPDATE public.auth_audit_events
- SET success = FALSE
- WHERE username = :username
- AND created_at = :now
- """
- ),
- {"username": f"wp12-{suffix}", "now": now},
- )
- db.session.flush()
- assert service.verify_seal(seal["uid"])["integrity_status"] == (
- "tampered"
- )
- nested.rollback()
- assert service.verify_seal(seal["uid"])["integrity_status"] == (
- "intact"
- )
- finally:
- with app.app_context():
- db.session.rollback()
- db.session.remove()
|