| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- from __future__ import annotations
- import os
- import uuid
- from copy import deepcopy
- from datetime import UTC, datetime, timedelta
- import pytest
- from sqlalchemy import create_engine, text
- from sqlalchemy.orm import Session
- from app.core.system.governance_audit_repository import (
- SqlAlchemyGovernanceAuditRepository,
- )
- from app.core.system.security_governance import SecurityGovernanceService
- from app.core.system.security_governance_repository import (
- SqlAlchemySecurityGovernanceRepository,
- )
- pytestmark = pytest.mark.integration
- def _uid():
- return str(uuid.uuid4())
- class ApprovalGateway:
- def __init__(self):
- self.tasks = {}
- def create_egress_task(self, request_record, workflow_uid, actor_uid):
- task = {"uid": _uid(), "status": "pending"}
- self.tasks[task["uid"]] = task
- return deepcopy(task)
- def get_task(self, uid):
- return deepcopy(self.tasks.get(uid))
- class NoopTransport:
- def deliver(self, sink, envelope):
- return {"status": "delivered", "remote_ref": "integration"}
- def test_second_domain_security_governance_and_vulnerability_closure_in_postgres():
- database_url = os.environ.get("TEST_DATABASE_URL")
- if not database_url:
- pytest.skip("TEST_DATABASE_URL is required")
- engine = create_engine(database_url)
- connection = engine.connect()
- transaction = connection.begin()
- session = Session(bind=connection)
- owner, reviewer, user, domain = _uid(), _uid(), _uid(), _uid()
- now = datetime.now(UTC)
- approvals = ApprovalGateway()
- try:
- for uid, name in ((owner, "owner"), (reviewer, "reviewer"), (user, "operator")):
- session.execute(text(
- "INSERT INTO public.users (id,username,display_name,password_hash,status) VALUES "
- "(CAST(:uid AS uuid),:username,:username,'integration-only','active')"
- ), {"uid": uid, "username": f"wp10-{name}-{uid[:8]}"})
- repository = SqlAlchemySecurityGovernanceRepository(session)
- service = SecurityGovernanceService(
- repository,
- approval_gateway=approvals,
- siem_transport=NoopTransport(),
- siem_host_allowlist={"siem.example.internal"},
- now_factory=lambda: now,
- )
- profile = service.create_classification_profile({
- "code": f"MATERIAL_{domain[:8].upper()}", "name": "备品备件分类规则",
- "business_domain_uid": domain, "default_classification": "internal",
- "rules": [
- {"field_tokens": ["phone"], "category": "personal_contact", "classification": "sensitive"},
- {"field_tokens": ["bank"], "category": "financial_account", "classification": "highly_sensitive"},
- ],
- }, actor_uid=owner)
- scan = service.scan_sensitive_sample({
- "profile_uid": profile["uid"], "resource_type": "material_master",
- "resource_uid": _uid(), "business_domain_uid": domain,
- "fields": [
- {"name": "material_code", "sample_values": ["MAT-0001"]},
- {"name": "supplier_phone", "sample_values": ["13800138000"]},
- {"name": "supplier_bank_account", "sample_values": ["6222021234567890"]},
- ],
- }, actor_uid=owner)
- assert scan["sample_retained"] is False
- assert {item["proposed_classification"] for item in scan["findings"]} == {"sensitive", "highly_sensitive"}
- reviewed = service.review_classification_finding(
- scan["findings"][0]["uid"],
- {"decision": "confirm", "final_classification": "sensitive", "reason": "供应商联系方式属于个人联系信息"},
- expected_version=1, actor_uid=reviewer,
- )
- assert reviewed["current_version"] == 2
- service.create_access_policy({
- "code": f"MATERIAL_READ_{domain[:8].upper()}", "name": "物料质量用途受控读取",
- "business_domain_uid": domain, "subject_user_uids": [user], "subject_roles": [],
- "purposes": ["material_quality"], "environments": ["test"], "actions": ["read"],
- "max_classification": "sensitive", "allowed_fields": ["material_code", "supplier_phone"],
- "review_due_at": (now + timedelta(days=7)).isoformat(),
- "expires_at": (now + timedelta(days=14)).isoformat(),
- }, actor_uid=owner)
- assert service.evaluate_access({
- "user_uid": user, "roles": ["editor"], "business_domain_uid": domain,
- "purpose": "material_quality", "environment": "test", "action": "read",
- "resource_uid": _uid(), "classification": "sensitive", "requested_fields": ["material_code"],
- })["decision"] == "authorized"
- assert service.submit_egress_request({
- "business_domain_uid": domain, "resource_uid": _uid(), "classification": "highly_sensitive",
- "purpose": "supplier_settlement", "environment": "production",
- "requested_fields": ["supplier_bank_account"], "minimized_fields": ["supplier_bank_account"],
- "masking_applied": True, "destination_zone": "partner",
- "expires_at": (now + timedelta(days=1)).isoformat(),
- }, actor_uid=user)["status"] == "denied"
- sbom = service.register_sbom({
- "artifact_name": "dataops-backend", "artifact_version": "wp10", "artifact_type": "application",
- "source_ref": "integration:wp10", "document": {
- "bomFormat": "CycloneDX", "specVersion": "1.5", "version": 1,
- "components": [{"type": "library", "name": "example-lib", "version": "1.0.0"}],
- },
- }, actor_uid=owner)
- finding = service.ingest_vulnerabilities(sbom["uid"], {
- "scanner": "trivy", "scan_ref": "integration:scan", "findings": [{
- "external_id": "CVE-2026-1000", "severity": "high", "component_name": "example-lib",
- "installed_version": "1.0.0", "fixed_version": "1.0.1", "title": "integration finding",
- }],
- }, actor_uid=owner)[0]
- finding = service.assign_vulnerability(finding["uid"], {
- "assignee_uid": user, "due_at": (now + timedelta(days=7)).isoformat()
- }, expected_version=1, actor_uid=owner)
- finding = service.resolve_vulnerability(finding["uid"], {
- "resolution_type": "patched", "resolved_version": "1.0.1", "resolution": "已升级并复扫",
- "evidence_refs": [{"type": "scan", "ref": "integration:fixed", "digest": "a" * 64}],
- }, expected_version=2, actor_uid=user)
- finding = service.close_vulnerability(finding["uid"], {"reason": "复扫无对应发现"}, expected_version=3, actor_uid=reviewer)
- assert finding["status"] == "closed"
- events = SqlAlchemyGovernanceAuditRepository(session).fetch_events(
- categories=["security_governance"], period_start=now - timedelta(minutes=1), period_end=now + timedelta(minutes=1)
- )
- assert any(item["action"] == "egress_denied" for item in events)
- serialized = repr(repository.list_findings())
- assert "13800138000" not in serialized
- assert "6222021234567890" not in serialized
- finally:
- session.close()
- transaction.rollback()
- connection.close()
- engine.dispose()
|