Skip to main content

SOC 2 and GDPR Compliance Readiness Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Move xTrakt from partial privacy/security controls to SOC 2 Type I/Type II and GDPR readiness by adding auditable controls, privacy-by-design product features, automated evidence collection, and operational governance.

Architecture: Treat compliance as a cross-cutting platform capability. Keep business features in their owning services, but centralize control evidence, privacy events, data subject workflows, retention orchestration, log redaction, and audit exports behind shared contracts and platform-level services.

Tech Stack: .NET 10, EF Core, PostgreSQL, Cloud Run, Cloud SQL, GCS, GCP KMS, Secret Manager, Pub/Sub/Cloud Scheduler, Angular 21, Vue 3/Vite, New Relic, GitHub Actions, Stripe, Twilio/SendGrid, OpenAI/Anthropic/Vertex AI.

Implementation status update - 2026-05-16: This plan is a historical baseline. The DSR MVP, compliance audit trail/export, privacy preferences/consent pieces, and cross-service DSR adapters/providers have shipped since the initial review. Remaining GDPR/SOC 2 gaps are evidence automation coverage, production smoke evidence, statutory deadline reporting, remaining rights workflows, retention/legal hold enforcement, vendor registry, access reviews, incident exercises, restore drills, and auditor-ready release manifests.

Implementation status update - 2026-05-20: Cross-service compliance audit fan-in is now end-to-end live in main:

  • A1 / A2 shipped (Platform /internal/compliance-audit/events ingestion endpoint + xTrakt.Base.HttpComplianceAuditEmitter).
  • A0 shipped — SSO client_credentials grant + compliance.audit.emit scope + 9 audit-emitter-* confidential OAuth clients + Platform ComplianceAuditEmit policy that accepts the scope claim + ClientCredentialsComplianceAuditTokenProvider (60 s refresh, semaphore-gated). xTrakt.Base 10.0.18 published to GitHub Packages via Cloud Build.
  • A3 shipped across all 8 backend services (vision/sso/ecm/sign/billing/tickets/template/portal-be) — each now references xTrakt.Base 10.0.18, registers services.AddClientCredentialsComplianceAuditEmitter(configuration), and carries Compliance:AuditTrail config sections in all environments.
  • B shipped — telemetry-consent regression tests in xtrakt-platform-fe and a telemetry audit doc; Track 4 gating + network-test checkboxes closed.
  • C1 shipped — SSO PrivilegedAccessAuditService fans every privileged event (MFA / auth / session, via existing callers) into the Platform compliance trail with safe metadata only; Platform outages cannot fail the local SSO operation.
  • C2–C7 pending. D2 + D4 shipped 2026-05-20 (umbrella-only slice — script + tests + workflow + runbook). D1 + D3 still pending — both require cross-repo/out-of-band work (branch protection on 14 repos for D1; per-backend Cloud Run deploy workflows for D3) and were intentionally scoped out of this batch. See the per-section status notes below for details.

Reference Standards

  • SOC 2: Scope the first audit around Security, Availability, Confidentiality, Privacy, and Processing Integrity. Security is mandatory for SOC 2; the other categories are relevant because xTrakt stores documents, performs AI extraction, returns structured outputs, and advertises uptime/SLA expectations.
  • GDPR: Prioritize Articles 5, 6, 7, 12-22, 25, 28, 30, 32, 33, 34, 35, and Chapter V. These map to lawful basis, consent records, data subject rights, privacy by design/default, processors/subprocessors, RoPA, security, breach notification, DPIA, and international transfers.
  • Important: This plan creates engineering and audit-readiness controls. Final legal wording, DPA/SCC execution, SOC 2 scoping, and auditor acceptance require counsel and an accredited SOC 2 auditor.

Current Baseline From Repository Review

Existing Strengths

  • The repo already has GDPR artifacts: docs/compliance/ropa.md, docs/compliance/dpia-document-extraction-fraud-detection.md, and docs/compliance/tia-international-transfers.md.
  • docs/superpowers/plans/2026-04-28-lgpd-gdpr-compliance.md already covers cookie consent, signup consent, New Relic/Fingerprint gating, HSTS, GeoIP over HTTPS, and PII scrubbing.
  • xtrakt.webapi/src/xTraktStartupBase.cs includes CORS origin validation, HTTPS/HSTS behavior, CSRF protection for cookie sessions, rate limiting, correlation logging, and authenticated defaults.
  • xtrakt-sso-be/src/Web/Startup.cs includes SSO rate limiting, secure cookie policy, HSTS, OpenIddict validation, MFA-related controllers, and platform admin policies.
  • xtrakt.base/src/Security/IFieldEncryptor.cs and xtrakt.base/src/Security/AesGcmSecretProtector.cs provide encryption primitives; several newer services already use IFieldEncryptor.
  • xtrakt-ecm-be has document retention concepts (RetentionPolicyId, PurgeScheduledFor) and GCS delete/move operations.
  • xtrakt-platform-be has Terms of Service entities/endpoints and the platform FE has a Terms editor/acceptance modal.
  • xtrakt-site/src/app.js has a public-site cookie consent implementation with Global Privacy Control awareness.

Primary Gaps

  • SOC 2 evidence is not automated. The root .github/workflows currently does not provide broad build/test/security/evidence gates across all services.
  • Audit trails are service-local and not uniformly immutable, exportable, hash-chained, or mapped to SOC 2 controls.
  • Consent and privacy preferences are incomplete across all product surfaces. Public site consent exists, but product apps still need platform-wide preference center and telemetry gating.
  • GDPR data subject request handling now exists as an MVP across Platform orchestration plus SSO, Vision, ECM, Sign, Billing, Template, Tickets, and log adapters/providers. Remaining gaps are production smoke evidence, statutory deadline reporting, operator runbooks, and full rectification/restriction workflow coverage.
  • Retention exists as metadata in ECM but not as an enforced, cross-service deletion/anonymization/legal-hold system.
  • Log, telemetry, raw event payload, extracted text, and AI prompt/response storage need a consistent sensitivity model and redaction policy.
  • RoPA/DPIA/TIA documents still contain placeholders and are not wired to a subprocessor registry, product configuration, evidence collection, or public trust center.
  • Vendor risk, access reviews, incident response, backup/restore drills, and change management are not tracked as recurring auditable controls.

Proposed New Product Features

  1. Compliance Center in Administration: shows SOC 2/GDPR readiness status, evidence snapshots, policy versions, audit export buttons, and open compliance tasks.
  2. Data Subject Request Portal: lets customers create access/export/delete/rectification/restriction requests and tracks statutory deadlines.
  3. Privacy Preference Center: central user-facing cookie, analytics, functional telemetry, marketing, and product communication preferences.
  4. Consent Record Service: immutable records for terms, privacy policy, DPA version, marketing consent, cookie choices, and special processing consent.
  5. Retention and Legal Hold Manager: tenant-configurable retention policies, dry-run purge reports, legal holds, purge audit logs, and GCS/DB/search cleanup.
  6. Immutable Compliance Audit Trail: append-only events, hash chaining, exports, and mapping to SOC 2 controls.
  7. Access Review Workflow: quarterly user/admin/production-access reviews with evidence and approvals.
  8. Subprocessor and Vendor Risk Registry: versioned vendor inventory, DPA/SCC/TIA status, region, data categories, and customer notification workflow.
  9. AI Processing Controls: tenant-level provider allowlist, region policy, no-training/zero-retention flags, prompt logging mode, model version audit, and human-review flags.
  10. Incident and Breach Workflow: severity triage, 72-hour GDPR clock, customer notification templates, RCA tracking, and postmortem evidence.
  11. Trust Center: public security/privacy posture, subprocessors, data protection summary, SOC 2 status, uptime, and report-request flow.

Implementation Tracks

Track 1 — Control Matrix, Policies, and Audit Scope

Files to create/update:

  • docs/compliance/soc2/control-matrix.md

  • docs/compliance/soc2/system-description.md

  • docs/compliance/soc2/evidence-register.md

  • docs/compliance/policies/access-control-policy.md

  • docs/compliance/policies/change-management-policy.md

  • docs/compliance/policies/incident-response-policy.md

  • docs/compliance/policies/vendor-management-policy.md

  • docs/compliance/policies/data-retention-policy.md

  • docs/compliance/policies/secure-sdlc-policy.md

  • docs/compliance/policies/ai-data-processing-policy.md

  • docs/compliance/ropa.md

  • docs/compliance/dpia-document-extraction-fraud-detection.md

  • docs/compliance/tia-international-transfers.md

  • Define SOC 2 scope: services, infrastructure, trust service categories, customers in scope, excluded systems, and third-party dependencies.

  • Map SOC 2 controls to existing and planned evidence: CI runs, deployments, access reviews, incidents, backup tests, vulnerability scans, audit exports, DSR logs, retention runs.

  • Remove all [TBC] placeholders from RoPA/DPIA/TIA or explicitly mark them as legal-owner blockers with due dates.

  • Add a policy version table and review cadence to every compliance policy.

  • Create a release gate that blocks audit readiness if a required policy or evidence source is missing an owner.

Validation:

  • rg -n "\\[TBC\\]|TODO|to be confirmed" docs/compliance docs/security returns only approved legal blockers with owners.
  • Control matrix maps every scoped SOC 2 criterion to at least one evidence artifact.

Track 2 — Automated Evidence and Security CI

Files to create/update:

  • .github/workflows/security-ci.yml

  • .github/workflows/compliance-evidence.yml

  • scripts/compliance/collect_evidence.py

  • scripts/compliance/render_evidence_index.py

  • docs/compliance/evidence/README.md

  • docs/compliance/evidence/evidence-manifest.schema.json

  • docs/compliance/evidence/evidence-manifest.json

  • Add a root security CI workflow that runs dependency checks for .NET and npm, secret scanning, SBOM generation, container scanning, and changed-service build/test jobs.

  • Add evidence collection that records commit SHA, actor, workflow URL, build/test results, vulnerability output, deployed image digests, migrations, and release approvals.

  • Store evidence manifests in docs/compliance/evidence/ for local audit review and optionally upload immutable artifacts to GCS.

  • Add monthly scheduled evidence snapshots for access reviews, vendor registry, policy versions, incident log, and backup/restore status.

  • Add a scripts/compliance/collect_evidence.py --validate mode for auditor-ready completeness checks.

Validation:

  • A pull request shows build/test/security scan statuses.
  • python3 scripts/compliance/collect_evidence.py --validate fails if a required control has no evidence artifact.
  • A generated evidence manifest can answer: who changed what, when, what was tested, what vulnerabilities were open, and what was deployed.

Implementation started:

  • Created .github/workflows/security-ci.yml with initial secret-pattern scan, npm audit evidence, .NET vulnerable package evidence, SBOM generation, Trivy filesystem/container-configuration scan, changed-service scope evidence, evidence manifest generation, index rendering, artifact upload, and a hard gate for secret-pattern matches.
  • Created .github/workflows/compliance-evidence.yml with a monthly/manual compliance evidence snapshot covering compliance docs and evidence automation scripts/workflows.
  • Created scripts/compliance/collect_evidence.py with manifest generation, artifact hashing, required-control validation, and restricted-data guard validation.
  • Created scripts/compliance/render_evidence_index.py with Markdown manifest/control/artifact index rendering.
  • Created scripts/compliance/discover_changed_services.py with Git-diff based service discovery and local fallback inventory.
  • Created scripts/compliance/run_changed_service_checks.py with changed-service .NET/npm build execution and evidence output.
  • Updated security-ci.yml to upload build evidence before enforcing changed-service build failures.
  • Created scripts/compliance/collect_release_metadata.py with repository, workflow, commit, actor, PR, changed-services, build-status, and scan-status evidence.
  • Created scripts/compliance/collect_migration_metadata.py with EF/SQL migration detection and release migration-review evidence.
  • Created scripts/compliance/collect_deployment_metadata.py with changed containerized-service detection, Dockerfile hashing, expected image names, and optional deployed image digest/revision capture.
  • Updated collect_release_metadata.py to collect GitHub PR review/approval metadata when GITHUB_TOKEN and pull-request context are available.
  • Updated security CI to write release manifests and indexes under docs/compliance/evidence/releases/latest/ while uploading raw scan/build artifacts.
  • Updated monthly evidence snapshots to write manifests and indexes under docs/compliance/evidence/monthly-snapshots/latest/.
  • Added access-review, vendor/subprocessor, incident, backup/restore, policy, and evidence automation artifacts to the monthly snapshot.
  • Updated render_evidence_index.py to generate links relative to the rendered index location.
  • Expand changed-service checks with service-specific tests where safe and deterministic. (D2 shipped 2026-05-20 — run_for_service in scripts/compliance/run_changed_service_checks.py, 7 pytest covers .NET/Angular/Vite/no-op/evidence-write/TRX-parse/JUnit-parse, Changed-service test evidence step added to .github/workflows/security-ci.yml with artifact upload to docs/compliance/evidence/changed-service-checks/.)
  • Connect deployment workflows to provide real image digests and Cloud Run revisions.
  • Decide approval threshold policy for release gates. (D4 shipped 2026-05-20 — policy in docs/compliance/runbooks/secure-release-review.md § 5.1 + cross-ref in docs/compliance/change/change-risk-classification-standard.md. meets_approval_threshold + compute_risk_class + HIGH_RISK_PATH_GLOBS + --enforce-approval-threshold CLI in scripts/compliance/collect_release_metadata.py. 18 pytest cases. Wired into security ci as a soft-warn step (continue-on-error: true, --soft-warn) until 2026-05-26; promote to blocking on that date by dropping both flags.)

Documentation completed without product-code changes:

  • Created docs/compliance/evidence/README.md.
  • Created docs/compliance/evidence/evidence-manifest.schema.json.
  • Created docs/compliance/evidence/evidence-manifest.example.json.
  • Created docs/compliance/evidence/evidence-automation-design.md.
  • Created docs/compliance/templates/secure-release-review-template.md.
  • Created docs/compliance/templates/deployment-approval-template.md.
  • Created docs/compliance/change/change-risk-classification-standard.md.
  • Created docs/compliance/templates/evidence-artifact-review-template.md.
  • Created docs/compliance/runbooks/secure-release-review.md.
  • Created docs/compliance/runbooks/security-ci-gate-spec.md.
  • Created docs/compliance/runbooks/vulnerability-management.md.
  • Created docs/compliance/exception-register.md.
  • Created docs/compliance/risk-register.md.
  • Created docs/compliance/compliance-calendar.md.
  • Created docs/compliance/system-inventory.md.
  • Created docs/compliance/data-inventory.md.
  • Created docs/compliance/checklists/release-privacy-security-checklist.md.
  • Created docs/compliance/checklists/secure-code-review-checklist.md.
  • Created docs/compliance/data-flow-maps.md.
  • Created docs/compliance/controller-processor-guidance.md.
  • Created docs/compliance/data-minimization-guidelines.md.
  • Created docs/compliance/security/threat-model.md.
  • Created docs/compliance/security/tenant-isolation-control-spec.md.
  • Created docs/compliance/security/secure-file-upload-standard.md.
  • Created docs/compliance/security/api-and-webhook-security-standard.md.
  • Created docs/compliance/security/vulnerability-severity-and-remediation-sla.md.
  • Created docs/compliance/security/penetration-test-and-security-assessment-plan.md.
  • Created docs/compliance/soc2/type-i-readiness-checklist.md.
  • Created docs/compliance/soc2/type-ii-observation-plan.md.
  • Created docs/compliance/soc2/control-owner-raci.md.
  • Created docs/compliance/soc2/trust-services-crosswalk.md.
  • Created docs/compliance/soc2/auditor-request-list.md.
  • Created docs/compliance/evidence/audit-package-assembly-guide.md.
  • Created docs/compliance/templates/control-owner-assignment-template.md.
  • Created docs/compliance/templates/auditor-request-tracker-template.md.

Track 3 — Immutable Compliance Audit Trail

Files to create/update:

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/ComplianceAuditEvent.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Repositories/IComplianceAuditEventRepository.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Infra/Mappings/ComplianceAuditEventMapping.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Infra/Repositories/ComplianceAuditEventRepository.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Application/Services/Interfaces/IComplianceAuditTrailService.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Application/Services/ComplianceAuditTrailService.cs

  • xtrakt-platform-be/src/Web/Controllers/ComplianceAuditController.cs

  • xtrakt.base/src/Compliance/ComplianceAuditEventContract.cs

  • xtrakt.base/src/Compliance/ComplianceAuditEmitter.cs

  • Create append-only ComplianceAuditEvent with TenantId, ActorUserId, ActorType, EventType, ResourceType, ResourceId, Purpose, SourceService, CorrelationId, IpHash, UserAgentHash, PayloadHash, PreviousHash, EventHash, and OccurredAt.

  • Add hash chaining per tenant/day so tampering is detectable without storing excessive PII in audit rows.

  • Add a shared emitter contract for all .NET services.

  • Emit events for authentication, MFA, API key, webhook, template, extraction, document view/download/share, signature, admin config, terms acceptance, consent change, DSR, retention, legal hold, support access, model/provider config, and billing permission changes.

  • Add CSV/JSONL export with date, tenant, resource, event type, actor, and hash verification status.

Validation:

  • Audit event creation is tested for hash-chain continuity and tenant isolation.
  • Attempting to update/delete an audit event is blocked by repository/service contract.
  • Export verifies hashes and identifies a manually altered event in a test fixture.

Documentation completed without product-code changes:

  • Created docs/compliance/audit/compliance-audit-event-taxonomy.md.
  • Created docs/compliance/runbooks/production-support-access.md.
  • Created docs/compliance/templates/production-access-request-template.md.
  • Added Platform ComplianceAuditEvent entity, mapping, repository, DI registrations, migration AddComplianceAuditEvents, and IComplianceAuditTrailService / ComplianceAuditTrailService.
  • Added focused tests for tenant/day hash-chain continuity and hashing of raw IP/user-agent metadata.
  • Added ComplianceAuditExportService, repository filtering, /compliance-audit/export, /compliance-audit/verify, JSONL/CSV rendering, and tamper/previous-hash/sequence-gap verification tests.
  • Added xTrakt.Base.Compliance event contract, emit result, and IComplianceAuditEmitter interface for cross-service emitters.
  • Added tests for the shared emitter contract and append/read-only audit repository surface.
  • Platform privacy preference updates now emit privacy_preference.updated compliance audit events without raw email/user PII in payloads.
  • Platform consent grants/withdrawals now emit consent.granted / consent.withdrawn compliance audit events with hashed network metadata.
  • Platform terms acceptance now emits terms.accepted compliance audit events alongside the legacy GDPR acceptance audit row.
  • SSO admin security configuration changes now emit admin.security_config.updated privileged audit events with changed keys only, excluding config values.
  • Platform template lifecycle changes now emit template.created, template.updated, template.title_updated, template.deleted, template.finished, and template.reprocessed compliance audit events without raw schema metadata in payloads.
  • Vision provider/model configuration changes now emit ai_provider.* and ai_model.* compliance audit events without API key material.
  • Vision webhook endpoint configuration changes now emit webhook_endpoint.* compliance audit events without webhook secrets, authorization headers, request bodies, or full URLs.
  • Vision classifier lifecycle changes now emit classifier.created, classifier.updated, and classifier.deleted compliance audit events without raw file names, OCR content, or metadata/schema payloads.
  • Platform API key creation and revocation now emit api_key.created and api_key.revoked compliance audit events without plaintext key material or raw actor identifiers in payloads.
  • SSO authentication attempts now emit authentication.succeeded and authentication.failed privileged audit events without raw usernames, visitor IDs, IP addresses, or user agents in payloads.

Track 4 — PII Redaction, Telemetry Controls, and Secure Logging

Files to create/update:

  • xtrakt.base/src/Privacy/PiiRedactor.cs

  • xtrakt.base/src/Privacy/PiiClassification.cs

  • xtrakt.base/tests/unit/Privacy/PiiRedactorTests.cs

  • xtrakt.webapi/src/Middleware/ErrorResponseLoggingMiddleware.cs

  • xtrakt.webapi/src/Logging/CorrelationIdLoggingMiddleware.cs

  • xtrakt-portal-fe/src/plugins/newrelic.ts

  • xtrakt-id-fe/src/src/app/services/fingerprint.service.ts

  • All xtrakt-*-be/src/Web/newrelic.config

  • Replace raw response-body error logging with redacted structured logging; log status, correlation ID, route/path, tenant marker, and safe redacted details only. (Error-code extraction from response envelopes remains a later enhancement.)

  • Add denylisted keys for tokens, authorization headers, document fields, CPF/CNPJ, passport/ID numbers, phone, email, addresses, raw OCR, extracted text, prompt/response content, and webhook payloads.

  • Configure New Relic to exclude request/response bodies, secrets, user-entered values, raw document content, and AI prompts.

  • Gate browser telemetry behind privacy preferences unless the specific signal is strictly necessary for security and documented in the DPIA/RoPA. (2026-05-19: B3 audit confirms all analytics telemetry is gated; the single un-gated call — id-fe FingerprintService.getVisitorId() at login — is a legitimate-interest security signal documented in docs/compliance/audits/2026-05-19-telemetry-consent-audit.md § 3.2. Full user-facing Preference Center remains Track 5.)

  • Replace direct use of raw X-Forwarded-For with trusted forwarded headers/service context handling.

Validation:

  • Unit tests prove redaction for JSON, nested objects, form data, headers, and exception messages in xtrakt.base.
  • Local error simulation does not write raw emails, document text, bearer tokens, API keys, or OCR output.
  • Network tests show New Relic/Fingerprint do not load before consent where consent is required. (2026-05-19: 4-case regression suite added in xtrakt-platform-fe/src/src/app/services/privacy-preferences.service.spec.ts § "B2 — no telemetry before analytics consent". 2026-05-20: equivalent specs added in xtrakt-id-fe/src/src/app/services/newrelic-init.spec.ts (5 cases — IIFE in assets/newrelic/init.js is the only id-fe gate since index.html loads the script unconditionally) and xtrakt-portal-fe/src/privacy-preferences.spec.ts (12 cases × 2 signals — predicate-level coverage for both installNewRelic's analytics gate AND the bootstrap-side Fingerprint Pro security gate, including the asymmetric GPC behaviour). All three FEs now have automated regression coverage for the no-telemetry-before-consent contract.)

Implementation started:

  • Extended xtrakt.base/src/Security/PiiMasker.cs with MaskText and MaskJson helpers for free-text and recursive JSON redaction.
  • Extended xtrakt.base/src/Security/PiiMasker.cs with MaskFields and MaskException helpers for header/form dictionaries and exception text.
  • Added unit coverage in xtrakt.base/tests/unit/Security/PiiMaskerTests.cs for e-mail, CPF/CNPJ, passport/ID keys, phone/address keys, bearer token, API key, raw OCR, prompt, nested JSON, truncated JSON, sensitive headers/form fields, and exception messages.
  • Verified dotnet test tests/unit/xTrakt.Base.Tests.csproj passes in xtrakt.base.
  • Bumped xtrakt.base/src/xTrakt.Base.csproj to 10.0.14 for the new public redaction helpers.
  • Generated local package xtrakt.base/artifacts/package/xTrakt.Base.10.0.14.nupkg.
  • Wired xtrakt.webapi/src/Middleware/ErrorResponseLoggingMiddleware.cs to redact logged error response bodies with PiiMasker.MaskJson.
  • Updated xtrakt.webapi/src/xTrakt.WebApi.csproj to consume xTrakt.Base 10.0.14.
  • Added middleware coverage in xtrakt.webapi/tests/unit/Middleware/ErrorResponseLoggingMiddlewareTests.cs proving logs are redacted while client responses remain unchanged and structured metadata includes status, method, path, correlation id, and tenant marker.
  • Updated xtrakt.webapi/tests/unit/Context/ServiceContextTests.cs for the current UserInfo empty-string contract from xTrakt.Base.
  • Verified dotnet test tests/unit/xTrakt.WebApi.Tests.csproj --no-restore passes in xtrakt.webapi after restoring from the local xTrakt.Base 10.0.14 package.
  • Updated backend newrelic.config files in xtrakt-api-be, xtrakt-billing-be, xtrakt-platform-be, xtrakt-portal-be, xtrakt-sso-be, and xtrakt-vision-be with explicit attribute exclusions for sensitive headers, request parameters, request/response bodies, raw OCR, extracted text, prompts, responses, tokens, secrets, passwords, and API keys.
  • Verified all updated newrelic.config files with xmllint --noout.
  • Added xtrakt-portal-fe/src/privacy-preferences.ts and gated portal New Relic browser loading on explicit analytics consent, including Global Privacy Control handling.
  • Gated portal Fingerprint plugin registration on explicit security-signal preference; the identity frontend Fingerprint service remains lazy and only loads during login security collection.
  • Updated xtrakt-id-fe/src/src/assets/newrelic/init.js and xtrakt-platform-fe/src/src/assets/newrelic/init.js so window.NREUM is only initialized when analytics consent exists and Global Privacy Control is not enabled.
  • Verified frontend builds: npm run build in xtrakt-portal-fe, xtrakt-id-fe/src, and xtrakt-platform-fe/src.
  • Replaced raw X-Forwarded-For reads in xtrakt-sso-be login/MFA/password-reset/register rate limits with Connection.RemoteIpAddress after trusted ForwardedHeadersMiddleware.
  • Replaced raw X-Forwarded-For reads in xtrakt-platform-be origin IP/terms acceptance flow with the trusted RemoteIpAddress-based accessor.
  • Replaced raw X-Forwarded-For reads in public rate-limit policies for xtrakt-billing-be, xtrakt-ecm-be, and xtrakt-tickets-be.
  • Verified builds for xtrakt-sso-be, xtrakt-platform-be, xtrakt-billing-be, xtrakt-ecm-be, and xtrakt-tickets-be.
  • Verified rg -n "X-Forwarded-For" -S xtrakt-*-be xtrakt-portal-be xtrakt.webapi xtrakt.base now only reports the explanatory anti-spoofing comment in xtrakt.webapi.
  • Publish xTrakt.Base 10.0.14 to GitHub Packages so xtrakt.webapi CI/deploy restores without the local package source. (Superseded — verified 2026-05-19 that 10.0.15 (and subsequent 10.0.16, 10.0.17) on GitHub Packages contain the same PiiMasker redaction surface as 10.0.14; consumer services restore from the central feed without a local source. See plan section B1 for verification commands.)

Documentation completed without product-code changes:

  • Created docs/compliance/standards/log-redaction-standard.md.
  • Created docs/compliance/checklists/privacy-by-design-checklist.md.
  • Created docs/compliance/checklists/dpia-trigger-checklist.md.
  • Created docs/compliance/privacy/gdpr-launch-readiness-checklist.md.
  • Created docs/compliance/privacy/gdpr-control-map.md.
  • Created docs/compliance/privacy/data-subject-rights-matrix.md.
  • Created docs/compliance/retention-schedule.md.
  • Created docs/compliance/data-residency-and-transfer-map.md.
  • Created docs/compliance/runbooks/monthly-compliance-review.md.
  • Created docs/compliance/templates/monthly-compliance-review-template.md.
  • Created docs/compliance/templates/policy-review-template.md.
  • Created docs/compliance/support-data-handling-standard.md.

Files to create/update:

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/ConsentRecord.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/PrivacyPreference.cs

  • xtrakt-platform-be/src/Web/Controllers/ConsentController.cs

  • xtrakt-platform-be/src/Web/Controllers/PrivacyPreferencesController.cs

  • xtrakt-platform-fe/src/src/app/admin/terms-editor.component.ts

  • xtrakt-platform-fe/src/src/app/menu/menu.component.ts

  • xtrakt-id-fe/src/src/app/signup/signup.component.ts

  • xtrakt-id-fe/src/src/app/signup/signup.component.html

  • xtrakt-portal-fe/src/components/CookieConsentBanner.vue

  • xtrakt-portal-fe/src/privacy-preferences.ts

  • xtrakt-portal-fe/src/services/privacy.ts

  • xtrakt-portal-fe/src/views/PrivacyPreferencesView.vue

  • xtrakt-site/src/app.js

  • Add immutable consent records for terms, privacy policy, DPA, cookies, analytics, functional telemetry, marketing, and special-category processing addenda.

  • Add server-side versioned acceptance, including policy version, locale, IP hash, user agent hash, tenant, user, purpose, lawful basis, and withdrawal timestamp.

  • Require explicit terms/privacy acceptance during signup and invite acceptance.

  • Add user-facing privacy preferences page for analytics, functional telemetry, product emails, marketing emails, and AI feature preferences.

  • Keep the marketing site cookie banner and product preference center aligned on consent schema and policy version.

Validation:

  • Signup cannot complete without required terms/privacy acceptance.
  • With analytics disabled, product telemetry does not load browser agents.
  • Consent withdrawal is recorded and prevents future non-essential tracking.

Implementation started:

  • Added platform domain entities PrivacyPreference and ConsentRecord.
  • Added EF mappings, repositories, DbSets, DI registration, and migration AddPrivacyPreferencesAndConsentRecords.
  • Added IPrivacyComplianceService / PrivacyComplianceService for user-scoped preferences and append-only consent records.
  • Stored consent forensic metadata as SHA-256 hashes of trusted client IP and User-Agent instead of raw values.
  • Added client DTOs under xTrakt.Platform.Client/Privacy.
  • Added handlers and endpoints: GET /privacy-preferences/me, PUT /privacy-preferences/me, POST /consents, and GET /consents/me.
  • Verified dotnet build src/Web/Web.csproj passes in xtrakt-platform-be.
  • Wired xtrakt-portal-fe to load/cache user privacy preferences from the platform API before New Relic/Fingerprint bootstrap decisions.
  • Added the portal Privacy Preferences page, navigation item, preference save flow, and append-only consent record creation for each consent category.
  • Completed i18n coverage for the new privacy page and related public-link status strings across all portal locales.
  • Verified npm run i18n:check and npm run build pass in xtrakt-portal-fe.
  • Added required Terms of Service / Privacy Policy acceptance to direct signup and invitation signup in xtrakt-id-fe.
  • Added server-side /Auth/register validation in xtrakt-sso-be so registration is rejected unless terms/privacy acceptance and a positive GDPR version are supplied.
  • Stored accepted GDPR version on newly created SSO users.
  • Verified xtrakt-id-fe: signup spec, npm run i18n:check, and npm run build.
  • Verified xtrakt-sso-be: dotnet build src/Web/Web.csproj.
  • Browser-checked direct signup at http://localhost:4100/auth-ui/signup; Terms/Privacy checkbox renders in the first step.
  • Wired xtrakt-platform-fe bootstrap to fetch /privacy-preferences/me, cache the product consent schema, and load New Relic only after analytics consent is resolved.
  • Removed static New Relic loading from xtrakt-platform-fe/src/index.html so the browser agent no longer starts before product privacy preferences are checked.
  • Verified xtrakt-platform-fe: targeted privacy-preferences service spec and npm run build.
  • Resolved existing xtrakt-platform-fe secondary-locale divergence; npm run i18n:check, npm run lint:i18n-parity, and npm run build pass.
  • Added xtrakt-id-fe PrivacyConsentService to create append-only platform consent records for required signup Terms of Service and Privacy Policy acceptance.
  • Wired direct signup and invitation signup to record required consent records after platform session creation, without blocking the user if the audit API is temporarily unavailable.
  • Verified xtrakt-id-fe: privacy consent service spec, signup spec, and npm run build.
  • Added explicit consent replay/backfill handling for users who completed signup while the append-only audit API was unavailable.
  • Queued failed consent records in local storage under xtrakt_pending_consent_records_v1 and replayed them before the next signup consent write.
  • Verified xtrakt-id-fe: privacy consent retry spec, signup spec, npm run i18n:check, and npm run build.
  • Added xtrakt-platform-fe user-facing Privacy Preferences page under Company, with controls for analytics, functional telemetry, security signals, product emails, marketing emails, and AI feature preferences.
  • Added the platform route /company/privacy-preferences and menu entries for both the legacy menu and the new shell menu.
  • Extended PrivacyPreferencesService with typed getPreferences and updatePreferences API calls, response-envelope normalization, and consent-cache refresh after saves.
  • Added EN/PT-BR copy for the platform Privacy Preferences page and fallback parity keys for all secondary locales.
  • Verified xtrakt-platform-fe: privacy preference service spec, privacy preference component spec, npm run i18n:check, npm run lint:i18n-parity, and npm run build.
  • Covered /company/privacy-preferences with service/component specs and production build; authenticated browser evidence remains optional operational evidence because hosted-login credential entry was blocked in the in-app browser.
  • Covered Platform privacy preference and consent APIs through service/build verification pending a broader shared API test harness.

Documentation completed without product-code changes:

  • Created docs/compliance/privacy/consent-and-preferences-model.md.
  • Created docs/compliance/privacy/privacy-notice-content-map.md.

Track 6 — GDPR Data Subject Request Workflow

Files to create/update:

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/DataSubjectRequest.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/DataSubjectRequestArtifact.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Application/Services/DataSubjectRequestOrchestrator.cs

  • xtrakt-platform-be/src/Web/Controllers/DataSubjectRequestsController.cs

  • xtrakt-platform-fe/src/src/app/admin/privacy-requests/*

  • Service adapters for SSO, Billing, Vision, ECM, Sign, Template, Tickets, Platform.

  • Implement request types: access, portability/export, rectification, erasure, restriction, objection, and support-assist for customer-controller requests.

  • Track statutory due date, requester identity verification, controller/customer approval, legal basis, assigned owner, status, and completion evidence.

  • Add service adapters that can search by user ID, email, phone, tenant ID, subject identifiers, document metadata, and customer-provided subject keys.

  • Generate export packages as signed ZIPs with JSON metadata, CSV summaries, source documents where legally allowed, and an explanation file.

  • Implement erasure/anonymization orchestration with legal-hold and statutory-retention exceptions.

  • Emit compliance audit events for every DSR state transition and data access.

Validation:

  • A seeded user can export all account, login, billing, consent, template, extraction, document, signature, ticket, and webhook-log references.
  • Erasure dry-run reports exact records/objects that would be deleted or anonymized.
  • Legal hold prevents deletion and records the reason.

Implementation started:

  • Added DataSubjectRequest and DataSubjectRequestArtifact domain entities in xtrakt-platform-be.
  • Added EF mappings, repository, DbSets, DI registration, and migration AddDataSubjectRequests.
  • Added platform client DTOs for DSR create/list/get/status-update responses and artifacts.
  • Added IDataSubjectRequestService / DataSubjectRequestService with 30-day statutory due date calculation, tenant-scoped lookup/listing, subject identifier hashes for search support, status transitions, and optional evidence artifact creation.
  • Added handlers and endpoints: POST /data-subject-requests, GET /data-subject-requests, GET /data-subject-requests/{id}, and PATCH /data-subject-requests/{id}/status.
  • Added validation for required legal basis, required subject identifier, allowed GDPR request types, and allowed request statuses.
  • Verified dotnet build src/Web/Web.csproj passes in xtrakt-platform-be.
  • Added initial DSR data inventory response contracts and GET /data-subject-requests/{id}/data-inventory.
  • Added tenant-scoped lookup support for existing Platform privacy preferences and consent records by DSR subject identifiers.
  • Inventory now reports Platform privacy preference and consent-history resources with export eligibility, erasure eligibility, and retention-exception metadata.
  • Re-verified dotnet build src/Web/Web.csproj passes in xtrakt-platform-be.
  • Refactored DSR inventory into IDataSubjectDataInventoryProvider so each service adapter can contribute inventory items without coupling directly to DataSubjectRequestService.
  • Added PlatformDataSubjectDataInventoryProvider as the first provider implementation for platform privacy preferences and consent records.
  • Re-verified dotnet build src/Web/Web.csproj passes after the provider extraction.
  • Added a dedicated internal SSO DSR inventory endpoint, shared service-auth validator, SSO inventory service, and repository count methods for account identity, active sessions, MFA setup, authentication logs, and password-reset records.
  • Added Platform SSO client contracts and SsoDataSubjectDataInventoryProvider so Platform DSR inventory can aggregate SSO data with fail-soft adapter status when the SSO endpoint or service secret is unavailable.
  • Re-verified dotnet build src/Web/Web.csproj passes in both xtrakt-sso-be and xtrakt-platform-be.
  • Added a dedicated internal Billing DSR inventory endpoint, token validator, billing inventory service, and repository count/look-up methods for customer profile, company membership, subscriptions, payment-method metadata, invoices, subscription history, and usage transactions.
  • Added Platform local Billing DSR Refit client and BillingDataSubjectDataInventoryProvider so the Platform inventory aggregator can include Billing without waiting for a published xTrakt.Billing.Client package.
  • Re-verified dotnet build src/Web/Web.csproj passes in both xtrakt-billing-be and xtrakt-platform-be.
  • Added a tenant-scoped internal Vision DSR inventory endpoint, Vision inventory service, repository count methods, and focused test coverage for usage, classifiers, classifier files, and agent runs without returning raw document/model payloads.
  • Added Platform local Vision DSR Refit client and VisionDataSubjectDataInventoryProvider so the Platform inventory aggregator can include Vision with fail-soft adapter status.
  • Re-verified dotnet test tests/xTrakt.Vision.IntegrationTests/xTrakt.Vision.IntegrationTests.csproj --filter DataSubjectInventoryServiceTests --no-restore, dotnet build src/Web/Web.csproj in xtrakt-vision-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added a tenant-scoped internal ECM DSR inventory endpoint, ECM inventory service, repository count methods, and focused test coverage for documents, versions, tags, extracted searchable text, retention state, purge schedule, locks, share links, policies, and audit events without returning file bytes, storage URIs, token hashes, extracted text, or raw event payloads.
  • Added Platform local ECM DSR Refit client and EcmDataSubjectDataInventoryProvider so the Platform inventory aggregator can include ECM with fail-soft adapter status; added EcmServices:BaseUrl configuration for default/dev/local profiles.
  • Re-verified dotnet test tests/unit/xTrackt.Ecm.Tests.csproj --filter DataSubjectInventoryServiceTests --no-restore, dotnet build src/Web/Web.csproj in xtrakt-ecm-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added a tenant-scoped internal Sign DSR inventory endpoint, Sign inventory service, aggregate repository counts, and focused test coverage for envelopes, sealed artifacts, document metadata, signer identity records, signature fields, signature audit events, active auth tokens, webhook endpoints, and webhook delivery attempts without returning file bytes, storage identifiers, hashes, public-link secrets, OTP values, or forensic bodies.
  • Added Platform local Sign DSR Refit client and SignDataSubjectDataInventoryProvider so the Platform inventory aggregator can include Sign with fail-soft adapter status; added SignServices:BaseUrl configuration for default/dev/local profiles.
  • Re-verified dotnet test tests/Web.Tests/Web.Tests.csproj --filter DataSubjectInventoryServiceTests --no-restore, dotnet build src/Web/Web.csproj in xtrakt-sign-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added a tenant-scoped internal Template DSR inventory endpoint, Template inventory service, aggregate repository counts, and focused test coverage for templates, published templates, versions, placeholder keys, render logs, render cache entries, and monthly quota usage without returning DOCX/PDF bytes, storage identifiers, cache hashes, or render payloads.
  • Added Platform local Template DSR Refit client and TemplateDataSubjectDataInventoryProvider so the Platform inventory aggregator can include Template with fail-soft adapter status; added TemplateServices:BaseUrl configuration for default/dev/local profiles.
  • Re-verified dotnet test tests/unit/xTrackt.Template.Tests.csproj --filter DataSubjectInventoryServiceTests --no-restore, dotnet build src/Web/Web.csproj in xtrakt-template-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added a tenant-scoped internal Tickets DSR inventory endpoint, Tickets inventory service, aggregate repository counts, and focused test coverage for support users, tickets, open ticket states, ticket messages, internal notes, attachments, and public ticket links without returning message bodies, attachment paths, public tokens, or token verification digests.
  • Added Platform local Tickets DSR Refit client and TicketsDataSubjectDataInventoryProvider so the Platform inventory aggregator can include Tickets with fail-soft adapter status; added TicketsServices:BaseUrl configuration for default/dev/local profiles.
  • Re-verified dotnet test tests/unit/Web.Tests.csproj --filter DataSubjectInventoryServiceTests --no-restore, dotnet build src/Web/Web.csproj in xtrakt-tickets-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added Platform operational-log inventory coverage for GDPR terms acceptance audits, tenant API usage logs, webhook usage logs, and webhook delivery references without returning raw request/response content, network identifiers, or delivery detail bodies.
  • Added focused test coverage for PlatformOperationalLogDataInventoryProvider and re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter PlatformOperationalLogDataInventoryProviderTests --no-restore plus dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added MVP DSR export package generation in Platform: POST /data-subject-requests/{id}/export-package returns a ZIP payload as base64 with manifest.json, inventory.json, README.txt, SHA-256 integrity hash, included file list, and an evidence artifact row on the DSR.
  • Added inventory-summary.csv to Platform DSR export packages so auditors and request handlers get a spreadsheet-friendly cross-service resource summary without source documents, extracted text, webhook bodies, secrets, hashes, or storage object contents.
  • Added first per-service DSR source export provider for Platform-owned privacy preferences and consent records, adding safe platform/privacy-preferences.json and platform/consent-records.json files to export packages when matching records exist.
  • Added per-source-file manifest metadata for DSR export packages so each adapter-emitted file records its content type, UTF-8 byte size, and SHA-256 hash inside manifest.json.
  • Hardened DSR export package source-file handling so adapter-emitted ZIP entry names cannot be empty, absolute, traversal paths, backslash paths, reserved package files, or duplicates.
  • Added focused ZIP/package test coverage for DataSubjectExportPackageBuilder and re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore plus dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Add per-service export adapters for source data that is legally allowed to be exported.
  • Added an inventory-backed per-service DSR source export provider that emits sanitized */exportable-inventory.json metadata files for export-eligible SSO, Billing, Vision, ECM, Sign, Template, and Tickets references, while leaving Platform-owned source records to the concrete Platform provider.
  • Registered the inventory-backed source export provider in Platform DI so DSR export packages include per-service exportable reference files alongside the manifest, inventory, CSV summary, README, and Platform privacy/consent files.
  • Added focused test coverage for sanitized non-Platform source export files and re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~InventoryBackedDataSubjectSourceExportProviderTests -v minimal, dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "FullyQualifiedName~InventoryBackedDataSubjectSourceExportProviderTests|FullyQualifiedName~DataSubjectExportPackageBuilderTests|FullyQualifiedName~PlatformDataSubjectSourceExportProviderTests|FullyQualifiedName~DataSubjectErasureDryRunBuilderTests|FullyQualifiedName~DataSubjectErasureExecutionOrchestratorTests" --no-restore -v minimal, and dotnet build src/Web/Web.csproj -v minimal in xtrakt-platform-be-dsr-track6.
  • Added detached HMAC package signatures for DSR export ZIPs via package-signature.json, response signature metadata, and a configurable Platform signer using Privacy:DsrExportSigningKey or the existing storage proxy signing key fallback.
  • Re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~DataSubjectExportPackageBuilderTests -v minimal, dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --no-restore -v minimal, and dotnet build src/Web/Web.csproj --no-restore -v minimal.
  • Added MVP erasure/anonymization dry-run in Platform: POST /data-subject-requests/{id}/erasure-dry-run converts inventory items into proposed actions (erase, retain, manual_review, blocked_by_legal_hold), totals candidate/blocked/legal-hold resources, hashes the report, and records an evidence artifact row.
  • Added focused test coverage for DataSubjectErasureDryRunBuilder and re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "DataSubjectErasureDryRunBuilderTests|DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore plus dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added MVP erasure/anonymization execution orchestration in Platform: POST /data-subject-requests/{id}/erasure-execution requires explicit confirmation, regenerates the current dry-run, blocks all adapters if legal hold is present, skips retained resources, records execution evidence, and completes the DSR only when all supported eligible resources execute successfully.
  • Added first Platform execution adapter for privacy_preference anonymization and repository support to scrub user identifiers and preference flags while preserving an evidence-friendly row.
  • Added focused test coverage for legal-hold blocking and adapter execution/skipping behavior; re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "DataSubjectErasureExecutionOrchestratorTests|DataSubjectErasureDryRunBuilderTests|DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore plus dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added SSO erasure/anonymization execution: internal service-auth endpoint, confirmation-required handler, user anonymization, session token/device/IP/user-agent scrubbing, MFA setup deletion, password-reset invalidation, Platform Refit contract, and SsoDataSubjectErasureExecutionAdapter.
  • Added focused Platform adapter coverage for SSO erasure execution and re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "SsoDataSubjectErasureExecutionAdapterTests|DataSubjectErasureExecutionOrchestratorTests|DataSubjectErasureDryRunBuilderTests|DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore, dotnet build src/Web/Web.csproj in xtrakt-sso-be, and dotnet build src/Web/Web.csproj in xtrakt-platform-be.
  • Added Billing erasure/anonymization execution: internal token-protected endpoint, confirmation-required handler, customer profile anonymization, statutory-retention blocking for invoices/subscriptions/payment/usage resources, Platform Refit contract, and BillingDataSubjectErasureExecutionAdapter.
  • Added focused Billing service and Platform adapter coverage; re-verified dotnet test tests/unit/xTrakt.Billing.Tests.csproj --filter DataSubjectErasureExecutionServiceTests --no-restore, dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "BillingDataSubjectErasureExecutionAdapterTests|SsoDataSubjectErasureExecutionAdapterTests|DataSubjectErasureExecutionOrchestratorTests|DataSubjectErasureDryRunBuilderTests|DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore, and dotnet build src/Web/Web.csproj in both xtrakt-billing-be and xtrakt-platform-be.
  • Added Vision erasure/deletion execution: authenticated tenant-scoped endpoint, confirmation-required handler, classifier training file deletion before classifier deletion, retention blocking for usage/agent-run resources, Platform Refit contract, and VisionDataSubjectErasureExecutionAdapter.
  • Added focused Vision service and Platform adapter coverage; re-verified dotnet test tests/xTrakt.Vision.IntegrationTests/xTrakt.Vision.IntegrationTests.csproj --filter "DataSubjectErasureExecutionServiceTests|DataSubjectInventoryServiceTests" --no-restore, dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter "VisionDataSubjectErasureExecutionAdapterTests|BillingDataSubjectErasureExecutionAdapterTests|SsoDataSubjectErasureExecutionAdapterTests|DataSubjectErasureExecutionOrchestratorTests|DataSubjectErasureDryRunBuilderTests|DataSubjectExportPackageBuilderTests|PlatformOperationalLogDataInventoryProviderTests" --no-restore, and dotnet build src/Web/Web.csproj in both xtrakt-vision-be and xtrakt-platform-be.
  • Added Platform erasure execution adapters for ECM, Sign, Template, and Tickets, with Refit execution contracts and DI registration.
  • Added internal confirmed erasure execution endpoints/services for ECM, Sign, Template, and Tickets.
  • Implemented conservative service-side blocking for ECM, Sign, Template, and Tickets aggregate inventory rows so DSR execution cannot accidentally delete tenant-wide resources before subject-scoped selectors exist.
  • Added the first concrete Tickets subject-scoped erasure execution: matching public ticket links are revoked, and closed/resolved requester ticket metadata is anonymized while open tickets remain blocked for review.
  • Added concrete Template render-cache erasure execution: matching rendered PDF cache objects are deleted from storage and their cache rows are removed using tenant plus creator audit metadata; added render-cache audit columns/index migration.
  • Added concrete Template draft-template erasure execution: unpublished draft templates created by the subject are deleted with their stored DOCX version objects, while published templates remain retained.
  • Added concrete Template draft-version erasure execution for template_version DSR items: draft template version objects are deleted via the subject-scoped draft-template selector, while published versions remain retained.
  • Added concrete Template draft-key erasure execution for template_key DSR items: draft template keys are deleted with their draft template versions via the subject-scoped draft-template selector, while published keys remain retained.
  • Added concrete Sign active-auth-token erasure execution: matching active signer OTP tokens are revoked by tenant plus subject email while signature evidence, signer identity, and sealed artifacts remain unchanged.
  • Hardened Sign active-auth-token erasure for encrypted signer PII: execution now loads active tenant token candidates, decrypts signer email for matching, and revokes only matching subject tokens.
  • Added concrete Sign draft-signer erasure execution: matching signer PII and draft signing-link secrets are scrubbed for draft envelopes by tenant plus subject email, while running, finished, sealed, or otherwise non-draft envelope signers remain retained.
  • Added concrete Sign draft-signature-field erasure execution: filled values are cleared for matching signature fields on draft envelopes by tenant plus subject email, while field layout/type and non-draft signature evidence remain retained.
  • Aligned Sign inventory erasure eligibility with implemented draft-only signer and signature-field erasure execution, while keeping running/completed/sealed signature evidence retention exceptions explicit.
  • Added concrete ECM share-link erasure execution: matching active public document share links are revoked and link secrets plus recipient email are scrubbed while document records and files remain unchanged.
  • Added concrete ECM document-lock erasure execution: edit locks owned by the subject user are deleted by tenant plus subject user id, while document records, versions, files, tags, and audit events remain unchanged.
  • Added concrete ECM extracted-text erasure execution: OCR/search text is cleared for subject-created document versions without parent retention restrictions, while source files, document metadata, and retention-managed versions remain unchanged or blocked for review.
  • Added concrete ECM document-tag erasure execution: subject-created tags are deleted from documents without parent retention restrictions, while source files, document records, audit events, and retention-managed tags remain unchanged or blocked for review.
  • Added concrete ECM document metadata erasure execution: subject-created document names, descriptions, and classifications are anonymized when no parent retention restriction applies, while source files, versions, tags, and audit events remain unchanged.
  • Added concrete ECM document-version metadata erasure execution: subject-created version file names and extraction error metadata are anonymized when the parent document has no retention restriction and the version is not signature evidence, while source files, storage identifiers, checksums, extracted text, signed versions, and retention-managed versions remain unchanged or blocked for review.
  • Added concrete Tickets customer-message erasure execution: customer-authored messages on closed/resolved requester tickets are redacted by tenant plus subject email, while messages on open tickets remain blocked for support/legal review.
  • Added concrete Tickets attachment erasure execution: attachments linked to customer-authored messages on closed/resolved requester tickets are deleted by tenant plus subject email, while open-ticket attachments remain blocked for support/legal review.
  • Aligned Tickets inventory erasure eligibility with implemented execution for requester ticket metadata and customer-authored messages, while keeping open-ticket and internal-note retention exceptions explicit.
  • Updated Platform erasure dry-runs so erasure-eligible resources with conditional retention/review notes still execute through service adapters, allowing each service to erase safe records and return per-record retention blocks instead of being skipped as manual-review-only rows.
  • Add subject-scoped record selectors and concrete delete/anonymize repository methods for ECM, Sign, Template, and Tickets so currently blocked erasure-eligible resources can execute safely.
  • Wired Platform DSR operations to IComplianceAuditTrailService for create, status updates, inventory reads, export package generation, erasure dry-run generation, and erasure execution reports with non-PII payloads.
  • Added focused DataSubjectRequestServiceAuditTests coverage for DSR audit emission and raw subject PII exclusion.
  • Emit immutable compliance audit events for every DSR state transition and data access after Track 3 audit trail is implemented.

Documentation completed without product-code changes:

  • Created docs/compliance/privacy/dsr-workflow.md.
  • Created docs/compliance/privacy/dsr-identity-verification-standard.md.
  • Created docs/compliance/privacy/data-export-package-spec.md.
  • Created docs/compliance/templates/dsr-record-template.md.

Files to create/update:

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/RetentionExecution.cs

  • xtrakt-platform-be/src/xTrakt.Platform.Application/Services/RetentionOrchestrator.cs

  • xtrakt-platform-be/src/Web/Controllers/RetentionController.cs

  • xtrakt-ecm-be/src/xTrackt.Ecm.Domain/Entities/Document.cs

  • xtrakt-ecm-be/src/xTrackt.Ecm.Infra/Services/GcsDocumentStorageService.cs

  • xtrakt-sign-be/src/xTrackt.Sign.Infra/Services/GcsDocumentStorageService.cs

  • xtrakt-template-be/src/xTrackt.Template.Infra/Services/GcsTemplateStorageService.cs

  • xtrakt-platform-fe/src/src/app/admin/retention/*

  • Implement retention policies by tenant, document type, data class, workflow, and service.

  • Add retention dry-run, scheduled execution, legal hold, approval workflow, and purge evidence.

  • Delete/anonymize DB rows, GCS objects, search indexes, signed URLs, raw webhook payloads, prompt/response logs, extracted text, temporary upload files, and support debug attachments.

  • Add CAS reference counting before deleting shared ECM objects by hash.

  • Add GCS lifecycle policy and object metadata classification for sensitive document stores. (Object metadata classification is implemented for new ECM, Sign, and Template GCS uploads; bucket lifecycle rule rollout still needs a non-destructive ops decision.)

Validation:

  • Dry-run output matches seeded records and objects.
  • Purge deletes the object from GCS, removes search results, anonymizes retained audit rows, and emits immutable audit events.
  • Legal hold blocks purge across all service adapters.

Implementation started:

  • Added Platform retention control-plane entities for tenant-scoped retention policies, retention executions, and execution items, including policy dimensions for service, resource type, data class, document type, workflow, action, approval requirement, and legal basis.
  • Added Platform retention dry-run/execution orchestration with adapter hooks, purge-candidate/retained/legal-hold item status, evidence hashes, immutable compliance audit events, and all-or-nothing legal-hold blocking before purge execution.
  • Added Platform /retention/policies, /retention/dry-runs, and /retention/executions endpoints plus EF migration AddRetentionPoliciesAndExecutions.
  • Added focused retention orchestrator tests proving dry-run output matches seeded adapter candidates, execution calls adapters when no legal hold exists, and legal hold blocks purge execution across adapters.
  • Added retention/classification metadata to newly uploaded ECM CAS objects, Sign source/sealed/audit PDFs, and Template source DOCX/rendered PDF cache objects.
  • Added scheduler-facing Platform retention dry-runs via POST /retention/scheduled-dry-runs, persisted schedule_id, and surfaced approval_required status when scheduled purge candidates are governed by approval-required policies.
  • Enforced approval workflow for retention execution: purge candidates tied to RequiresApproval policies now require an approval ticket before adapters are called.
  • Added focused retention tests for scheduled dry-run evidence and approval-ticket blocking; re-verified dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~RetentionOrchestratorTests --no-restore -v minimal, full Platform client tests, and dotnet build src/Web/Web.csproj --no-restore -v minimal.

Documentation completed without product-code changes:

  • Created docs/compliance/privacy/retention-and-legal-hold-workflow.md.
  • Created docs/compliance/runbooks/deletion-verification.md.
  • Created docs/compliance/templates/retention-execution-template.md.

Track 8 — Encryption, Key Management, and Data Classification

Files to create/update:

  • xtrakt.base/src/Security/IFieldEncryptor.cs

  • xtrakt.base/src/Security/EncryptionSentinels.cs

  • xtrakt.base/src/Privacy/DataClassificationAttribute.cs

  • xtrakt-vision-be/src/xTrakt.Vision.Infra/Interceptors/AuditFieldEncryptionInterceptor.cs

  • xtrakt-sign-be/src/xTrackt.Sign.Application/Services/SignerPiiExtensions.cs

  • ECM, Platform, SSO, Billing, Template mappings containing PII fields.

  • Create a data classification attribute/schema: public, internal, confidential, restricted, special-category, credential, audit-only.

  • Encrypt restricted fields: extracted text, raw OCR, prompt/response logs where stored, signature event forensic columns, signer PII, document metadata, webhook secrets, API keys, billing identifiers, login IP/UA metadata, and support attachments.

  • Add tenant-aware key selection, rotation metadata, and re-encryption jobs.

  • Add key destruction/disable procedure for enterprise offboarding where contractually allowed.

  • Document KMS IAM roles and evidence for key access reviews.

Validation:

  • Database inspection confirms sensitive fields are encrypted at rest.
  • Existing encrypted sentinel prefixes continue to decrypt after migration.
  • Key rotation job re-encrypts records without changing business-visible values.

Implementation started:

  • Added xTrakt.Base.Privacy data classification enum, attribute, and policy helper covering public, internal, confidential, restricted, special-category, credential, and audit-only data classes.
  • Added classification tests and bumped xTrakt.Base to 10.0.15; generated local package xTrakt.Base.10.0.15.nupkg.
  • Merged PR xtrakt-ai/xtrakt.base#5 on 2026-05-15 with commit 9f7c634.
  • Published xTrakt.Base 10.0.15 through Cloud Build 4759b5ab-244a-4d69-8cb2-ff4b18da28f2.
  • Added IFieldEncryptionRotationService, FieldEncryptionRotationService, candidate/result contracts, and focused rotation tests in xTrakt.Base.
  • Merged PR xtrakt-ai/xtrakt.base#6 on 2026-05-15 with commit 91202db.
  • Published xTrakt.Base 10.0.16 through Cloud Build f43fab3a-1932-49b7-9cfb-c85c3ef416ce.
  • Provisioned missing dev tenant KEK tenant-cflavio-xtrakt-ai and backfilled one legacy SSO plaintext PII row with the production KmsFieldEncryptor.
  • Verified SSO dev DB encrypted-field inspection: users.firstname_plaintext=0, users.lastname_plaintext=0, users.phone_plaintext=0, mfa.totp_plaintext=0, mfa.backup_plaintext=0.

Documentation completed without product-code changes:

  • Created docs/compliance/standards/data-classification-standard.md.
  • Created docs/compliance/standards/key-management-standard.md.
  • Created docs/compliance/security/encryption-coverage-matrix.md.
  • Created docs/compliance/runbooks/key-rotation.md.

Track 9 — Access Control, MFA, Sessions, and Access Reviews

Files to create/update:

  • xtrakt-sso-be/src/Web/Controllers/MfaController.cs

  • xtrakt-sso-be/src/Web/Controllers/SessionController.cs

  • xtrakt-sso-be/src/Web/Controllers/AdminController.cs

  • xtrakt-platform-be/src/Web/Controllers/UserController.cs

  • xtrakt-platform-fe/src/src/app/admin/access-reviews/*

  • docs/compliance/evidence/access-reviews/README.md

  • Require MFA for all administrators and all production support roles.

  • Add managed sessions with revocation, device visibility, last activity, and high-risk session alerts.

  • Add quarterly access review workflow for app roles, production IAM, GitHub org/repo permissions, GCP IAM, Stripe, New Relic, Twilio/SendGrid, and AI provider dashboards.

  • Add emergency/break-glass access with approval, expiration, and post-use review.

  • Emit immutable audit events for grants, revocations, MFA changes, and support access.

Validation:

  • Access review cannot close until every privileged account has approved/revoked status.
  • Admin without MFA cannot access security/compliance administration.
  • Session revocation invalidates refresh/session cookies.

Implementation started:

  • SSO now emits amr=mfa only after successful second-factor challenge verification.
  • SSO /admin/* authorization policy now requires both cogneris-platform-admin role and amr=mfa, blocking admin access for password-only tokens.
  • Existing SSO session endpoints list active sessions and revoke one/all sessions; repository revocation marks refresh sessions revoked.
  • Merged PR xtrakt-ai/xtrakt-sso-be#13 on 2026-05-15 with commit 017c098.
  • Deployed SSO dev Cloud Run revision xtrakt-sso-be-00098-w6j with image gcr.io/sigma-composite-478314-s6/xtrakt-sso-be:017c098; OpenID configuration probe returned HTTP 200.
  • Added SSO append-only PrivilegedAccessAuditEvent table, repository, hash-chain service, and EF migration AddPrivilegedAccessAuditEvents.
  • SSO now emits privileged audit events for MFA enable/disable and session revocation flows.
  • Added unit tests for privileged audit hash chaining and session revocation audit emission.
  • Merged PR xtrakt-ai/xtrakt-sso-be#14 on 2026-05-15 with commit 3e697f1.
  • Deployed SSO dev Cloud Run revision xtrakt-sso-be-00100-rql with image gcr.io/sigma-composite-478314-s6/xtrakt-sso-be:3e697f1; OpenID configuration probe returned HTTP 200.
  • Verified SSO dev DB migration created privileged_access_audit_events.

Documentation completed without product-code changes:

  • Created docs/compliance/runbooks/access-review.md.
  • Created docs/compliance/templates/access-review-template.md.
  • Created docs/compliance/templates/access-review-evidence-package-template.md.
  • Created docs/compliance/evidence/access-reviews/README.md.

Track 10 — Vendor, Subprocessor, and Transfer Governance

Files to create/update:

  • docs/compliance/subprocessors.yml

  • docs/compliance/vendor-risk-register.md

  • docs/scripts/compliance/render_subprocessors.py

  • xtrakt-site/src/data-protection.html

  • xtrakt-platform-fe/src/src/app/admin/vendors/*

  • Move subprocessors into a structured registry with vendor, purpose, data categories, regions, DPA status, SCC/TIA status, SOC 2/ISO evidence, review date, owner, and customer-notification flag.

  • Generate the public Data Protection subprocessor section from this registry.

  • Add vendor onboarding and annual review workflow with risk rating.

  • Add 30-day customer notice workflow for new subprocessors and material changes.

  • Track AI provider settings: no-training commitment, retention setting, region, model family, and approved use cases.

Validation:

  • Registry renders public page without manual edits.
  • Missing DPA/SCC/TIA evidence blocks vendor status from becoming approved.
  • New vendor creates a customer notification task.

Documentation completed without product-code changes:

  • Created docs/compliance/subprocessors.yml.
  • Created docs/compliance/vendor-risk-register.md.
  • Created docs/compliance/templates/vendor-review-template.md.
  • Created docs/compliance/templates/subprocessor-change-notice-template.md.
  • Created docs/compliance/checklists/vendor-onboarding-checklist.md.
  • Created docs/compliance/vendor-due-diligence-questionnaire.md.
  • Created docs/compliance/templates/vendor-due-diligence-response-template.md.
  • Created docs/compliance/vendors/vendor-exit-and-data-return-plan.md.
  • Created docs/scripts/compliance/render_subprocessors.py.
  • Created docs/scripts/compliance/vendor_governance.py.
  • Generated docs/compliance/subprocessors-public.md.
  • Generated docs/compliance/vendor-onboarding-workflow.md.
  • Generated docs/compliance/subprocessor-change-notice-tasks.md.
  • Wired xtrakt-site/src/data-protection.html to the registry-backed generated subprocessor section.

Track 11 — AI Processing Governance and Processing Integrity

Files to create/update:

  • xtrakt-vision-be/src/Web/Controllers/TenantAgentGuardsController.cs

  • xtrakt-vision-be/src/Web/Controllers/ProvidersController.cs

  • xtrakt-vision-be/src/xTrakt.Vision.Domain/Entities/*

  • xtrakt-vision-be/src/xTrakt.Vision.Application/Services/UsageService.cs

  • xtrakt-platform-fe/src/src/app/admin/ai-governance/*

  • Add tenant policies for allowed providers, regions, model classes, prompt storage mode, response storage mode, no-training requirement, and zero-retention requirement.

  • Record model version, provider, prompt hash, response hash, confidence, human-review flag, and downstream webhook delivery ID for each extraction/agent run.

  • Add field-level confidence and processing-integrity checks for high-stakes documents.

  • Add human-review requirements for fraud/risk/adverse-action workflows.

  • Provide customer-facing AI processing export for audits and DPIA support.

Validation:

  • Tenant policy blocks an extraction if the selected provider/region is not approved.
  • Extraction audit trail can reconstruct model/provider/configuration used without exposing full prompt content.
  • Low-confidence/high-risk extraction is routed to review instead of auto-finalizing.

Documentation completed without product-code changes:

  • Created docs/compliance/ai/ai-governance-control-spec.md.
  • Created docs/compliance/checklists/ai-feature-launch-checklist.md.
  • Created docs/compliance/templates/ai-model-card-template.md.
  • Created docs/compliance/templates/ai-evaluation-record-template.md.

Track 12 — Incident Response, Breach Notification, Availability, and Backups

Files to create/update:

  • docs/compliance/runbooks/security-incident-response.md

  • docs/compliance/runbooks/gdpr-breach-notification.md

  • docs/compliance/runbooks/backup-restore-drill.md

  • docs/compliance/runbooks/cloud-run-outage.md

  • xtrakt-platform-be/src/xTrakt.Platform.Domain/Entities/Incident.cs

  • xtrakt-platform-be/src/Web/Controllers/IncidentsController.cs

  • xtrakt-platform-fe/src/src/app/admin/incidents/*

  • Add incident severity levels, owners, communication channels, customer impact assessment, and 72-hour GDPR timer.

  • Add incident workflow: detection, triage, containment, eradication, recovery, notification, RCA, corrective action, and evidence export.

  • Add backup/restore drill schedule and evidence capture for Cloud SQL, GCS, secrets/config, and tenant-level recovery.

  • Add availability metrics, error-budget/SLA dashboard, and incident linkage to status page.

  • Add tabletop exercises twice per year with action items.

Validation:

  • A simulated incident produces timeline, severity, customer impact, breach decision, notification draft, RCA, and corrective actions.
  • Backup restore drill evidence includes RTO/RPO achieved and restore validation.
  • Availability report can be exported by month and tenant/service.

Documentation completed without product-code changes:

  • Created docs/compliance/runbooks/security-incident-response.md.
  • Created docs/compliance/runbooks/gdpr-breach-notification.md.
  • Created docs/compliance/runbooks/breach-decision-tree.md.
  • Created docs/compliance/runbooks/backup-restore-drill.md.
  • Created docs/compliance/runbooks/cloud-run-outage.md.
  • Created docs/compliance/availability/backup-coverage-matrix.md.
  • Created docs/compliance/templates/incident-record-template.md.
  • Created docs/compliance/templates/incident-communication-matrix-template.md.
  • Created docs/compliance/templates/security-exception-template.md.
  • Created docs/compliance/templates/customer-incident-notification-template.md.
  • Created docs/compliance/templates/gdpr-regulator-notification-template.md.
  • Created docs/compliance/business-continuity-disaster-recovery.md.
  • Created docs/compliance/runbooks/tabletop-exercise-program.md.
  • Created docs/compliance/templates/tabletop-exercise-template.md.

Track 13 — Compliance Center and Trust Center UX

Files to create/update:

  • xtrakt-platform-fe/src/src/app/admin/compliance-center/*

  • xtrakt-platform-be/src/Web/Controllers/ComplianceCenterController.cs

  • xtrakt-site/src/security.html

  • xtrakt-site/src/data-protection.html

  • xtrakt-site/src/trust.html

  • Add an internal Compliance Center with control status, evidence freshness, DSR queue, retention jobs, incidents, access reviews, vendor reviews, policy versions, and audit exports.

  • Add customer-facing Trust Center page with security overview, data protection summary, subprocessors, availability/status link, report request form, and compliance roadmap.

  • Add downloadable customer evidence package for enterprise sales: security overview, DPA template, subprocessor list, TIA summary, latest pen-test/SOC 2 status, and incident history summary.

Validation:

  • Admin can see stale/missing evidence without opening the repository.
  • Public trust page renders from structured compliance data where feasible.
  • Customer evidence package contains no secrets or internal-only details.

Documentation completed without product-code changes:

  • Created docs/compliance/trust-center-content-plan.md.
  • Created docs/compliance/customer-evidence-package.md.
  • Created docs/compliance/security-questionnaire-response-bank.md.
  • Created docs/compliance/templates/dpa-request-intake-template.md.
  • Created docs/compliance/public-claims-review-procedure.md.
  • Created docs/compliance/templates/public-claim-review-template.md.
  • Created docs/compliance/runbooks/government-access-request.md.
  • Created docs/compliance/templates/government-access-request-template.md.
  • Created docs/compliance/training/security-privacy-training-plan.md.
  • Created docs/compliance/training/policy-acknowledgement-register.md.
  • Created docs/compliance/metrics/soc2-gdpr-metrics.md.
  • Created docs/compliance/metrics/compliance-center-dashboard-spec.md.
  • Created docs/compliance/metrics/availability-slo-and-sla-targets.md.
  • Created docs/compliance/runbooks/ropa-update-procedure.md.
  • Created docs/compliance/customer-shared-responsibility-matrix.md.
  • Created docs/compliance/templates/dpa-security-annex-template.md.
  • Created docs/compliance/templates/privacy-notice-change-review-template.md.

Phase 0 — Audit Scoping and Ownership (1-2 weeks)

  • Choose SOC 2 trust service categories and auditor.
  • Confirm legal entity, DPO/privacy owner, EU/UK representative requirements, and customer contract scope.
  • Freeze the initial control matrix and evidence register.

Phase 1 — High-Risk GDPR and Logging Controls (4-6 weeks)

  • Consent records and privacy preference center.
  • Product telemetry gating.
  • PII redaction and secure logging.
  • DSR MVP: access/export/delete dry-run.
  • Retention dry-run across ECM and core platform data.

Phase 2 — SOC 2 Control Foundation (6-8 weeks)

  • Security CI and evidence automation.
  • Immutable compliance audit trail.
  • MFA/session hardening and access review workflow.
  • Incident workflow and backup restore drills.
  • Vendor/subprocessor registry.

Phase 3 — Privacy-by-Design Maturity (8-12 weeks)

  • Full cross-service DSR orchestration.
  • Retention/legal-hold enforcement.
  • Encryption rollout for all restricted fields.
  • AI governance controls and processing integrity evidence.
  • Trust Center and customer evidence package.

Phase 4 — Audit Observation Period (3-6 months)

  • Run Type I readiness assessment.
  • Remediate auditor gaps.
  • Start Type II observation period with automated evidence.
  • Perform recurring access reviews, vendor reviews, vulnerability management, incidents/tabletops, restore drills, and release approvals.

Acceptance Criteria

  • SOC 2 control matrix has evidence for every in-scope criterion.
  • Every privileged access grant, production deployment, incident, vendor approval, DSR action, retention run, and security configuration change is auditable.
  • GDPR data subject rights can be executed through a tracked workflow with export, deletion/anonymization, legal hold exceptions, and completion evidence.
  • Non-essential cookies, analytics, and product telemetry are consent-gated where required.
  • Sensitive document, OCR, AI, webhook, billing, login, and signature data is classified, encrypted or minimized, and redacted from logs.
  • Retention policies are enforced across databases, GCS, search indexes, logs, temporary files, and AI artifacts.
  • Vendor/subprocessor registry drives public transparency and internal approval gates.
  • Incident and breach workflows can meet GDPR notification timelines and SOC 2 evidence expectations.
  • Compliance Center exposes stale controls and missing evidence before an auditor or customer finds them.

Open Business Decisions

  • Confirm SOC 2 categories for first report: recommended Security, Availability, Confidentiality, Privacy, and Processing Integrity.
  • Decide whether first SOC 2 is Type I only or Type I followed by Type II observation.
  • Confirm EU/UK representative and DPO/privacy lead.
  • Decide default data residency and whether EU-only processing is an enterprise SKU.
  • Decide customer-managed keys timing and pricing.
  • Decide default document, extracted text, prompt/response, webhook log, audit log, and support ticket retention periods.
  • Confirm whether Fingerprint/device intelligence is treated as strictly necessary fraud prevention or consent-gated functional telemetry per jurisdiction.
  • Confirm AI provider zero-retention/no-training contractual status and region availability.
  • Confirm breach notification customer SLA and who signs regulator/customer notices.

Source References


Remaining Work — Detailed Tasks (Added 2026-05-19)

Scope: Tracks 2, 3, and 4 unchecked items only. Track 7 (Retention/Deletion/Legal Hold) is excluded per session decision on 2026-05-19 and will be planned separately. Steps follow TDD bite-sized format so a fresh engineer can pick them up.

Design decisions locked at planning time (2026-05-19):

  • SSO MFA + privileged events: SSO continues to write hash-chained PrivilegedAccessAuditEvent rows AND additionally emits parallel ComplianceAuditEventContract via the shared emitter so the Platform trail has the full picture. No data migration.
  • Track 3 operational-event scope: all listed event families — MFA, extraction, document view/download/share, signature, support access, billing permission changes.
  • Shared emitter wiring: each service in scope gets a real HttpComplianceAuditEmitter implementation of xTrakt.Base.Compliance.IComplianceAuditEmitter that POSTs to a new Platform internal endpoint. Service-local emitters (e.g. Vision's logger-only ComplianceAuditEmitter) are kept as IObservabilityAuditEmitter for service-local debug logging only; new code calls the shared xTrakt.Base.Compliance.IComplianceAuditEmitter.
  • Extraction event volume note: extraction/agent-run events are high-volume. Plan emits them at start/end/fail granularity only (no per-token), with a 7-day rolling aggregate review one month after rollout to decide on sampling — captured as an open ops decision below.

Implementation order

  1. Section A — Foundational shared emitter (Platform internal endpoint A1 + HttpComplianceAuditEmitter A2 in xtrakt.base). Shipped + merged.
  2. Section A0(Added 2026-05-19, after A1/A2 shipped; executed 2026-05-20.) SSO client-credentials infra + A1 policy refinement + caching token provider. Shipped + merged across 4 repos. xTrakt.Base 10.0.18 published to GitHub Packages.
  3. Section A3 — Per-service wiring of the emitter using A0's ClientCredentialsComplianceAuditTokenProvider. Shipped + merged for all 8 services.
  4. Section B — Track 4 leftovers. Shipped + merged.
  5. 🟡 Section C — Track 3 per-service emit coverage. C1 (SSO bridge) shipped + merged; C2–C7 pending.
  6. 🟡 Section D — Track 2 CI/release polish. D2 + D4 shipped 2026-05-20 (umbrella-only — script + tests + workflow + runbook). D1 + D3 pending — both require cross-repo/out-of-band work (D1: branch protection on 14 repos via gh api; D3: per-backend Cloud Run deploy workflows that don't live in the umbrella checkout).

Frequent commits per task. All commits must pass dotnet build / npm run build for the touched service before push.

Status checkpoint (2026-05-20)

  • A1 shipped + merged. PR xtrakt-platform-be#49. Endpoint POST /internal/compliance-audit/events; originally gated by PlatformAdmin, refined by A0.3 below.
  • A2 shipped + merged. PR xtrakt.base#8. HttpComplianceAuditEmitter + ComplianceAuditEmitterOptions + IComplianceAuditTokenProvider + DI extension. 5 tests.
  • A0 shipped + merged across 4 repos (4 PRs):
    • A0.1 / A0.2 / A0.4xtrakt-sso-be#17. AllowClientCredentialsFlow enabled; compliance.audit.emit scope registered; 9 confidential audit-emitter-* OAuth clients seeded; deterministic dev secrets in appsettings.Local.json. 17 new tests (3 server-config + 14 seeder).
    • A0.3xtrakt-platform-be#50. ComplianceAuditEmit policy accepts cogneris-platform-admin role OR compliance.audit.emit scope claim. 6 new policy-evaluation tests; 109 client-tests passing.
    • A0.5 — appended to xtrakt.base#8. ClientCredentialsComplianceAuditTokenProvider (SemaphoreSlim-gated 60 s refresh, TimeProvider injection for testing). AddClientCredentialsComplianceAuditEmitter composition DI helper. Version bumped to 10.0.18. 3 new tests; 73 base-tests passing.
    • A0.6xtrakt-docs#42. Runbook landed in the separate xtrakt-docs repo at compliance/runbooks/compliance-audit-client-secrets.md (not the monorepo path the original task body anticipated). Covers the 9 client_ids, GCP Secret Manager key layout (both SSO and consumer sides), deterministic local-dev secrets, rotation procedure, and single-client + full-fleet emergency revocation.
  • xTrakt.Base 10.0.18 published to GitHub Packages via Cloud Build (gcloud builds submit --config cloudbuild.yaml against xtrakt.base on main, build 2b961b9a-e567-47f9-8e77-b299da520121). Available at nuget.pkg.github.com/xtrakt-ai.
  • A3 shipped + merged for all 8 backend services. PRs (all merged): vision#30, sso#18, ecm#10, sign#11, tickets#9, template#8, billing#55, portal#6. Every service now references xTrakt.Base 10.0.18, registers services.AddClientCredentialsComplianceAuditEmitter(configuration), and has Compliance:AuditTrail config sections in appsettings.json / .Local.json / .Development.json (where tracked). Vision-be required disambiguation of its existing local IComplianceAuditEmitter; billing-be needed a direct xTrakt.Base reference added to Application (transitive 10.0.6 was insufficient).
  • B shipped + merged. B1 xtrakt-docs#43 (Track 4 10.0.14 publish closure + plan-extension landing), B2 xtrakt-platform-fe#80 (4 regression tests asserting no telemetry before consent), B3 xtrakt-docs#44 (telemetry-consent audit doc + Track 4 line 232/238 flipped to [x]).
  • 🟡 C1 shipped + merged. PR xtrakt-sso-be#19. PrivilegedAccessAuditService.EmitAsync fans out to the Platform compliance trail with safe metadata (chain hash as PayloadHash, never the payload itself); Platform outages are swallowed so SSO operations never fail because of the trail. 23 tests passing (+2 new).
  • C2–C7 pending — pattern is established. Each remaining C task is a focused per-service emit-call-sites + tests PR. C7 is doc-only (close Track 3 parent + validations).
  • 🟡 D2 shipped 2026-05-20 (umbrella only, no PR — this monorepo umbrella is not a git repo, so changes land directly on the shared filesystem; each sub-repo that re-uses these scripts/workflows will pick them up via its own sync). scripts/compliance/run_changed_service_checks.py gained run_for_service(repo_path, service_name) that dispatches to dotnet test for .NET, npm run test … --reporter=junit for Angular (src/package.json) and Vite (package.json at root), and is a no-op when no recognised test entry exists. TRX / JUnit XML parsed into {tests_run, passed, failed, skipped, duration_ms}. New CLI --mode tests --evidence-out … --commit … writes per-service evidence under docs/compliance/evidence/changed-service-checks/<service>/<commit>.json plus an aggregate summary.json. 7 pytest cases (scripts/compliance/test_run_changed_service_checks.py) all green; the legacy --mode build path unchanged. .github/workflows/security-ci.yml extended with Changed-service test evidence (after the existing build step, sets CHANGED_SERVICE_TESTS_FAILED) and Upload changed-service test evidence (90-day retention artifact, if-no-files-found: ignore). docs/compliance/evidence/changed-service-checks/README.md documents layout + auditor lookup. Track 2 implementation log line 140 flipped [x].
  • 🟡 D4 shipped 2026-05-20 (umbrella only — same caveat as D2). Policy documented in docs/compliance/runbooks/secure-release-review.md § 5.1 with the LOW/MEDIUM/HIGH/EMERGENCY threshold table, the 9 HIGH-risk path globs, and the 2026-05-20 → 2026-05-26 soft-warn rollout window. Cross-reference added to docs/compliance/change/change-risk-classification-standard.md. scripts/compliance/collect_release_metadata.py gained APPROVAL_THRESHOLDS, HIGH_RISK_PATH_GLOBS, meets_approval_threshold(risk_class, approvals), compute_risk_class(changed_files) (HIGH if any glob matches, MEDIUM otherwise), and enforce_approval_threshold(args). CLI gained --enforce-approval-threshold, --risk-class {LOW,MEDIUM,HIGH,EMERGENCY}, and --soft-warn; the prior required-arg contract is preserved when those flags are absent. 18 pytest cases (scripts/compliance/test_collect_release_metadata.py) all green. .github/workflows/security-ci.yml got a new Enforce approval threshold (warn-only until 2026-05-26) step gated on github.event_name == 'pull_request', running with both continue-on-error: true and --soft-warn. Promote to blocking on 2026-05-26 by removing both flags. Track 2 implementation log line 142 flipped [x].
  • D1 + D3 still pending — both inherently cross-repo and out-of-band:
    • D1 Step 4 writes branch protection on 14 repos via gh api (admin scope required, affects the whole team's PR flow). D1 Step 5 opens a no-op PR on each of the 14 in-scope repos. Neither is doable from this umbrella; they need a deliberate decision + admin tokens. Side note discovered while wiring D4: the canonical job names in D1 Step 3 of the plan (secret-scan, npm-audit, dotnet-audit, sbom, trivy-fs, trivy-config, changed-service-build) don't match the umbrella security-ci.yml, which runs all of those as steps inside a single security job. Before D1 Step 4 fires, either the workflow must be refactored to expose those as top-level jobs, or branch protection must reference the single Security scans and compliance evidence check.
    • D3 modifies .github/workflows/cloud-run-deploy-*.yml per backend repo. None of the 14 sub-repos in this checkout has a cloud-run-deploy*.yml — those workflows live elsewhere (likely in a separate deploy-orchestration repo or were never checked in to the inspected branches). Locating them is a prerequisite to executing D3.

Section A — Shared compliance emitter infrastructure (Track 3 prerequisite)

Task A1: Platform internal compliance audit ingestion endpoint

Files:

  • Create: xtrakt-platform-be/src/Web/Controllers/InternalComplianceAuditController.cs

  • Create: xtrakt-platform-be/src/xTrakt.Platform.Application/Handlers/InternalComplianceAuditEmit/InternalComplianceAuditEmitHandler.cs

  • Create: xtrakt-platform-be/src/xTrakt.Platform.Application/Handlers/InternalComplianceAuditEmit/InternalComplianceAuditEmitRequest.cs

  • Create: xtrakt-platform-be/src/xTrakt.Platform.Application/Handlers/InternalComplianceAuditEmit/InternalComplianceAuditEmitResponse.cs

  • Test: xtrakt-platform-be/tests/xTrakt.Platform.Client.Tests/InternalComplianceAuditControllerTests.cs

  • Step 1: Write the failing test

Note: The platform's existing service signature (verified at planning time in xtrakt-platform-be/src/xTrakt.Platform.Application/Services/Interfaces/IComplianceAuditTrailService.cs) is Task<ComplianceAuditEvent> EmitAsync(ComplianceAuditEventInput input). The handler below maps to the entity returned and projects safe fields into the response DTO.

// tests/xTrakt.Platform.Client.Tests/InternalComplianceAuditControllerTests.cs
public class InternalComplianceAuditControllerTests
{
[Fact]
public async Task EmitAsync_PersistsViaTrailService_AndReturnsChainMetadata()
{
var trail = Substitute.For<IComplianceAuditTrailService>();
trail.EmitAsync(Arg.Any<ComplianceAuditEventInput>())
.Returns(new ComplianceAuditEvent
{
Id = Guid.NewGuid(),
TenantId = "tenant-a",
EventType = "mfa.enabled",
ResourceType = "user",
ResourceId = "user-1",
ChainDate = new DateOnly(2026, 5, 19),
SequenceNumber = 1,
PreviousHash = null,
EventHash = "abc",
OccurredAt = DateTime.UtcNow
});

var handler = new InternalComplianceAuditEmitHandler(trail);
var response = await handler.ProcessAsync(new InternalComplianceAuditEmitRequest
{
TenantId = "tenant-a",
ActorUserId = "user-1",
ActorType = "user",
EventType = "mfa.enabled",
ResourceType = "user",
ResourceId = "user-1",
SourceService = "xtrakt-sso-be",
PayloadHash = "deadbeef"
});

Assert.False(response.HasErrors);
Assert.Equal("abc", response.Data.EventHash);
await trail.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventInput>(i =>
i.EventType == "mfa.enabled" &&
i.SourceService == "xtrakt-sso-be" &&
i.PayloadHash == "deadbeef"));
}

[Fact]
public async Task EmitAsync_RequiresSourceService()
{
var handler = new InternalComplianceAuditEmitHandler(Substitute.For<IComplianceAuditTrailService>());
var response = await handler.ProcessAsync(new InternalComplianceAuditEmitRequest
{
TenantId = "tenant-a",
EventType = "mfa.enabled",
ResourceType = "user"
});

Assert.True(response.HasErrors);
Assert.Contains(response.Errors, e => e.Code == "source_service_required");
}
}
  • Step 2: Run test to verify it fails

Run: cd xtrakt-platform-be && dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~InternalComplianceAuditControllerTests Expected: FAIL with "type or namespace 'InternalComplianceAuditEmitHandler' could not be found".

  • Step 3: Implement Request/Response DTOs
// src/xTrakt.Platform.Application/Handlers/InternalComplianceAuditEmit/InternalComplianceAuditEmitRequest.cs
namespace xTrakt.Platform.Application.Handlers.InternalComplianceAuditEmit;

public class InternalComplianceAuditEmitRequest
{
public string? TenantId { get; set; }
public string? ActorUserId { get; set; }
public string ActorType { get; set; } = "system";
public string EventType { get; set; } = string.Empty;
public string ResourceType { get; set; } = string.Empty;
public string? ResourceId { get; set; }
public string Purpose { get; set; } = "compliance";
public string SourceService { get; set; } = string.Empty;
public string? CorrelationId { get; set; }
public string? IpHash { get; set; }
public string? UserAgentHash { get; set; }
public string? PayloadHash { get; set; }
public DateTime? OccurredAt { get; set; }
}

// .../InternalComplianceAuditEmitResponse.cs
public class InternalComplianceAuditEmitResponse
{
public Guid EventId { get; set; }
public string TenantId { get; set; } = string.Empty;
public string EventType { get; set; } = string.Empty;
public string ResourceType { get; set; } = string.Empty;
public string? ResourceId { get; set; }
public DateOnly ChainDate { get; set; }
public int SequenceNumber { get; set; }
public string? PreviousHash { get; set; }
public string EventHash { get; set; } = string.Empty;
public DateTime OccurredAt { get; set; }
}
  • Step 4: Implement Handler
// .../InternalComplianceAuditEmitHandler.cs
using xTrakt.Application;
using xTrakt.Platform.Application.Services.Interfaces;

namespace xTrakt.Platform.Application.Handlers.InternalComplianceAuditEmit;

public class InternalComplianceAuditEmitHandler
: HandlerBase<InternalComplianceAuditEmitRequest, InternalComplianceAuditEmitResponse>
{
private readonly IComplianceAuditTrailService _trail;

public InternalComplianceAuditEmitHandler(IComplianceAuditTrailService trail) => _trail = trail;

protected override Task ValidateAsync()
{
if (string.IsNullOrWhiteSpace(Input.SourceService))
Response.AddError("source_service_required", "SourceService is required.");
if (string.IsNullOrWhiteSpace(Input.EventType))
Response.AddError("event_type_required", "EventType is required.");
if (string.IsNullOrWhiteSpace(Input.ResourceType))
Response.AddError("resource_type_required", "ResourceType is required.");
return Task.CompletedTask;
}

protected override async Task HandleAsync()
{
var entity = await _trail.EmitAsync(new ComplianceAuditEventInput
{
TenantId = Input.TenantId,
ActorUserId = Input.ActorUserId,
ActorType = Input.ActorType,
EventType = Input.EventType,
ResourceType = Input.ResourceType,
ResourceId = Input.ResourceId,
Purpose = Input.Purpose,
SourceService = Input.SourceService,
CorrelationId = Input.CorrelationId,
IpHash = Input.IpHash,
UserAgentHash = Input.UserAgentHash,
PayloadHash = Input.PayloadHash,
OccurredAt = Input.OccurredAt ?? DateTime.UtcNow
});

Response.Data = new InternalComplianceAuditEmitResponse
{
EventId = entity.Id,
TenantId = entity.TenantId ?? string.Empty,
EventType = entity.EventType,
ResourceType = entity.ResourceType,
ResourceId = entity.ResourceId,
ChainDate = entity.ChainDate,
SequenceNumber = (int)entity.SequenceNumber,
PreviousHash = entity.PreviousHash,
EventHash = entity.EventHash,
OccurredAt = entity.OccurredAt
};
}
}

Type note: ComplianceAuditEvent.SequenceNumber is long in the domain entity. The response DTO uses int to match ComplianceAuditEmitResult in xtrakt.base. The cast is safe within plausible audit-row counts; widen ComplianceAuditEmitResult.SequenceNumber to long in a future xtrakt.base release if cross-tenant totals ever approach Int32 limits.

  • Step 5: Implement Controller
// src/Web/Controllers/InternalComplianceAuditController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using xTrakt.Base.Web;
using xTrakt.Platform.Application.Handlers.InternalComplianceAuditEmit;

namespace xTrakt.Platform.Web.Controllers;

[ApiController]
[Authorize(AuthenticationSchemes = "InternalJwt")]
[Route("internal/compliance-audit")]
public class InternalComplianceAuditController : xTraktControllerBase<InternalComplianceAuditController>
{
[HttpPost("events")]
public async Task<IServiceResponse<InternalComplianceAuditEmitResponse>> EmitAsync(
[FromServices] InternalComplianceAuditEmitHandler handler,
[FromBody] InternalComplianceAuditEmitRequest request)
=> await handler.ProcessAsync(request);
}
  • Step 6: Run test to verify it passes

Run: cd xtrakt-platform-be && dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~InternalComplianceAuditControllerTests Expected: PASS (2/2).

  • Step 7: Verify build

Run: cd xtrakt-platform-be && dotnet build src/Web/Web.csproj Expected: build succeeds.

  • Step 8: Commit
cd xtrakt-platform-be
git add src/Web/Controllers/InternalComplianceAuditController.cs \
src/xTrakt.Platform.Application/Handlers/InternalComplianceAuditEmit/ \
tests/xTrakt.Platform.Client.Tests/InternalComplianceAuditControllerTests.cs
git commit -m "feat(platform): add internal compliance audit ingestion endpoint for cross-service emitters"

Task A2: HttpComplianceAuditEmitter in xtrakt.base

Files:

  • Create: xtrakt.base/src/Compliance/HttpComplianceAuditEmitter.cs

  • Create: xtrakt.base/src/Compliance/ComplianceAuditEmitterOptions.cs

  • Create: xtrakt.base/src/Compliance/ComplianceAuditEmitterServiceCollectionExtensions.cs

  • Modify: xtrakt.base/src/xTrakt.Base.csproj (bump version to 10.0.17)

  • Test: xtrakt.base/tests/unit/Compliance/HttpComplianceAuditEmitterTests.cs

  • Step 1: Write the failing test

// tests/unit/Compliance/HttpComplianceAuditEmitterTests.cs
public class HttpComplianceAuditEmitterTests
{
[Fact]
public async Task EmitAsync_PostsToConfiguredEndpoint_WithBearerToken_AndReturnsParsedResult()
{
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""
{
"data": {
"eventId": "11111111-1111-1111-1111-111111111111",
"tenantId": "tenant-a",
"eventType": "mfa.enabled",
"resourceType": "user",
"resourceId": "user-1",
"chainDate": "2026-05-19",
"sequenceNumber": 7,
"previousHash": "prev",
"eventHash": "next",
"occurredAt": "2026-05-19T12:00:00Z"
},
"hasErrors": false
}
""", Encoding.UTF8, "application/json")
});
var client = new HttpClient(handler) { BaseAddress = new Uri("https://platform.local") };
var tokenProvider = Substitute.For<IComplianceAuditTokenProvider>();
tokenProvider.GetTokenAsync(Arg.Any<CancellationToken>()).Returns("test-token");

var emitter = new HttpComplianceAuditEmitter(
client,
tokenProvider,
Options.Create(new ComplianceAuditEmitterOptions { Path = "/internal/compliance-audit/events" }),
NullLogger<HttpComplianceAuditEmitter>.Instance);

var result = await emitter.EmitAsync(new ComplianceAuditEventContract
{
TenantId = "tenant-a",
EventType = "mfa.enabled",
ResourceType = "user",
ResourceId = "user-1",
SourceService = "xtrakt-sso-be"
});

Assert.Equal(Guid.Parse("11111111-1111-1111-1111-111111111111"), result.EventId);
Assert.Equal("next", result.EventHash);
Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method);
Assert.Equal("/internal/compliance-audit/events", handler.LastRequest.RequestUri!.PathAndQuery);
Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme);
Assert.Equal("test-token", handler.LastRequest.Headers.Authorization.Parameter);
}

[Fact]
public async Task EmitAsync_OnNon2xx_ThrowsAndLogsWithoutPayloadBody()
{
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.InternalServerError));
var emitter = NewEmitter(handler);
await Assert.ThrowsAsync<ComplianceAuditEmitException>(() =>
emitter.EmitAsync(new ComplianceAuditEventContract
{
EventType = "x.y", ResourceType = "z", SourceService = "svc"
}));
}

[Fact]
public async Task EmitAsync_AcceptsBareDataEnvelope_AndUnwrappedShape()
{
// text/plain envelope (xTrakt convention) and bare object both succeed.
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("""{"eventId":"22222222-2222-2222-2222-222222222222","eventHash":"h","tenantId":"t","eventType":"e","resourceType":"r","chainDate":"2026-05-19","sequenceNumber":1,"occurredAt":"2026-05-19T00:00:00Z"}""")
});
var emitter = NewEmitter(handler);
var result = await emitter.EmitAsync(new ComplianceAuditEventContract
{
EventType = "e", ResourceType = "r", SourceService = "svc"
});
Assert.Equal("h", result.EventHash);
}

// Helper omitted for brevity in plan — engineer implements RecordingHandler and NewEmitter.
}
  • Step 2: Run test to verify it fails

Run: cd xtrakt.base && dotnet test tests/unit/xTrakt.Base.Tests.csproj --filter FullyQualifiedName~HttpComplianceAuditEmitterTests Expected: FAIL with "HttpComplianceAuditEmitter could not be found".

  • Step 3: Implement options + token provider
// src/Compliance/ComplianceAuditEmitterOptions.cs
namespace xTrakt.Base.Compliance;

public class ComplianceAuditEmitterOptions
{
public string Path { get; set; } = "/internal/compliance-audit/events";
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5);
public int MaxRetryAttempts { get; set; } = 1;
}

public interface IComplianceAuditTokenProvider
{
Task<string> GetTokenAsync(CancellationToken cancellationToken = default);
}
  • Step 4: Implement HttpComplianceAuditEmitter
// src/Compliance/HttpComplianceAuditEmitter.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace xTrakt.Base.Compliance;

public sealed class HttpComplianceAuditEmitter : IComplianceAuditEmitter
{
private readonly HttpClient _client;
private readonly IComplianceAuditTokenProvider _tokenProvider;
private readonly ComplianceAuditEmitterOptions _options;
private readonly ILogger<HttpComplianceAuditEmitter> _logger;

public HttpComplianceAuditEmitter(
HttpClient client,
IComplianceAuditTokenProvider tokenProvider,
IOptions<ComplianceAuditEmitterOptions> options,
ILogger<HttpComplianceAuditEmitter> logger)
{
_client = client;
_tokenProvider = tokenProvider;
_options = options.Value;
_logger = logger;
}

public async Task<ComplianceAuditEmitResult> EmitAsync(
ComplianceAuditEventContract auditEvent,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(auditEvent.EventType);
ArgumentException.ThrowIfNullOrWhiteSpace(auditEvent.ResourceType);
ArgumentException.ThrowIfNullOrWhiteSpace(auditEvent.SourceService);

using var request = new HttpRequestMessage(HttpMethod.Post, _options.Path)
{
Content = JsonContent.Create(auditEvent)
};
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer",
await _tokenProvider.GetTokenAsync(cancellationToken));

using var response = await _client.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Compliance audit emit failed status={Status} event={EventType} resource={ResourceType}/{ResourceId} source={SourceService}",
(int)response.StatusCode, auditEvent.EventType, auditEvent.ResourceType, auditEvent.ResourceId, auditEvent.SourceService);
throw new ComplianceAuditEmitException(
$"Compliance audit emit failed: {(int)response.StatusCode}");
}

await using var body = await response.Content.ReadAsStreamAsync(cancellationToken);
using var doc = await JsonDocument.ParseAsync(body, cancellationToken: cancellationToken);
var root = doc.RootElement.TryGetProperty("data", out var data) ? data : doc.RootElement;
return JsonSerializer.Deserialize<ComplianceAuditEmitResult>(root.GetRawText(), JsonOpts)!;
}

private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
}

public sealed class ComplianceAuditEmitException : Exception
{
public ComplianceAuditEmitException(string message) : base(message) { }
}
  • Step 5: Implement DI extension
// src/Compliance/ComplianceAuditEmitterServiceCollectionExtensions.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace xTrakt.Base.Compliance;

public static class ComplianceAuditEmitterServiceCollectionExtensions
{
public static IServiceCollection AddHttpComplianceAuditEmitter(
this IServiceCollection services,
IConfiguration configuration,
string sectionName = "Compliance:AuditTrail")
{
var section = configuration.GetSection(sectionName);
services.Configure<ComplianceAuditEmitterOptions>(section);
var baseUrl = section["BaseUrl"]
?? throw new InvalidOperationException($"{sectionName}:BaseUrl is required");

services.AddHttpClient<IComplianceAuditEmitter, HttpComplianceAuditEmitter>(c =>
{
c.BaseAddress = new Uri(baseUrl);
c.Timeout = TimeSpan.FromSeconds(5);
});
return services;
}
}
  • Step 6: Bump xtrakt.base/src/xTrakt.Base.csproj
<!-- xtrakt.base/src/xTrakt.Base.csproj -->
<PackageVersion>10.0.17</PackageVersion>
  • Step 7: Run all tests + verify build

Run: cd xtrakt.base && dotnet test tests/unit/xTrakt.Base.Tests.csproj Expected: PASS (including the 3 new HttpComplianceAuditEmitter cases).

  • Step 8: Commit
cd xtrakt.base
git add src/Compliance/HttpComplianceAuditEmitter.cs \
src/Compliance/ComplianceAuditEmitterOptions.cs \
src/Compliance/ComplianceAuditEmitterServiceCollectionExtensions.cs \
src/xTrakt.Base.csproj \
tests/unit/Compliance/HttpComplianceAuditEmitterTests.cs
git commit -m "feat(base): add HttpComplianceAuditEmitter for cross-service audit fan-in"
  • Step 9: Publish via existing Cloud Build / GitHub release pipeline

Run: git tag v10.0.17 && git push --tags (or trigger the existing publish workflow per xtrakt.base/.github/workflows/). Verify: Package shows up at https://github.com/orgs/xtrakt-ai/packages/nuget/package/xTrakt.Base version 10.0.17.


Task A3: Wire HttpComplianceAuditEmitter in each service (✅ shipped + merged for all 8 services 2026-05-20)

Shipped form (2026-05-20): all 8 service PRs merged. The original task body (preserved below) is correct in shape but the per-service details vary slightly. Summary by service:

ServicePRxTrakt.Base bumpNotes
xtrakt-vision-be#3010.0.6 / 10.0.11 → 10.0.18 (Application + Client + Domain)Existing local IComplianceAuditEmitter (observability-only) disambiguated via fully-qualified type registration; Section C2 will rename it.
xtrakt-sso-be#1810.0.6 → 10.0.18 (Domain)SSO authenticates against itself (audit-emitter-sso-be) for the client_credentials exchange.
xtrakt-ecm-be#1010.0.10 → 10.0.18 (Domain)No appsettings.Local.json exists in ecm-be — follows existing convention. PR was force-pushed once to drop unrelated pre-staged migration work.
xtrakt-sign-be#1110.0.11 → 10.0.18 (Domain + Infra)appsettings.Local.json is gitignored — devs populate per the A0.6 runbook.
xtrakt-tickets-be#910.0.10 → 10.0.18 (Domain)
xtrakt-template-be#810.0.4 → 10.0.18 (Domain)appsettings.Local.json is gitignored.
xtrakt-billing-be#5510.0.6 → 10.0.18 (Domain) plus new direct xTrakt.Base 10.0.18 reference added to xTrakt.Billing.Application.csprojTransitive xTrakt.Base 10.0.6 via xTrakt.Application was insufficient because the Compliance namespace only exists in 10.0.15+.
xtrakt-portal-be#610.0.11 → 10.0.18 (monolithic csproj)Single-csproj layout; wiring lives in Program.cs (after the existing AddPortal* registrations) instead of an Application/Startup file.

Per A0.5's helper, every service registers the emitter with a single line:

services.AddClientCredentialsComplianceAuditEmitter(configuration);

The original A3 task referenced an InternalJwtComplianceAuditTokenProvider.cs per-service stub; that file was never created — the A0 amendment locked in ClientCredentialsComplianceAuditTokenProvider from xtrakt.base instead.

Config sections added to every service's appsettings.json / .Local.json (where tracked) / .Development.json:

"Compliance": {
"AuditTrail": {
"BaseUrl": "<platform-be base url>",
"Path": "/internal/compliance-audit/events",
"Token": {
"SsoBaseUrl": "<sso base url>",
"ClientId": "audit-emitter-<svc>-be",
"ClientSecret": "<set-via-secret-manager:xtrakt-json-<svc>>"
}
}
}

Local dev uses deterministic dev secrets that match the SSO seeder (see A0.4).


The original task body follows for historical reference.

Per-service work; repeat for: xtrakt-sso-be, xtrakt-vision-be, xtrakt-ecm-be, xtrakt-sign-be, xtrakt-billing-be, xtrakt-tickets-be, xtrakt-template-be, xtrakt-portal-be.

For each <svc> below run all steps inside that service's repo.

Files (per service):

  • Modify: <svc>/src/<Application>/Startup.cs (register emitter)

  • Modify: <svc>/src/Web/appsettings.json (add Compliance:AuditTrail config keys with placeholders)

  • Modify: <svc>/src/Web/appsettings.Development.json (dev URL)

  • Modify: <svc>/src/Web/appsettings.Local.json (local URL)

  • Modify: <svc>/src/<Application>/xTrakt.<Svc>.Application.csproj (bump xTrakt.Base reference to 10.0.17)

  • Create: <svc>/src/<Application>/Services/InternalJwtComplianceAuditTokenProvider.cs

  • Step 1: Bump xTrakt.Base to 10.0.17 in the service's .csproj

<PackageReference Include="xTrakt.Base" Version="10.0.17" />
  • Step 2: Add the token provider
// <svc>/src/<Application>/Services/InternalJwtComplianceAuditTokenProvider.cs
using Microsoft.Extensions.Configuration;
using xTrakt.Base.Compliance;
using xTrakt.Base.Security;

namespace xTrakt.<Svc>.Application.Services;

public sealed class InternalJwtComplianceAuditTokenProvider : IComplianceAuditTokenProvider
{
private readonly IInternalJwtIssuer _issuer;
private readonly string _service;

public InternalJwtComplianceAuditTokenProvider(IInternalJwtIssuer issuer, IConfiguration cfg)
{
_issuer = issuer;
_service = cfg["ServiceName"] ?? "xtrakt-<svc>-be";
}

public Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
=> _issuer.IssueAsync(audience: "xtrakt-platform-be", subject: _service, scopes: new[] { "compliance.audit.emit" }, cancellationToken);
}

Note for engineer: if the service does not yet have IInternalJwtIssuer, register the existing user-bearer forwarding token provider already used for DSR inventory adapters (e.g. SSO's IDataSubjectServiceTokenProvider). Pick whichever scheme that service already uses for internal Platform calls — do not invent a new auth scheme.

  • Step 3: Register the emitter in Startup
// In <svc>/src/<Application>/Startup.cs inside AddApplicationServices(services, configuration):
services.AddScoped<IComplianceAuditTokenProvider, InternalJwtComplianceAuditTokenProvider>();
services.AddHttpComplianceAuditEmitter(configuration);
  • Step 4: Add config keys
// appsettings.json — production placeholders, real value from GCP Secret Manager
"Compliance": {
"AuditTrail": {
"BaseUrl": "https://platform-be.xtrakt.ai",
"Path": "/internal/compliance-audit/events"
}
}

// appsettings.Local.json
"Compliance": {
"AuditTrail": {
"BaseUrl": "http://localhost:8081",
"Path": "/internal/compliance-audit/events"
}
}

// appsettings.Development.json
"Compliance": {
"AuditTrail": {
"BaseUrl": "https://xtrakt-platform-be-dev-...run.app",
"Path": "/internal/compliance-audit/events"
}
}
  • Step 5: Verify build

Run: cd <svc> && dotnet build src/Web/Web.csproj Expected: PASS.

  • Step 6: Commit (per service)
cd <svc>
git add -A
git commit -m "feat(<svc>): wire shared HttpComplianceAuditEmitter to Platform audit trail"

2026-05-19 amendment to A3: The original A3 task above referenced an InternalJwtComplianceAuditTokenProvider that would issue tokens via a not-yet-existent IInternalJwtIssuer. After A1 shipped with a [Authorize(Policy = PlatformAdmin)] gate, the architectural decision (recorded in Section A0 below) is to refine A1 to also accept tokens carrying the compliance.audit.emit scope, and to obtain those tokens via OAuth client-credentials against SSO. Under A0, each service uses ClientCredentialsComplianceAuditTokenProvider (from xtrakt.base) — there is no per-service token-provider stub. The A3 step list above stands; only the file InternalJwtComplianceAuditTokenProvider.cs is replaced by a single line in Startup.cs that registers services.AddClientCredentialsComplianceAuditEmitter(configuration) (the new helper introduced in A0.5).


Section A0 — SSO client-credentials infrastructure, A1 policy refinement, caching token provider (Added 2026-05-19, shipped 2026-05-20)

Why this exists: A1 shipped with [Authorize(Policy = AuthorizationPolicies.PlatformAdmin)] because that was the only existing service-to-service auth pattern in the codebase. That role is over-privileged for an audit emitter (it grants access to every cross-tenant admin endpoint). The proper fix is a narrow compliance.audit.emit scope, obtained via OAuth client-credentials against SSO. SSO does not have client-credentials enabled today — this section adds it.

Order inside A0: A0.1 → A0.2 → A0.3 are sequential. A0.4 and A0.5 can run in parallel after A0.3. A0.6 is documentation that can finish last.

Shipped 2026-05-20. PRs merged: sso#17 (A0.1/A0.2/A0.4), platform-be#50 (A0.3, stacked on the A1 PR), xtrakt.base#8 (A0.5, appended to the A2 PR), docs#42 (A0.6). xTrakt.Base 10.0.18 published to GitHub Packages via Cloud Build 2b961b9a-e567-47f9-8e77-b299da520121. Deviations from the original task text are noted inline below.

Task A0.1: Enable client_credentials grant in SSO OpenIddict server (✅ shipped — sso#17)

Deviations:

  • The integration-test step (a real POST /connect/token round-trip) is deferredxtrakt-sso-be has no WebApplicationFactory harness today. Replaced with 3 unit-level assertions that drive Startup.ConfigureOpenIddictServer against IOptions<OpenIddictServerOptions> and confirm client_credentials (plus legacy grants) are registered and access-token lifetime is 1 h. Integration coverage is a tracked follow-up.
  • The inline lambda inside AddInfraestructureServices was extracted into the public static Startup.ConfigureOpenIddictServer(...) so the unit test can drive it without a Postgres-backed DbContext.

Files (in xtrakt-sso-be):

  • Modify: src/Web/Startup.cs (OpenIddict server registration)

  • Modify: src/Web/Configuration/OpenIddictConfiguration.cs (if a dedicated config file exists — confirm via rg -n "AddOpenIddict|AddServer" xtrakt-sso-be/src)

  • Test: xtrakt-sso-be/tests/...Integration.../ClientCredentialsFlowTests.cs

  • Step 1: Read the current OpenIddict server config

rg -n "AddOpenIddict\(\)|\.AddServer\(\)|\.Allow.*Flow\(\)|RegisterScopes\(" xtrakt-sso-be/src

Identify the existing AllowAuthorizationCodeFlow() / AllowRefreshTokenFlow() / etc. call chain.

  • Step 2: Write a failing integration test
// tests/.../ClientCredentialsFlowTests.cs
[Fact]
public async Task TokenEndpoint_ClientCredentials_GrantedScope_ReturnsAccessToken()
{
using var client = _factory.CreateClient();
var form = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", "audit-emitter-test"),
new KeyValuePair<string, string>("client_secret", "test-secret-32-chars-minimum-len!"),
new KeyValuePair<string, string>("scope", "compliance.audit.emit"),
});
var response = await client.PostAsync("/connect/token", form);
response.EnsureSuccessStatusCode();

var body = await response.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(body);
Assert.True(doc.RootElement.TryGetProperty("access_token", out var tok));
Assert.False(string.IsNullOrEmpty(tok.GetString()));
Assert.True(doc.RootElement.TryGetProperty("scope", out var scope));
Assert.Contains("compliance.audit.emit", scope.GetString());
}

The test fixture seeds a single audit-emitter-test confidential client with compliance.audit.emit scope. Use the integration-test bootstrapping that already exists in xtrakt-sso-be/tests.

  • Step 3: Run, expected FAILunsupported_grant_type or invalid_client.

  • Step 4: Enable client-credentials grant

In the OpenIddict server config:

services.AddOpenIddict()
.AddServer(options =>
{
// ... existing config ...
options.AllowClientCredentialsFlow();
options.SetTokenEndpointUris("/connect/token"); // confirm not already set
// ensure the access-token lifetime is reasonable for service-to-service:
options.SetAccessTokenLifetime(TimeSpan.FromHours(1));
});
  • Step 5: Run the test again, expected PASS.

  • Step 6: Commit on a new branch feat/sso-client-credentials-grant

git -C xtrakt-sso-be checkout -b feat/sso-client-credentials-grant
git -C xtrakt-sso-be add -A
git -C xtrakt-sso-be commit -m "feat(sso): enable client_credentials grant on OpenIddict server"

Task A0.2: Register the compliance.audit.emit scope (✅ shipped — sso#17)

Deviations:

  • Combined with A0.4 in a single commit (both touch OpenIddictSeeder). The scope and the 9 audit-emitter applications are seeded by the same boot path. New file src/xTrackt.Sso.Infra/Services/OpenIddictAuditScopes.cs carries the scope-name constant.
  • OpenIddictSeeder was refactored to take IOpenIddictScopeManager + IOpenIddictApplicationManager + ILogger, resolved from a DI scope in Program.cs (the DomainEntityInterceptor that originally motivated the standalone-DbContext pattern is a no-op at startup with no HttpContext, so DI-scoped resolution is safe).
  • The scope is also registered on the OpenIddict server options via RegisterScopes(...) so it shows up in /.well-known/openid-configuration discovery and the manager validates token requests asking for it.

Files (in xtrakt-sso-be):

  • Modify: src/Web/Seed/... (find the existing seed entry that registers OpenIddict scopes — likely OpenIddictSeed.cs or similar)

  • Migration: probably not needed (OpenIddict scopes are seeded at app startup, not via EF migration), confirm by reading the existing seed runner.

  • Test: extend ClientCredentialsFlowTests or add a focused scope-existence test.

  • Step 1: Identify the existing OpenIddict scope seed location

rg -n "IOpenIddictScopeManager\|CreateAsync.*OpenIddictScopeDescriptor" xtrakt-sso-be/src
  • Step 2: Write a failing test
[Fact]
public async Task OpenIddictSeed_RegistersComplianceAuditEmitScope()
{
using var scope = _factory.Services.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
var found = await manager.FindByNameAsync("compliance.audit.emit");
Assert.NotNull(found);
}
  • Step 3: Run, expected FAIL.

  • Step 4: Add the scope to the seed

// In OpenIddictSeed.cs (or wherever scopes are seeded):
if (await scopeManager.FindByNameAsync("compliance.audit.emit") is null)
{
await scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "compliance.audit.emit",
DisplayName = "Compliance Audit Event Emit",
Description = "Allows the bearer to write events into the Platform compliance audit trail."
});
}
  • Step 5: Run the test, expected PASS.

  • Step 6: Commit on the same branch

git -C xtrakt-sso-be commit -am "feat(sso): register compliance.audit.emit OpenIddict scope"

Task A0.3: Refine A1 policy to accept the scope claim (✅ shipped — platform-be#50)

Notes from shipped form:

  • AuthorizationPolicies.ComplianceAuditEmit constant added; policy registered in Web/Startup.cs accepting either role=cogneris-platform-admin OR scope / scp claim containing compliance.audit.emit (handles both single-token claims and space-separated lists per RFC 8693 §4.2).
  • InternalComplianceAuditController switched from [Authorize(Policy = PlatformAdmin)] to [Authorize(Policy = ComplianceAuditEmit)].
  • Structural test in InternalComplianceAuditControllerTests updated; 6 new policy-evaluation tests in ComplianceAuditEmitPolicyTests exercise authorized + unauthorized claim shapes. 109 client-tests passing (was 105, +4 net new).

Files (in xtrakt-platform-be):

  • Modify: src/Web/Authentication/AuthorizationPolicies.cs (add ComplianceAuditEmit constant)

  • Modify: src/Web/Startup.cs (register the new policy that accepts platform-admin OR scope claim)

  • Modify: src/Web/Controllers/InternalComplianceAuditController.cs (change [Authorize(Policy = AuthorizationPolicies.PlatformAdmin)] to [Authorize(Policy = AuthorizationPolicies.ComplianceAuditEmit)])

  • Modify: tests/xTrakt.Platform.Client.Tests/InternalComplianceAuditControllerTests.cs (update the structural role-attribute test to assert the new policy)

  • Step 1: Write the failing test (extend the existing structural test)

[Fact]
public void Controller_RequiresComplianceAuditEmitPolicy()
{
var attr = typeof(InternalComplianceAuditController)
.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: false)
.Cast<AuthorizeAttribute>()
.Single();
Assert.Equal(AuthorizationPolicies.ComplianceAuditEmit, attr.Policy);
}

Add a second test that builds a ClaimsPrincipal with only the compliance.audit.emit scope claim and verifies the policy authorizes them (uses IAuthorizationService.AuthorizeAsync).

  • Step 2: Run, expected FAIL (ComplianceAuditEmit constant does not exist).

  • Step 3: Add the constant

// AuthorizationPolicies.cs
public const string ComplianceAuditEmit = "ComplianceAuditEmit";
  • Step 4: Register the policy in Startup
// Startup.cs — inside AddAuthorization(options => { ... })
options.AddPolicy(AuthorizationPolicies.ComplianceAuditEmit, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(ctx =>
// platform admins keep access
ctx.User.HasClaim(c =>
(c.Type == "role" || c.Type == System.Security.Claims.ClaimTypes.Role) &&
c.Value == "cogneris-platform-admin")
// or any client with the narrow scope
|| ctx.User.HasClaim(c =>
(c.Type == "scope" || c.Type == "scp") &&
c.Value.Split(' ').Contains("compliance.audit.emit")));
});
  • Step 5: Change the controller attribute
// InternalComplianceAuditController.cs
[Authorize(Policy = AuthorizationPolicies.ComplianceAuditEmit)]
  • Step 6: Run, expected PASS (both structural test and the policy-evaluation test).

  • Step 7: Commit on a new branch feat/compliance-audit-emit-policy off feat/compliance-audit-internal-endpoint (so it stacks on the existing A1 PR)

git -C xtrakt-platform-be checkout feat/compliance-audit-internal-endpoint
git -C xtrakt-platform-be checkout -b feat/compliance-audit-emit-policy
git -C xtrakt-platform-be add -A
git -C xtrakt-platform-be commit -m "feat(platform): accept compliance.audit.emit scope on internal audit endpoint"

Task A0.4: Seed 9 confidential OAuth clients in SSO (✅ shipped — sso#17)

Notes from shipped form:

  • Task title corrected from 8 → 9 to match the actual client list (platform-be, sso-be, vision-be, ecm-be, sign-be, billing-be, tickets-be, template-be, portal-be).
  • Combined with A0.2 in a single commit (see A0.2 deviation note).
  • Secrets read from ComplianceAuditClients:<clientId>:Secret via IConfiguration. Missing secrets log a warning and skip that client rather than failing boot (so a partial dev environment doesn't block startup).
  • Deterministic dev secrets committed in xtrakt-sso-be/src/Web/appsettings.Local.json so other services' Local configs can match. Production secrets live in xtrakt-json-sso (see xtrakt-docs repo: compliance/runbooks/compliance-audit-client-secrets.md).
  • 13 unit tests (9 client-existence theory rows + 4 scope/idempotency cases) drive the seeder against an EF Core InMemory OpenIdDictDbContext.

Files (in xtrakt-sso-be):

  • Modify: src/Web/Seed/... (same file as A0.2 — the OpenIddict applications seed)
  • Modify: appsettings.Local.json (local dev: store a ComplianceAuditClients section with deterministic test secrets so dev boxes can exchange tokens without Secret Manager)
  • Test: extend ClientCredentialsFlowTests or add a focused application-seed test.

Client names (client_id):

  • audit-emitter-platform-be
  • audit-emitter-sso-be
  • audit-emitter-vision-be
  • audit-emitter-ecm-be
  • audit-emitter-sign-be
  • audit-emitter-billing-be
  • audit-emitter-tickets-be
  • audit-emitter-template-be
  • audit-emitter-portal-be

(That's 9, not 8 — platform-be is also a consumer for some operational events the plan lists in Section C.)

  • Step 1: Write a failing test that proves all 9 clients exist after seed runs
[Theory]
[InlineData("audit-emitter-platform-be")]
[InlineData("audit-emitter-sso-be")]
[InlineData("audit-emitter-vision-be")]
[InlineData("audit-emitter-ecm-be")]
[InlineData("audit-emitter-sign-be")]
[InlineData("audit-emitter-billing-be")]
[InlineData("audit-emitter-tickets-be")]
[InlineData("audit-emitter-template-be")]
[InlineData("audit-emitter-portal-be")]
public async Task OpenIddictSeed_RegistersAuditEmitterClient(string clientId)
{
using var scope = _factory.Services.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var app = await manager.FindByClientIdAsync(clientId);
Assert.NotNull(app);
var permissions = await manager.GetPermissionsAsync(app!);
Assert.Contains("gt:client_credentials", permissions); // grant type
Assert.Contains("scp:compliance.audit.emit", permissions); // scope
}
  • Step 2: Run, expected FAIL.

  • Step 3: Extend the seed

foreach (var (clientId, displayName) in new[]
{
("audit-emitter-platform-be", "Platform compliance audit emitter"),
("audit-emitter-sso-be", "SSO compliance audit emitter"),
("audit-emitter-vision-be", "Vision compliance audit emitter"),
("audit-emitter-ecm-be", "ECM compliance audit emitter"),
("audit-emitter-sign-be", "Sign compliance audit emitter"),
("audit-emitter-billing-be", "Billing compliance audit emitter"),
("audit-emitter-tickets-be", "Tickets compliance audit emitter"),
("audit-emitter-template-be", "Template compliance audit emitter"),
("audit-emitter-portal-be", "Portal compliance audit emitter"),
})
{
if (await appManager.FindByClientIdAsync(clientId) is null)
{
// Read the client secret from configuration. In prod this is sourced from
// Secret Manager (key: xtrakt-json-sso → `ComplianceAuditClients:<clientId>:Secret`).
// In Local dev the key lives in appsettings.Local.json. Never default-construct.
var secret = configuration[$"ComplianceAuditClients:{clientId}:Secret"]
?? throw new InvalidOperationException(
$"ComplianceAuditClients:{clientId}:Secret is not configured.");

await appManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = secret,
ClientType = OpenIddictConstants.ClientTypes.Confidential,
DisplayName = displayName,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "compliance.audit.emit"
}
});
}
}
  • Step 4: Add ComplianceAuditClients to appsettings.Local.json with deterministic 32-char dev secrets (NOT production secrets):
"ComplianceAuditClients": {
"audit-emitter-platform-be": { "Secret": "local-only-32char-platform-be-aaaaaaaa" },
"audit-emitter-sso-be": { "Secret": "local-only-32char-sso-be-aaaaaaaaaaa" },
// ... one row per client; deterministic so other services' Local config can match
}
  • Step 5: Run the test, expected PASS (9 theory rows).

  • Step 6: Commit on the SSO branch (still feat/sso-client-credentials-grant)

git -C xtrakt-sso-be commit -am "feat(sso): seed compliance-audit-emit confidential OAuth clients"

Task A0.5: ClientCredentialsComplianceAuditTokenProvider in xtrakt.base (✅ shipped — xtrakt.base#8)

Notes from shipped form:

  • Implementation matches the plan body. TimeProvider injection added via an internal constructor (gated by InternalsVisibleTo("xTrakt.Base.Tests")) so cache-hit / cache-miss tests can advance time without sleeping.
  • RecordingHandler from A2's tests was promoted to a shared file tests/unit/_TestHelpers/RecordingHandler.cs and gained request-body snapshotting (so callers can assert form contents after the request's using-scope disposes FormUrlEncodedContent).
  • Version bumped to 10.0.18; local xTrakt.Base.10.0.18.nupkg packed; published to GitHub Packages via Cloud Build (build 2b961b9a-e567-47f9-8e77-b299da520121).
  • 3 new tests; 73 base-tests passing (was 70, +3).

Files (in xtrakt.base):

  • Create: src/Compliance/ClientCredentialsComplianceAuditTokenProvider.cs
  • Create: src/Compliance/ClientCredentialsComplianceAuditOptions.cs
  • Modify: src/Compliance/ComplianceAuditEmitterServiceCollectionExtensions.cs — add AddClientCredentialsComplianceAuditEmitter(...) that wires both the token provider AND AddHttpComplianceAuditEmitter
  • Modify: src/xTrakt.Base.csproj (bump to 10.0.18)
  • Test: tests/unit/Compliance/ClientCredentialsComplianceAuditTokenProviderTests.cs

Behavior:

  • Calls SSO POST <SsoBaseUrl>/connect/token with grant_type=client_credentials, client_id, client_secret, scope=compliance.audit.emit.
  • Caches the access token in-process; refreshes when remaining lifetime drops below 60 seconds.
  • Thread-safe (use SemaphoreSlim around the refresh path; concurrent emitters during a refresh wait briefly and reuse the new token).
  • Token persistence is in-memory only — no Secret Manager writes, no disk.
// src/Compliance/ClientCredentialsComplianceAuditOptions.cs
namespace xTrakt.Base.Compliance;

public class ClientCredentialsComplianceAuditOptions
{
public string SsoBaseUrl { get; set; } = string.Empty; // e.g. https://sso.xtrakt.ai
public string ClientId { get; set; } = string.Empty; // e.g. audit-emitter-vision-be
public string ClientSecret { get; set; } = string.Empty; // from Secret Manager
public string Scope { get; set; } = "compliance.audit.emit";
public string TokenPath { get; set; } = "/connect/token";
public TimeSpan RefreshBeforeExpiry { get; set; } = TimeSpan.FromSeconds(60);
}
// src/Compliance/ClientCredentialsComplianceAuditTokenProvider.cs
public sealed class ClientCredentialsComplianceAuditTokenProvider : IComplianceAuditTokenProvider
{
private readonly HttpClient _http;
private readonly ClientCredentialsComplianceAuditOptions _opts;
private readonly ILogger<ClientCredentialsComplianceAuditTokenProvider> _logger;
private readonly SemaphoreSlim _gate = new(1, 1);
private CachedToken? _cached;

public ClientCredentialsComplianceAuditTokenProvider(
HttpClient http,
IOptions<ClientCredentialsComplianceAuditOptions> opts,
ILogger<ClientCredentialsComplianceAuditTokenProvider> logger)
{
_http = http;
_opts = opts.Value;
_logger = logger;
}

public async Task<string> GetTokenAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
if (_cached is { } t && t.ExpiresAt - _opts.RefreshBeforeExpiry > now)
return t.AccessToken;

await _gate.WaitAsync(cancellationToken);
try
{
if (_cached is { } t2 && t2.ExpiresAt - _opts.RefreshBeforeExpiry > DateTime.UtcNow)
return t2.AccessToken;

using var req = new HttpRequestMessage(HttpMethod.Post, _opts.TokenPath)
{
Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", _opts.ClientId),
new KeyValuePair<string, string>("client_secret", _opts.ClientSecret),
new KeyValuePair<string, string>("scope", _opts.Scope),
})
};

using var resp = await _http.SendAsync(req, cancellationToken);
if (!resp.IsSuccessStatusCode)
{
_logger.LogWarning(
"Compliance audit token exchange failed status={Status} client={ClientId}",
(int)resp.StatusCode, _opts.ClientId);
throw new ComplianceAuditEmitException(
$"Token exchange failed: {(int)resp.StatusCode}");
}
await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken);
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
var accessToken = doc.RootElement.GetProperty("access_token").GetString()!;
var expiresIn = doc.RootElement.TryGetProperty("expires_in", out var e)
? e.GetInt32() : 3600;

_cached = new CachedToken(accessToken, DateTime.UtcNow.AddSeconds(expiresIn));
return accessToken;
}
finally
{
_gate.Release();
}
}

private sealed record CachedToken(string AccessToken, DateTime ExpiresAt);
}

Add DI helper:

// ComplianceAuditEmitterServiceCollectionExtensions.cs — add second extension method
public static IServiceCollection AddClientCredentialsComplianceAuditEmitter(
this IServiceCollection services,
IConfiguration configuration,
string tokenSection = "Compliance:AuditTrail:Token",
string emitterSection = "Compliance:AuditTrail")
{
services.Configure<ClientCredentialsComplianceAuditOptions>(configuration.GetSection(tokenSection));
services.AddHttpClient<IComplianceAuditTokenProvider, ClientCredentialsComplianceAuditTokenProvider>(
(sp, c) =>
{
var opts = sp.GetRequiredService<IOptions<ClientCredentialsComplianceAuditOptions>>().Value;
if (string.IsNullOrWhiteSpace(opts.SsoBaseUrl))
throw new InvalidOperationException($"{tokenSection}:SsoBaseUrl is required");
c.BaseAddress = new Uri(opts.SsoBaseUrl);
c.Timeout = TimeSpan.FromSeconds(10);
});

services.AddHttpComplianceAuditEmitter(configuration, emitterSection);
return services;
}

Tests (3):

[Fact] async Task GetTokenAsync_OnFirstCall_PostsClientCredentials_AndCachesToken() { ... }
[Fact] async Task GetTokenAsync_WithinRefreshWindow_ReturnsCachedToken_WithoutHttp() { ... }
[Fact] async Task GetTokenAsync_AfterExpiry_RefreshesAndReplacesCache() { ... }

Use the same RecordingHandler from A2's test file (refactor it into a tests/unit/_TestHelpers/RecordingHandler.cs shared file).

  • Step 1: Write the 3 tests + shared RecordingHandler refactor.
  • Step 2: Run, expected FAIL.
  • Step 3: Implement Options + Provider + DI extension.
  • Step 4: Run, expected PASS.
  • Step 5: Bump xTrakt.Base.csproj <Version> to 10.0.18.
  • Step 6: Run full unit test project; expect 73 passing (70 + 3).
  • Step 7: Pack locally dotnet pack src/xTrakt.Base.csproj -c Release -o artifacts/package.
  • Step 8: Commit on the existing branch feat/http-compliance-audit-emitter so A2 and A0.5 ship together:
git -C xtrakt.base commit -am "feat(base): add ClientCredentialsComplianceAuditTokenProvider with token caching"

Task A0.6: Document Secret Manager keys + local-dev fallback (✅ shipped — docs#42)

Notes from shipped form:

  • Runbook landed in the separate xtrakt-docs repo (not the monorepo docs/ tree the original task body anticipated): compliance/runbooks/compliance-audit-client-secrets.md. Covers the 9 client_ids, SSO-side + consumer-side Secret Manager key layout, deterministic local-dev secrets, rotation procedure (production + local), single-client + full-fleet emergency revocation, and operational notes (1 h token TTL, 60 s in-process refresh, no automatic rotation yet).
  • Cross-linked from xtrakt-docs repo: compliance/runbooks/secure-release-review.md § 6.

Files:

  • Create: docs/compliance/runbooks/compliance-audit-client-secrets.md

  • Modify: existing docs/compliance/runbooks/secure-release-review.md (cross-link)

  • Step 1: Write the runbook

Content covers:

  • The 9 client_ids and their corresponding Secret Manager keys (e.g., xtrakt-json-ssoComplianceAuditClients:audit-emitter-vision-be:Secret).

  • Secret rotation procedure: who generates, where it's stored, how SSO and the consuming service are restarted.

  • Local-dev: appsettings.Local.json carries deterministic stable dev secrets so token exchange works on dev boxes without GCP access.

  • Emergency revocation: how to disable a single client without taking down the whole emit path.

  • Step 2: Commit on a new doc-only branch in the monorepo (no service branch)

git -C <monorepo> checkout -b docs/compliance-audit-client-secrets
git -C <monorepo> add docs/compliance/runbooks/compliance-audit-client-secrets.md
git -C <monorepo> commit -m "docs(compliance): runbook for compliance-audit-emit OAuth client secrets"

Section A0 — push policy (✅ executed 2026-05-20)

All 4 branches pushed and merged:

  • feat/sso-client-credentials-grant (xtrakt-sso-be) → #17 merged. Covered A0.1, A0.2, A0.4.
  • feat/compliance-audit-emit-policy (xtrakt-platform-be, stacked on the A1 PR which also merged) → #50 merged. Covered A0.3.
  • New commits on feat/http-compliance-audit-emitter (xtrakt.base) → #8 merged. Covered A0.5. Version bumped to 10.0.18.
  • docs/compliance-audit-client-secrets (xtrakt-docs) → #42 merged. Covered A0.6.

Publish gate cleared: xTrakt.Base 10.0.18 is on GitHub Packages. The Cloud Build pipeline does not auto-fire on push to main (no GitHub trigger exists for xtrakt.base); the publish was kicked off manually via gcloud builds submit --config cloudbuild.yaml --project sigma-composite-478314-s6 . from the xtrakt.base working tree (build 2b961b9a-e567-47f9-8e77-b299da520121, status SUCCESS, 27 s). The push step reported Version 10.0.18 of "xTrakt.Base" has already been pushed via --skip-duplicate, confirming the package was already on the feed. This is the same publish path used for 10.0.15 / 10.0.16 / 10.0.17.


Section B — Track 4 leftovers (✅ shipped + merged 2026-05-20)

All 3 sub-tasks shipped and merged. 2026-05-20: B2 cross-FE coverage closed — xtrakt-id-fe and xtrakt-portal-fe now ship their own regression specs (originally filed as "follow-ups" on the Track 4 line 246 caveat).

  • B1 docs#43 — Track 4 10.0.14 publish line marked superseded by 10.0.15+; plan extension landed.
  • B2 — three frontends:
    • xtrakt-platform-fe#80 — 4 regression cases under PrivacyPreferencesService spec § "B2 — no telemetry before analytics consent" (no consent record / explicit false / site-cookie / GPC override). 7/7 tests pass.
    • xtrakt-id-fe — commit f386216 on feat/section-b2-id-fe-telemetry-consent. src/src/app/services/newrelic-init.spec.ts evaluates the IIFE in assets/newrelic/init.js per scenario (5 cases: no record / product false / site-cookie false / GPC override / positive sanity). 61/61 specs pass overall. Required src/src/types/test-shims.d.ts to expose minimal node:fs types without pulling @types/node into the app build.
    • xtrakt-portal-fe — commit 4ae4ebc on feat/section-b2-portal-fe-telemetry-consent. Portal-fe had zero test infrastructure before this commit; added vitest.config.mjs + npm test script invoking vitest+jsdom via npx --yes --package vitest@^3 --package jsdom@^25 -- vitest run (devDependency promotion deferred until a GitHub Packages PAT rotation; vitest config is .mjs so it does not require vitest to be resolvable from node_modules). 12 cases (6 cases × 2 signals — analytics for installNewRelic's gate AND security for the bootstrap Fingerprint registration), including the GPC asymmetry (analytics blocked under GPC, security allowed as legitimate-interest). Spec tests hasPrivacySignalConsent directly rather than going through installNewRelic because node_modules/@newrelic/browser-agent was wiped by a macOS Finder duplicate-rename event and re-installation is gated by the same stale PAT.
  • B3 docs#44docs/compliance/audits/2026-05-19-telemetry-consent-audit.md catalogs every telemetry grep hit per FE; Track 4 line 232 ("Gate browser telemetry") and line 238 ("Network tests show ... do not load before consent") flipped to [x].

Task B1: Verify or supersede the xTrakt.Base 10.0.14 publish

Context: Track 4 has an open task to publish xTrakt.Base 10.0.14 to GitHub Packages. Track 8 already published 10.0.15 and 10.0.16, both of which contain the redaction helpers introduced in 10.0.14. Section A2 above adds 10.0.17. The unchecked Track 4 task is therefore stale and should be closed by verification.

Files:

  • Modify: docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md (close the Track 4 publish item with a verification note)

  • Step 1: Verify xTrakt.Base 10.0.15 (or later) is available on GitHub Packages and that it contains PiiMasker.MaskText/MaskJson/MaskFields/MaskException

Run:

curl -s -u "$GITHUB_PACKAGES_USER:$GITHUB_PACKAGES_TOKEN" \
https://nuget.pkg.github.com/xtrakt-ai/download/xtrakt.base/index.json | jq '.versions'

Expected: list includes 10.0.15 and 10.0.16 (and 10.0.17 once A2 publishes).

mkdir /tmp/xtb && cd /tmp/xtb && curl -sLo xt.nupkg \
-u "$GITHUB_PACKAGES_USER:$GITHUB_PACKAGES_TOKEN" \
https://nuget.pkg.github.com/xtrakt-ai/download/xtrakt.base/10.0.15/xtrakt.base.10.0.15.nupkg
unzip -p xt.nupkg lib/net10.0/xTrakt.Base.dll > xt.dll
dotnet tool install -g ilspycmd 2>/dev/null || true
ilspycmd xt.dll | grep -E "MaskText|MaskJson|MaskFields|MaskException"

Expected: all four method names present.

  • Step 2: Update the Track 4 implementation log

Edit the existing Track 4 line:

- [ ] Publish `xTrakt.Base` `10.0.14` to GitHub Packages so `xtrakt.webapi` CI/deploy restores without the local package source.

to:

- [x] Publish `xTrakt.Base` `10.0.14` to GitHub Packages so `xtrakt.webapi` CI/deploy restores without the local package source. *(Superseded — verified 2026-05-19 that `10.0.15` on GitHub Packages contains the same `PiiMasker` redaction surface as `10.0.14`; consumer services restore from the central feed without a local source.)*
  • Step 3: Commit
git add docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md
git commit -m "docs(compliance): close Track 4 xTrakt.Base 10.0.14 publish as superseded by 10.0.15"

Three frontends, three near-identical specs.

Files:

  • Create: xtrakt-portal-fe/tests/e2e/telemetry-consent.spec.ts (Playwright if portal already uses it; otherwise Vitest + jsdom monkey-patch — pick the existing harness)
  • Create: xtrakt-id-fe/src/cypress/e2e/telemetry-consent.cy.ts (or whichever harness the repo uses)
  • Create: xtrakt-platform-fe/src/cypress/e2e/telemetry-consent.cy.ts
  • Modify: per-FE CI workflow to run the new spec

Step pattern per FE (shown for xtrakt-portal-fe):

  • Step 1: Write the failing test
// xtrakt-portal-fe/tests/e2e/telemetry-consent.spec.ts
import { test, expect } from '@playwright/test';

const TELEMETRY_HOST_PATTERNS = [
/\bnewrelic\.com\b/i,
/\bnr-data\.net\b/i,
/\bjs-agent\.newrelic\.com\b/i,
/\bfpjs\.io\b/i,
/\bfingerprint\.com\b/i,
];

test('no telemetry loads before analytics consent', async ({ page, context }) => {
await context.clearCookies();
await page.addInitScript(() => localStorage.clear());

const violations: string[] = [];
page.on('request', (req) => {
const url = req.url();
if (TELEMETRY_HOST_PATTERNS.some((p) => p.test(url))) violations.push(url);
});

await page.goto('http://localhost:5173/');
// Cookie banner visible, no consent yet.
await expect(page.getByTestId('cookie-banner')).toBeVisible();
await page.waitForTimeout(2000);

expect(violations).toEqual([]);
});

test('telemetry loads after analytics consent', async ({ page }) => {
await page.goto('http://localhost:5173/');
await page.getByTestId('cookie-banner-accept-all').click();

await page.waitForRequest((req) =>
/js-agent\.newrelic\.com/.test(req.url()), { timeout: 5000 }
);
});
  • Step 2: Run to verify it fails before any product-code fix

Run: cd xtrakt-portal-fe && npx playwright test telemetry-consent.spec.ts Expected: passes immediately if the gating from Track 4 is correct, or FAIL listing the request URL(s) that violated the rule.

  • Step 3: If failing, fix the violating loader to gate on privacy-preferences.ts analytics consent

Per the existing portal pattern at xtrakt-portal-fe/src/plugins/newrelic.ts and xtrakt-portal-fe/src/privacy-preferences.ts — find the unguarded fetch/script tag and add the same gating helper used by other signals. Refer to the existing checked implementation log in Track 4 for exact APIs.

  • Step 4: Run again, verify it passes

  • Step 5: Add the spec to CI

Modify xtrakt-portal-fe/package.json (or repo workflow) so npm test / npm run test:e2e runs the new spec. If the FE has no E2E harness today, add a vitest + jsdom smoke that asserts document.querySelector('script[src*="newrelic"]') === null until the consent helper is called.

  • Step 6: Commit (per FE)
cd xtrakt-portal-fe
git add tests/e2e/telemetry-consent.spec.ts
git commit -m "test(portal): network test asserting no telemetry before analytics consent"
  • Repeat Steps 1–6 for xtrakt-id-fe and xtrakt-platform-fe — same spec body, only the dev-server URL and selector for the cookie banner change. Use:
    • xtrakt-id-fe dev URL: http://localhost:4100. Banner selector: see xtrakt-id-fe/src/src/app/shared/cookie-banner/....
    • xtrakt-platform-fe dev URL: http://localhost:4200. Banner selector: see xtrakt-platform-fe/src/src/app/shared/cookie-consent/....

Implementation deviations (2026-05-20): portal-fe ended up testing the hasPrivacySignalConsent predicate directly rather than going through installNewRelic because node_modules/@newrelic/browser-agent was wiped by a macOS Finder duplicate-rename event (the same PAT block prevents npm install-driven recovery in this session). The predicate covers both gates (analytics → NewRelic, security → Fingerprint) and the GPC-asymmetric handling. Id-fe ended up evaluating the IIFE inside assets/newrelic/init.js per scenario because index.html loads that script unconditionally, with no Angular-side service to gate. Both deliver the same regression contract as the platform-fe spec from the original PR — see the Section B summary above for commit refs and case counts.


Task B3: Final telemetry audit (any unguarded signal)

  • Step 1: Grep each FE for telemetry SDK names that are NOT routed through the privacy-preferences helper
for fe in xtrakt-portal-fe xtrakt-id-fe xtrakt-platform-fe xtrakt-site; do
echo "=== $fe ==="
rg -n "newrelic|fpjs|fingerprint|hotjar|fullstory|mixpanel|amplitude|intercom|datadog-rum|gtag\(" $fe \
| rg -v "privacy-preferences|cookie-banner|cookie-consent|spec\.|test\."
done

Expected: every match is either inside a privacy-preferences-gated helper, a cookie banner UI, or a test file. Anything else needs to be gated or removed.

  • Step 2: For each violation, route the loader through the FE's existing analytics-consent helper (see Track 4 implementation log for the exact helpers per FE). Add a regression entry to the Task B2 spec for that signal.

  • Step 3: Commit per FE

git add -A && git commit -m "fix(<fe>): gate <signal> behind privacy preferences"
  • Step 4: Update Track 4 parent task and validation lines to [x] once B1, B2, and B3 are complete.

Section C — Track 3 per-service operational event coverage

Status (2026-05-20): C1 shipped + merged (sso#19). C2–C7 pending — pattern is established (inject IComplianceAuditEmitter from xTrakt.Base.Compliance, emit ComplianceAuditEventContract after the existing local write, wrap in try/catch so Platform outages cannot fail the local operation, write 1–2 focused tests per event type).

Every emit task below assumes Section A is complete in the service being modified (i.e. IComplianceAuditEmitter from xtrakt.base is resolvable from DI). Each task introduces a constant set of event types, injects the emitter into the existing handler/service, emits an event with a hashed payload (no raw PII / no document content / no secrets), and adds focused tests.

Universal payload rules (apply to every emit task in this section):

  • Never include raw email, phone, document filenames, OCR text, prompt content, response content, API keys, signature image data, webhook secrets, or full URLs.
  • Hash any forensic metadata (IP, User-Agent) with the existing PiiMasker/SHA-256 helpers already used by Track 3's checked emit sites.
  • PayloadHash should be a SHA-256 hex of the canonical JSON of the safe metadata you serialize for PayloadJson. If you store no payload, leave PayloadHash = null.
  • SourceService must be "xtrakt-<svc>-be".
  • Purpose = "compliance" for routine emits; "privileged" for admin/MFA/auth/support events; "data-subject" only for DSR-related events.

Task C1 (SSO): MFA + authentication + session compliance bridge (✅ shipped — sso#19)

Notes from shipped form:

  • PrivilegedAccessAuditService now takes IComplianceAuditEmitter + ILogger. After _repository.AddAsync succeeds, it emits a ComplianceAuditEventContract with safe metadata: SourceService="xtrakt-sso-be", Purpose="privileged", PayloadHash = the local row's chain hash (no payload content). Wrapped in try/catch; Platform outage logs a warning at Warning level and the local SSO operation continues.
  • Every existing caller (MfaService, LoginAuditService, AuthService, SessionService — all the call sites listed in the original task body) automatically gains Platform fan-out because they all route through PrivilegedAccessAuditService. No caller-side changes needed in this PR.
  • Tests: 23 passing in tests/unit/PrivilegedAccessAudit/ (21 before + 2 new): safe-metadata-only assertion (no email/payload leak into the fan-out) + local-row-still-persists-when-emitter-throws.
  • Out of scope for this PR (deferred follow-up): filling in the missing emit-call-sites listed in the original task table (mfa.setup_started, mfa.setup_completed, mfa.setup_failed, mfa.backup_codes_regenerated). Track 9 already covered mfa.enabled / mfa.disabled / authentication.* / session.* locally, and those now fan out automatically.

Context: SSO already writes hash-chained PrivilegedAccessAuditEvent rows for MFA enable/disable and session revocation. Per design decision, we ALSO emit ComplianceAuditEventContract via the shared emitter so Platform's trail has cross-service coverage.

Files:

  • Modify: xtrakt-sso-be/src/xTrackt.Sso.Application/Services/PrivilegedAccessAuditService.cs
  • Modify: xtrakt-sso-be/src/xTrackt.Sso.Application/Services/MfaService.cs
  • Modify: xtrakt-sso-be/src/xTrackt.Sso.Application/Services/AuthService.cs
  • Modify: xtrakt-sso-be/src/xTrackt.Sso.Application/Services/SessionService.cs
  • Test: xtrakt-sso-be/tests/unit/.../PrivilegedAccessComplianceBridgeTests.cs
  • Test: xtrakt-sso-be/tests/unit/.../MfaServiceComplianceAuditTests.cs
  • Test: xtrakt-sso-be/tests/unit/.../AuthServiceComplianceAuditTests.cs
  • Test: xtrakt-sso-be/tests/unit/.../SessionServiceComplianceAuditTests.cs

Event types:

  • mfa.setup_started
  • mfa.setup_completed
  • mfa.setup_failed
  • mfa.enabled
  • mfa.disabled
  • mfa.backup_codes_regenerated
  • authentication.succeeded
  • authentication.failed
  • session.revoked
  • session.bulk_revoked

Approach: Inject IComplianceAuditEmitter (from xTrakt.Base.Compliance) into PrivilegedAccessAuditService. After the existing AppendAsync writes the hash-chained local row, fire-and-forget call the shared emitter with the same metadata. Wrap in try/catch so a Platform outage cannot fail SSO operations — log a warning and continue.

  • Step 1: Write the failing test (PrivilegedAccessComplianceBridgeTests)
[Fact]
public async Task AppendAsync_AlsoEmitsComplianceAuditEvent()
{
var localRepo = Substitute.For<IPrivilegedAccessAuditEventRepository>();
var emitter = Substitute.For<IComplianceAuditEmitter>();
var service = new PrivilegedAccessAuditService(localRepo, emitter, NullLogger<PrivilegedAccessAuditService>.Instance);

await service.AppendAsync(new PrivilegedAccessAuditAppendInput
{
TenantId = "tenant-a",
ActorUserId = "user-1",
EventType = "mfa.enabled",
ResourceType = "user",
ResourceId = "user-1"
});

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "mfa.enabled" &&
e.SourceService == "xtrakt-sso-be" &&
e.TenantId == "tenant-a" &&
string.IsNullOrEmpty(e.PayloadHash) == false),
Arg.Any<CancellationToken>());
}

[Fact]
public async Task AppendAsync_DoesNotFail_WhenEmitterThrows()
{
var localRepo = Substitute.For<IPrivilegedAccessAuditEventRepository>();
var emitter = Substitute.For<IComplianceAuditEmitter>();
emitter.EmitAsync(Arg.Any<ComplianceAuditEventContract>(), Arg.Any<CancellationToken>())
.Returns<Task<ComplianceAuditEmitResult>>(_ => throw new ComplianceAuditEmitException("platform down"));
var service = new PrivilegedAccessAuditService(localRepo, emitter, NullLogger<PrivilegedAccessAuditService>.Instance);

await service.AppendAsync(new PrivilegedAccessAuditAppendInput
{
TenantId = "tenant-a", EventType = "mfa.enabled", ResourceType = "user", ResourceId = "u"
});

await localRepo.Received(1).AppendAsync(Arg.Any<PrivilegedAccessAuditEvent>(), Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail

Run: cd xtrakt-sso-be && dotnet test tests/unit/... --filter FullyQualifiedName~PrivilegedAccessComplianceBridgeTests

  • Step 3: Modify PrivilegedAccessAuditService to inject IComplianceAuditEmitter and call it after the local write
public async Task AppendAsync(PrivilegedAccessAuditAppendInput input, CancellationToken ct = default)
{
var localEvent = await _local.AppendAsync(BuildLocalEvent(input), ct);

try
{
await _emitter.EmitAsync(new ComplianceAuditEventContract
{
TenantId = input.TenantId,
ActorUserId = input.ActorUserId,
ActorType = input.ActorType ?? "user",
EventType = input.EventType,
ResourceType = input.ResourceType,
ResourceId = input.ResourceId,
Purpose = "privileged",
SourceService = "xtrakt-sso-be",
CorrelationId = input.CorrelationId,
IpHash = input.IpHash,
UserAgentHash = input.UserAgentHash,
PayloadHash = localEvent.EventHash,
OccurredAt = localEvent.OccurredAt
}, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Compliance audit fan-out failed event={EventType} tenant={TenantId}",
input.EventType, input.TenantId);
}
}
  • Step 4: Run tests, expected pass

  • Step 5: Write missing emit-call-sites for the events not yet covered locally

For each event in the list above, find the relevant SSO code path and add a call to PrivilegedAccessAuditService.AppendAsync:

EventFile / method
mfa.setup_startedMfaService.StartSetupAsync
mfa.setup_completedMfaService.CompleteSetupAsync
mfa.setup_failedMfaService.CompleteSetupAsync (failure branch)
mfa.enabledalready wired per Track 9
mfa.disabledalready wired per Track 9
mfa.backup_codes_regeneratedMfaService.RegenerateBackupCodesAsync
authentication.succeededAuthService.SignInAsync (success)
authentication.failedAuthService.SignInAsync (failure)
session.revokedSessionService.RevokeAsync
session.bulk_revokedSessionService.RevokeAllAsync

For each new call site, add a focused test that proves IComplianceAuditEmitter.EmitAsync is invoked with the correct EventType and Purpose="privileged".

  • Step 6: Run full SSO test suite + build
cd xtrakt-sso-be
dotnet test xtrakt-sso-be.sln
dotnet build src/Web/Web.csproj
  • Step 7: Commit
git add -A
git commit -m "feat(sso): bridge privileged + MFA + auth + session events to Platform compliance trail"

Task C2 (Vision): extraction and agent-run events

Files:

  • Modify: xtrakt-vision-be/src/xTrakt.Vision.Application/Services/PlaygroundService.cs
  • Modify: xtrakt-vision-be/src/xTrakt.Vision.Application/Services/ProductionService.cs
  • Modify: xtrakt-vision-be/src/xTrakt.Vision.Application/Agents/Runner/AgentRunner.cs (both RunSyncAsync and the async variant if present)
  • Test: xtrakt-vision-be/tests/xTrakt.Vision.IntegrationTests/ComplianceAudit/ExtractionAuditTests.cs
  • Test: xtrakt-vision-be/tests/xTrakt.Vision.IntegrationTests/ComplianceAudit/AgentRunAuditTests.cs

Event types (note: emit at start/end/fail granularity, NOT per-token):

  • extraction.started
  • extraction.completed
  • extraction.failed
  • agent_run.started
  • agent_run.completed
  • agent_run.failed

Payload examples (safe, no document content):

// extraction.completed PayloadJson
{
"engine": "OpenAI",
"model": "gpt-5.4",
"promptHash": "sha256:abc...", // hash of the rendered prompt, never the prompt text
"responseHash": "sha256:def...",
"confidence": 0.92,
"fieldsCount": 14,
"humanReviewRequired": false,
"requestId": "req-..."
}
  • Step 1: Write the failing test for extraction.completed
[Fact]
public async Task PlaygroundService_OnExtractionComplete_EmitsExtractionCompletedEvent()
{
var emitter = Substitute.For<IComplianceAuditEmitter>();
var svc = BuildPlaygroundService(emitter);

await svc.ExtractAsync(SeedRequest(), CancellationToken.None);

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "extraction.completed" &&
e.ResourceType == "extraction" &&
e.SourceService == "xtrakt-vision-be" &&
e.PayloadHash != null),
Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail

  • Step 3: Replace IComplianceAuditEmitter (local vision interface) with the shared xTrakt.Base.Compliance.IComplianceAuditEmitter

In each Vision file that currently imports xTrakt.Vision.Application.Services.Interfaces.IComplianceAuditEmitter, change the import to xTrakt.Base.Compliance.IComplianceAuditEmitter. Replace the call site shape (the existing ComplianceAuditRecord becomes ComplianceAuditEventContract). The local logger-only emitter class can stay but is renamed to IObservabilityAuditEmitter to make the boundary obvious.

Suggested rename:

  • xtrakt-vision-be/src/xTrakt.Vision.Application/Services/Interfaces/IComplianceAuditEmitter.csIObservabilityAuditEmitter.cs (keep ComplianceAuditRecord for local logging).
  • xtrakt-vision-be/src/xTrakt.Vision.Application/Services/ComplianceAuditEmitter.csObservabilityAuditEmitter.cs.

After rename, all existing emit call sites in vision (classifier, webhook endpoint, providers, ai models — listed in Track 3 implementation log) must be updated to ALSO emit through the shared emitter. The cleanest approach is a small decorator:

public sealed class VisionComplianceAuditEmitterFanOut : IComplianceAuditEmitter // xTrakt.Base.Compliance
{
private readonly IComplianceAuditEmitter _platform; // HttpComplianceAuditEmitter
private readonly IObservabilityAuditEmitter _local;

public VisionComplianceAuditEmitterFanOut(
[FromKeyedServices("platform")] IComplianceAuditEmitter platform,
IObservabilityAuditEmitter local)
{ _platform = platform; _local = local; }

public async Task<ComplianceAuditEmitResult> EmitAsync(ComplianceAuditEventContract e, CancellationToken ct = default)
{
await _local.EmitAsync(ToLocal(e), ct);
try { return await _platform.EmitAsync(e, ct); }
catch { return DefaultResult(e); }
}
}

Register the keyed HttpComplianceAuditEmitter as "platform" and register VisionComplianceAuditEmitterFanOut as the public IComplianceAuditEmitter.

  • Step 4: Wire emit calls into PlaygroundService.ExtractAsync, ProductionService.ExtractAsync, and AgentRunner.RunSyncAsync (and the async variant if present)

Each call site:

  1. Capture started = DateTime.UtcNow and a requestId.
  2. Call await _emitter.EmitAsync({ EventType: "extraction.started", ... }) BEFORE invoking the engine.
  3. On success, emit extraction.completed with the payload shape above.
  4. On exception, emit extraction.failed with { "errorClass": ex.GetType().Name } only.

Use the same pattern for agent_run.* in AgentRunner.

  • Step 5: Add focused tests for each event type

Six tests total (3 extraction + 3 agent_run). Each asserts: emit invoked exactly once, EventType matches, PayloadHash not null on *.completed, raw prompt/response text never appears in PayloadJson.

  • Step 6: Run tests + build
cd xtrakt-vision-be
dotnet test tests/xTrakt.Vision.IntegrationTests/xTrakt.Vision.IntegrationTests.csproj --filter FullyQualifiedName~ComplianceAudit
dotnet build src/Web/Web.csproj
  • Step 7: Commit
git add -A
git commit -m "feat(vision): emit extraction + agent_run compliance events with hashed prompt/response metadata"

Task C3 (ECM): document view / download / share events

Files (ECM uses handler-per-action pattern, no DocumentService.cs):

  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Document/Create/CreateDocumentHandler.cs (emit document.uploaded)
  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Document/UploadVersion/UploadVersionHandler.cs (emit document.uploaded for new versions)
  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Document/Get/GetDocumentHandler.cs (emit document.viewed)
  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Document/GetDownloadUrl/GetDownloadUrlHandler.cs (emit document.downloaded when the signed URL is minted)
  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Share/Create/CreateShareLinkHandler.cs (emit document.share_link.created)
  • Modify: xtrakt-ecm-be/src/xTrackt.Ecm.Application/Handlers/Share/Resolve/ResolveShareLinkHandler.cs (emit document.share_link.accessed)
  • Create or extend: an ECM share-link-revoke path — current repo has no Revoke handler under Handlers/Share/. Either add Handlers/Share/Revoke/RevokeShareLinkHandler.cs per the existing handler pattern OR locate the implicit revoke (e.g. an Update handler that toggles IsActive=false) and emit there. Engineer: confirm via rg -n "IsActive\s*=\s*false" xtrakt-ecm-be/src before deciding.
  • Test: xtrakt-ecm-be/tests/unit/ComplianceAudit/DocumentAuditTests.cs
  • Test: xtrakt-ecm-be/tests/unit/ComplianceAudit/ShareLinkAuditTests.cs

Event types:

  • document.uploaded
  • document.viewed
  • document.downloaded
  • document.share_link.created
  • document.share_link.accessed
  • document.share_link.revoked

Payload guidance: include documentId, versionId, tenantId, actorUserId (or "anonymous-share-link" for public access), purpose: "compliance". Never include file names, paths, GCS object URIs, extracted text, or share-link secrets.

  • Step 1: Write the failing test for document.downloaded
[Fact]
public async Task DownloadDocument_EmitsDocumentDownloadedEvent()
{
var emitter = Substitute.For<IComplianceAuditEmitter>();
var ctrl = BuildDocumentsController(emitter);

await ctrl.DownloadAsync(documentId: Guid.NewGuid(), versionId: Guid.NewGuid(), ct: default);

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "document.downloaded" &&
e.ResourceType == "document" &&
e.SourceService == "xtrakt-ecm-be"),
Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail.

  • Step 3: Inject IComplianceAuditEmitter into the relevant service/controller and emit per event type (one per event family).

Each call site: emit AFTER the action returns successfully. For view events, emit only if the byte stream / signed URL is actually delivered, not for 4xx/5xx.

  • Step 4: Add tests for the remaining 5 event types (one per event family — use the same pattern shown in Step 1).

  • Step 5: Run + build

cd xtrakt-ecm-be
dotnet test tests/unit/xTrackt.Ecm.Tests.csproj --filter FullyQualifiedName~ComplianceAudit
dotnet build src/Web/Web.csproj
  • Step 6: Commit
git add -A
git commit -m "feat(ecm): emit document view/download/share compliance events"

Task C4 (Sign): envelope lifecycle events

Files (Sign uses handler-per-action pattern, no EnvelopeService.cs):

  • Modify: xtrakt-sign-be/src/xTrackt.Sign.Application/Handlers/Envelope/Create/CreateEnvelopeHandler.cs (emit envelope.created)
  • Modify: xtrakt-sign-be/src/xTrackt.Sign.Application/Handlers/Envelope/Send/SendEnvelopeHandler.cs (emit envelope.sent)
  • Modify: xtrakt-sign-be/src/xTrackt.Sign.Application/Handlers/Sign/GetSession/GetSigningSessionHandler.cs (emit envelope.viewed_by_signer after OTP-verified session resolves)
  • Modify: xtrakt-sign-be/src/xTrackt.Sign.Application/Handlers/Sign/Commit/CommitSignatureHandler.cs (emit envelope.signed per signer; if this is the last signer, also emit envelope.completed)
  • Locate/create the decline + void handlers under Handlers/Envelope/ — current repo has neither. Engineer: confirm via rg -n "Decline\|Void\|Cancel" xtrakt-sign-be/src and either add Handlers/Envelope/Decline/DeclineEnvelopeHandler.cs and Handlers/Envelope/Void/VoidEnvelopeHandler.cs following the existing handler pattern OR identify the existing status-update path. Each should emit envelope.declined / envelope.voided.
  • Test: xtrakt-sign-be/tests/Web.Tests/ComplianceAudit/EnvelopeAuditTests.cs

Event types:

  • envelope.created
  • envelope.sent
  • envelope.viewed_by_signer
  • envelope.signed (per signer)
  • envelope.completed (all signers done)
  • envelope.declined
  • envelope.voided

Payload guidance: include envelopeId, signerIdHash (SHA-256 of signer email — never raw email), tenantId. Never include raw signer email, signer name, file content, OTP, public-link secret.

  • Step 1: Write the failing test for envelope.signed
[Fact]
public async Task SignerCompletes_EmitsEnvelopeSignedEvent_WithSignerIdHash_NotEmail()
{
var emitter = Substitute.For<IComplianceAuditEmitter>();
var svc = BuildEnvelopeService(emitter);

await svc.RecordSignerCompletionAsync(
envelopeId: SeedEnvelope.Id,
signerEmail: "signer@example.com",
ct: default);

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "envelope.signed" &&
e.ResourceType == "envelope" &&
e.SourceService == "xtrakt-sign-be" &&
e.PayloadHash != null &&
!ContainsRawEmail(e)),
Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail.

  • Step 3: Implement emit calls at each lifecycle transition with the signer-email-as-hash payload pattern.

  • Step 4: Add 6 more tests, one per remaining event type.

  • Step 5: Run + build

cd xtrakt-sign-be
dotnet test tests/Web.Tests/Web.Tests.csproj --filter FullyQualifiedName~ComplianceAudit
dotnet build src/Web/Web.csproj
  • Step 6: Commit
git add -A
git commit -m "feat(sign): emit envelope lifecycle compliance events with hashed signer identifiers"

Task C5 (Billing): permission and subscription change events

Files:

  • Modify: xtrakt-billing-be/src/Web/Controllers/CompanyMemberController.cs
  • Modify: xtrakt-billing-be/src/Web/Controllers/CustomerController.cs
  • Modify: xtrakt-billing-be/src/Web/Controllers/PaymentMethodController.cs
  • Modify: xtrakt-billing-be/src/Web/Controllers/SubscriptionController.cs
  • Modify: xtrakt-billing-be/src/Web/Controllers/InvitationController.cs
  • Test: xtrakt-billing-be/tests/unit/ComplianceAudit/BillingAuditTests.cs

Event types:

  • billing.member.invited
  • billing.member.added
  • billing.member.role_changed
  • billing.member.removed
  • billing.payment_method.added
  • billing.payment_method.removed
  • billing.payment_method.set_default
  • billing.subscription.created
  • billing.subscription.plan_changed
  • billing.subscription.cancelled

Payload guidance: include tenantId, actorUserId, subjectUserIdHash (hashed if cross-user), roleFrom/roleTo for role changes, planFrom/planTo for plan changes, paymentMethodLast4 and paymentMethodBrand for payment methods — never the full PAN or Stripe customer PII.

  • Step 1: Write the failing test for billing.member.role_changed
[Fact]
public async Task UpdateMemberRole_EmitsRoleChangedEvent_WithRoleFromTo()
{
var emitter = Substitute.For<IComplianceAuditEmitter>();
var svc = BuildCompanyMemberService(emitter);

await svc.UpdateRoleAsync(memberId: SeedMember.Id, newRole: "admin", ct: default);

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "billing.member.role_changed" &&
e.ResourceType == "billing_member" &&
e.SourceService == "xtrakt-billing-be"),
Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail.

  • Step 3: Implement emit calls at each handler that mutates membership / payment / subscription. Payload includes the from/to values.

  • Step 4: Add tests for each remaining event type (9 more).

  • Step 5: Run + build

cd xtrakt-billing-be
dotnet test tests/unit/xTrakt.Billing.Tests.csproj --filter FullyQualifiedName~ComplianceAudit
dotnet build src/Web/Web.csproj
  • Step 6: Commit
git add -A
git commit -m "feat(billing): emit member + payment + subscription permission compliance events"

Task C6 (Tickets + Platform): support access events

Context: "Support access" = any time a support engineer or cogneris-platform-admin operates on tenant data. There are two surfaces:

  1. Tickets — agent actions on customer tickets (assign, internal note, escalate, view attachment).
  2. Platform — support-staff impersonation / read-of-tenant-data via admin tools.

Files:

  • Modify: xtrakt-tickets-be/src/Web/Controllers/TicketsController.cs
  • Modify: xtrakt-tickets-be/src/Web/Controllers/TicketMessagesController.cs
  • Modify: xtrakt-tickets-be/src/Web/Controllers/AgentsController.cs
  • Modify: xtrakt-platform-be/src/xTrakt.Platform.Application/Services/SupportAccessService.cs (create if absent — the support-access feature must emit when admin users read tenant data)
  • Test: xtrakt-tickets-be/tests/unit/ComplianceAudit/TicketAuditTests.cs
  • Test: xtrakt-platform-be/tests/xTrakt.Platform.Client.Tests/SupportAccessAuditTests.cs

Event types:

  • ticket.assigned
  • ticket.internal_note.added
  • ticket.escalated
  • ticket.attachment.viewed
  • support.session.started (admin begins acting on behalf of a tenant)
  • support.session.ended
  • support.tenant_data.read (admin reads tenant data — coarsely per page/list, not per row)
  • support.tenant_data.modified

Payload guidance: include tenantId (the target tenant), actorUserId (the support engineer), ticketId where applicable, reason string (from the support-access ticket / approval). Never include message bodies, attachment file names, customer email, or PII fields.

  • Step 1: Write the failing test for ticket.escalated
[Fact]
public async Task EscalateTicket_EmitsTicketEscalatedEvent()
{
var emitter = Substitute.For<IComplianceAuditEmitter>();
var svc = BuildTicketService(emitter);
await svc.EscalateAsync(SeedTicket.Id, newPriority: "high", ct: default);

await emitter.Received(1).EmitAsync(
Arg.Is<ComplianceAuditEventContract>(e =>
e.EventType == "ticket.escalated" &&
e.SourceService == "xtrakt-tickets-be"),
Arg.Any<CancellationToken>());
}
  • Step 2: Run, expected fail.

  • Step 3: Implement emit calls for the 4 ticket events in Tickets, then for the 4 support. events in Platform.*

For Platform support events, the simplest insertion point is a thin filter/middleware that wraps support-admin endpoints. If a [Authorize(Roles="cogneris-platform-admin")] action targets a different tenant than the admin's home tenant, emit support.tenant_data.read / support.tenant_data.modified with the target tenant ID.

  • Step 4: Add tests for each remaining event type (7 more).

  • Step 5: Run + build for both services

cd xtrakt-tickets-be && dotnet test tests/unit/Web.Tests.csproj --filter FullyQualifiedName~ComplianceAudit && dotnet build src/Web/Web.csproj
cd ../xtrakt-platform-be && dotnet test tests/xTrakt.Platform.Client.Tests/xTrakt.Platform.Client.Tests.csproj --filter FullyQualifiedName~SupportAccessAuditTests && dotnet build src/Web/Web.csproj
  • Step 6: Commit (per repo)
git -C xtrakt-tickets-be commit -am "feat(tickets): emit ticket agent action compliance events"
git -C xtrakt-platform-be commit -am "feat(platform): emit support tenant-data access compliance events"

Task C7: Close Track 3 parent task and validations

  • Step 1: Update Track 3 line 189 in the original section:
- [ ] Emit events for authentication, MFA, API key, webhook, template, extraction, document view/download/share, signature, admin config, terms acceptance, consent change, DSR, retention, legal hold, support access, model/provider config, and billing permission changes.

to:

- [x] Emit events for authentication, MFA, API key, webhook, template, extraction, document view/download/share, signature, admin config, terms acceptance, consent change, DSR, retention, legal hold, support access, model/provider config, and billing permission changes.

(Retention/legal hold remain owned by Track 7 and are emitted there. The completion criterion for Track 3 is the operational events listed in C1–C6 above are wired and tested.)

  • Step 2: Run a cross-service emit smoke against the dev environment
# From any service host with the InternalJwt token:
curl -X POST "$PLATFORM_BASE_URL/internal/compliance-audit/events" \
-H "Authorization: Bearer $INTERNAL_JWT" \
-H "Content-Type: application/json" \
-d '{"tenantId":"smoke-tenant","eventType":"smoke.ping","resourceType":"smoke","sourceService":"manual-smoke"}'

Expected: 200, response body has eventHash and sequenceNumber.

  • Step 3: Commit the plan-checkbox update
git add docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md
git commit -m "docs(compliance): mark Track 3 operational-event emit parent task complete"

Section D — Track 2 CI / release polish

Task D1: PR-visible build/test/security scan statuses

Context: The security-CI workflow exists but it is unclear if it is wired to the pull_request event AND to required status checks in branch protection. This task verifies and (if missing) wires both.

Files (per backend + frontend repo):

  • Modify: .github/workflows/security-ci.yml

  • Modify: branch protection (out-of-band, via the GitHub UI or gh api)

  • Modify: docs/compliance/runbooks/secure-release-review.md (record the canonical list of required statuses)

  • Step 1: Verify the workflow runs on pull_request

rg -n "^on:" -A 10 .github/workflows/security-ci.yml

Expected: a pull_request: block exists with branches: [main, release/*].

  • Step 2: If absent, add it
# .github/workflows/security-ci.yml
on:
push:
branches: [main, "release/*"]
pull_request:
branches: [main, "release/*"]
  • Step 3: Identify canonical job names that must be required
rg -n "name:" .github/workflows/security-ci.yml | sed 's/^.*name: //'

Expected canonical names (record these in the runbook): secret-scan, npm-audit, dotnet-audit, sbom, trivy-fs, trivy-config, changed-service-build.

  • Step 4: Apply branch protection via gh api
gh api -X PUT repos/xtrakt-ai/xtrakt-platform-be/branches/main/protection \
--input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": ["secret-scan","npm-audit","dotnet-audit","sbom","trivy-fs","trivy-config","changed-service-build"]
},
"enforce_admins": false,
"required_pull_request_reviews": {"required_approving_review_count": 1, "dismiss_stale_reviews": true},
"restrictions": null
}
JSON

Repeat for every repo in scope: xtrakt-platform-be, xtrakt-sso-be, xtrakt-vision-be, xtrakt-ecm-be, xtrakt-sign-be, xtrakt-billing-be, xtrakt-tickets-be, xtrakt-template-be, xtrakt-portal-be, xtrakt-id-fe, xtrakt-platform-fe, xtrakt-portal-fe, xtrakt.base, xtrakt.webapi.

  • Step 5: Open a noop PR per repo to confirm checks render

For each repo, create a no-op branch (touch a comment in the README, push, open PR via gh pr create) and verify the GitHub UI shows the seven required statuses. Close the PR without merging once verified.

  • Step 6: Update Track 2 validation

In the plan, change:

- [ ] A pull request shows build/test/security scan statuses.

to:

- [x] A pull request shows build/test/security scan statuses. *(Verified 2026-05-19 across the 14 in-scope repos; required statuses configured via branch protection.)*
  • Step 7: Commit and push
git add .github/workflows/security-ci.yml docs/compliance/runbooks/secure-release-review.md \
docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md
git commit -m "ci: wire security-ci to PR events and document required status checks"

Task D2: Service-specific tests inside changed-service checks (✅ shipped 2026-05-20 — umbrella only)

Notes from shipped form:

  • run_for_service(repo_path, service_name, *, commit_sha, evidence_root, artifact_dir) lives in scripts/compliance/run_changed_service_checks.py alongside the existing build-mode code. It detects three service shapes: src/Web/Web.csprojdotnet test, src/package.json with vitest/ng testnpm run test … --reporter=junit (run from src/), package.json at repo root with vitestnpm test … --reporter=junit. When no entry point is present the function returns tests_run: False so the build does not fail on test-less services.
  • Counts are parsed from TRX (ResultSummary/Counters) for .NET and JUnit XML (testsuites aggregate + child testsuite fallback) for vitest, into {tests_run, passed, failed, skipped, duration_ms}.
  • CLI mode selector: --mode {build,tests} (defaults to build for back-compat). --mode tests requires --evidence-out; --commit defaults to GITHUB_SHA. Per-service evidence written to <evidence_root>/<service>/<commit>.json plus an aggregate summary.json.
  • 7 pytest cases in scripts/compliance/test_run_changed_service_checks.py: .NET / Angular / Vite happy path, no-op when no entry, evidence-file write, TRX parsing, JUnit parsing. All green.
  • Workflow: .github/workflows/security-ci.yml gained Changed-service test evidence (after the existing build step; sets CHANGED_SERVICE_TESTS_FAILED) and Upload changed-service test evidence (90-day retention, if-no-files-found: ignore, if: always()). The hard-gates step at the bottom was not touched — failures are surfaced via ::error:: but don't yet block until production data confirms no flakiness.
  • Evidence layout documented in docs/compliance/evidence/changed-service-checks/README.md.
  • Plan checkbox line 140 flipped [x] with shipped annotation.
  • Caveat: the umbrella is not a git repo, so there is no PR for this work. The script + workflow + README live on the shared filesystem; whichever consuming repo holds the authoritative copy of scripts/compliance/ and .github/workflows/security-ci.yml needs to absorb this diff before D2 takes effect in CI.

Files:

  • Modify: scripts/compliance/run_changed_service_checks.py

  • Modify: .github/workflows/security-ci.yml

  • Create: docs/compliance/evidence/changed-service-checks/README.md

  • Step 1: Write a small Python test asserting the script invokes dotnet test for a backend service and npm test for a frontend service

# scripts/compliance/test_run_changed_service_checks.py
import subprocess
from unittest.mock import patch
from scripts.compliance.run_changed_service_checks import run_for_service

def test_runs_dotnet_test_for_backend(tmp_path):
(tmp_path / "src" / "Web").mkdir(parents=True)
(tmp_path / "src" / "Web" / "Web.csproj").write_text("<Project/>")
with patch("subprocess.run") as run:
run.return_value = subprocess.CompletedProcess([], 0, "", "")
result = run_for_service(tmp_path, service_name="xtrakt-platform-be")
assert any("dotnet test" in " ".join(call.args[0]) for call in run.call_args_list)
assert result["tests_run"] is True

def test_runs_npm_test_for_frontend(tmp_path):
(tmp_path / "src" / "package.json").write_text('{"scripts":{"test":"vitest"}}')
with patch("subprocess.run") as run:
run.return_value = subprocess.CompletedProcess([], 0, "", "")
result = run_for_service(tmp_path, service_name="xtrakt-platform-fe")
assert any("npm test" in " ".join(call.args[0]) for call in run.call_args_list)
  • Step 2: Run, expected fail

Run: cd <repo-root> && python -m pytest scripts/compliance/test_run_changed_service_checks.py -q Expected: AttributeError — run_for_service not defined.

  • Step 3: Extend run_changed_service_checks.py

Add a run_for_service(repo_path, service_name) -> dict that:

  • For .NET services (presence of src/Web/Web.csproj): runs dotnet test --no-restore --filter "Category!=Integration" --logger trx --results-directory <artifact-dir> and parses TRX to count pass/fail.

  • For Angular FE (presence of src/package.json with vitest/ng test): runs npm run test -- --run --reporter=junit and parses JUnit XML.

  • For Vue/Vite FE (presence of package.json with vitest): runs npm test -- --run --reporter=junit.

  • Writes a per-service evidence file at docs/compliance/evidence/changed-service-checks/<service>/<commit>.json with {tests_run, passed, failed, skipped, duration_ms}.

  • Step 4: Run tests, expected pass

  • Step 5: Wire into the workflow

# .github/workflows/security-ci.yml — extend the changed-service-build job
- name: Run changed-service tests
run: python scripts/compliance/run_changed_service_checks.py --mode tests \
--service-list "${{ steps.discover.outputs.services }}" \
--evidence-out docs/compliance/evidence/changed-service-checks/
- name: Upload changed-service test evidence
uses: actions/upload-artifact@v4
with:
name: changed-service-tests
path: docs/compliance/evidence/changed-service-checks/
  • Step 6: Update Track 2 implementation log

Change:

- [ ] Expand changed-service checks with service-specific tests where safe and deterministic.

to:

- [x] Expand changed-service checks with service-specific tests where safe and deterministic.
  • Step 7: Commit
git add scripts/compliance/run_changed_service_checks.py \
scripts/compliance/test_run_changed_service_checks.py \
.github/workflows/security-ci.yml \
docs/compliance/evidence/changed-service-checks/README.md \
docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md
git commit -m "ci: run service-specific tests in changed-service-build and capture as evidence"

Task D3: Capture real Cloud Run image digest + revision in deployment evidence

Context: collect_deployment_metadata.py already accepts digests and revisions via flag; what's missing is the Cloud Run deploy workflow in each backend repo actually passing them through.

Files (per backend repo with a cloud-run-deploy-*.yml):

  • Modify: .github/workflows/cloud-run-deploy-dev.yml

  • Modify: .github/workflows/cloud-run-deploy-prod.yml (if exists)

  • Step 1: Read the current deploy step

rg -n "gcloud run deploy" .github/workflows/cloud-run-deploy-*.yml
  • Step 2: Extend the deploy step to capture digest + revision
# .github/workflows/cloud-run-deploy-dev.yml
- name: Deploy to Cloud Run
id: deploy
run: |
gcloud run deploy ${{ env.SERVICE_NAME }} \
--image "${IMAGE_URI}" \
--region "${REGION}" \
--format=json > /tmp/cr-deploy.json
DIGEST=$(jq -r '.status.imageDigest // .spec.template.spec.containers[0].image' /tmp/cr-deploy.json)
REVISION=$(jq -r '.status.latestCreatedRevisionName // .metadata.name' /tmp/cr-deploy.json)
echo "image_digest=$DIGEST" >> $GITHUB_OUTPUT
echo "revision=$REVISION" >> $GITHUB_OUTPUT

- name: Record deployment evidence
run: |
python scripts/compliance/collect_deployment_metadata.py \
--service "${{ env.SERVICE_NAME }}" \
--image-digest "${{ steps.deploy.outputs.image_digest }}" \
--revision "${{ steps.deploy.outputs.revision }}" \
--commit "${{ github.sha }}" \
--workflow-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--evidence-out docs/compliance/evidence/releases/latest/deployments/
  • Step 3: Verify locally with a no-op test that collect_deployment_metadata.py writes the expected JSON shape — add a unit test if one does not exist already.

  • Step 4: Open a test PR per repo, deploy to dev, verify the deployment evidence file has a real digest (sha256:…) and a non-empty revision name.

  • Step 5: Update Track 2 implementation log

Change:

- [ ] Connect deployment workflows to provide real image digests and Cloud Run revisions.

to:

- [x] Connect deployment workflows to provide real image digests and Cloud Run revisions.
  • Step 6: Commit (per repo)
git add .github/workflows/cloud-run-deploy-*.yml
git commit -m "ci: capture Cloud Run image digest and revision in deployment evidence"

Task D4: Approval threshold policy for release gates (✅ shipped 2026-05-20 — umbrella only, soft-warn until 2026-05-26)

Notes from shipped form:

  • Policy table + HIGH-risk globs + rollout window documented in docs/compliance/runbooks/secure-release-review.md § 5.1. Doc bumped to v0.2.
  • Cross-reference + minimum-approval summary added to docs/compliance/change/change-risk-classification-standard.md. Doc bumped to v0.2.
  • scripts/compliance/collect_release_metadata.py gained module-level APPROVAL_THRESHOLDS ({LOW:1, MEDIUM:1, HIGH:2, EMERGENCY:0}) and HIGH_RISK_PATH_GLOBS (9 globs, in sync with the runbook).
  • Pure helpers: meets_approval_threshold(risk_class, approvals) (defaults unknown classes to MEDIUM=1, conservative) and compute_risk_class(changed_files) (matches via fnmatch.fnmatchcase, returns HIGH if any glob matches, MEDIUM otherwise; LOW and EMERGENCY are reviewer-asserted via --risk-class).
  • New enforce_approval_threshold(args) reads changed-services.json → risk class, hits the existing collect_pr_reviews flow → approval count, prints a single approval-threshold: … line + GitHub-Actions annotation, returns 0 (pass / soft-warn), 1 (fail), or 2 (unclassifiable).
  • CLI added --enforce-approval-threshold, --risk-class {LOW,MEDIUM,HIGH,EMERGENCY}, and --soft-warn. The original required-arg contract for the full evidence-collection flow is preserved when enforcement is the only flag set; the prior --changed-services … invocation continues to work unchanged.
  • 18 pytest cases in scripts/compliance/test_collect_release_metadata.py: per-class thresholds, EMERGENCY edge, unknown-class default, MEDIUM-for-empty-diff, 9 parametrized HIGH glob matches, mixed-diff escalation, and a smoke check that the in-code glob list matches the runbook.
  • Workflow: new Enforce approval threshold (warn-only until 2026-05-26) step in .github/workflows/security-ci.yml gated on github.event_name == 'pull_request', running with both continue-on-error: true AND --soft-warn (belt-and-braces — either flag alone is sufficient; both make it impossible to block merges in the warn window).
  • 2026-05-26 cutover: remove continue-on-error: true AND drop --soft-warn from the step. No code change needed beyond those two yaml lines.
  • Plan checkbox line 142 flipped [x] with shipped annotation.
  • Same umbrella-caveat as D2 — no PR; consuming repo needs to absorb the diff.

Files:

  • Modify: docs/compliance/runbooks/secure-release-review.md

  • Modify: docs/compliance/change/change-risk-classification-standard.md

  • Modify: scripts/compliance/collect_release_metadata.py (enforce minimum approvals based on risk class)

  • Test: scripts/compliance/test_collect_release_metadata.py

  • Step 1: Document the policy

In secure-release-review.md, add a section "Approval Threshold by Risk Class":

| Risk class | Minimum reviewer approvals | Required reviewers |
|------------|----------------------------|------------------------------|
| LOW | 1 | any maintainer |
| MEDIUM | 1 | any maintainer (CODEOWNERS) |
| HIGH | 2 | CODEOWNERS + security review |
| EMERGENCY | post-merge review within 24h | CODEOWNERS |

Security-sensitive paths that automatically escalate to HIGH:
- xtrakt.base/src/Security/**, xtrakt.base/src/Privacy/**
- xtrakt-sso-be/** (any change)
- xtrakt-billing-be/src/**/Stripe**.cs
- docs/compliance/policies/**
- docs/compliance/soc2/control-matrix.md
- .github/workflows/security-ci.yml, .github/workflows/compliance-evidence.yml
- scripts/compliance/**
  • Step 2: Write the failing test for the enforcement script
# scripts/compliance/test_collect_release_metadata.py
from scripts.compliance.collect_release_metadata import meets_approval_threshold

def test_high_risk_requires_two_approvals():
assert meets_approval_threshold(risk_class="HIGH", approvals=1) is False
assert meets_approval_threshold(risk_class="HIGH", approvals=2) is True

def test_low_risk_requires_one_approval():
assert meets_approval_threshold(risk_class="LOW", approvals=1) is True
assert meets_approval_threshold(risk_class="LOW", approvals=0) is False
  • Step 3: Run, expected fail.

  • Step 4: Implement meets_approval_threshold and a CLI flag --enforce-approval-threshold that returns non-zero exit if the PR does not meet the threshold for its computed risk class.

APPROVAL_THRESHOLDS = {"LOW": 1, "MEDIUM": 1, "HIGH": 2, "EMERGENCY": 0}

def meets_approval_threshold(risk_class: str, approvals: int) -> bool:
return approvals >= APPROVAL_THRESHOLDS.get(risk_class.upper(), 1)

Compute risk_class from the existing changed-files diff: if any path matches the HIGH-risk globs, return HIGH; else MEDIUM by default.

  • Step 5: Run, expected pass.

  • Step 6: Wire into security CI as a soft warning first (non-blocking for 1 week), then promote to blocking.

# .github/workflows/security-ci.yml
- name: Enforce approval threshold (warn-only until 2026-05-26)
continue-on-error: true
run: python scripts/compliance/collect_release_metadata.py --enforce-approval-threshold
  • Step 7: Update Track 2 implementation log

Change:

- [ ] Decide approval threshold policy for release gates.

to:

- [x] Decide approval threshold policy for release gates. *(Policy documented in docs/compliance/runbooks/secure-release-review.md, enforced by collect_release_metadata.py — soft-warn until 2026-05-26 then blocking.)*
  • Step 8: Commit
git add docs/compliance/runbooks/secure-release-review.md \
docs/compliance/change/change-risk-classification-standard.md \
scripts/compliance/collect_release_metadata.py \
scripts/compliance/test_collect_release_metadata.py \
.github/workflows/security-ci.yml \
docs/superpowers/plans/2026-05-14-soc2-gdpr-compliance-readiness.md
git commit -m "feat(compliance): enforce approval threshold by risk class in release evidence"

Cross-cutting follow-ups (deferred, not part of completion criteria)

  • Sampling/aggregation policy for high-volume extraction events (review 30 days after C2 rollout — open a separate spec).
  • Move the xTrakt.Base.Compliance Refit / HttpClient configuration to xtrakt.webapi if more than half of in-scope services duplicate it.
  • Decide whether Audit-export auditor packages should bundle SSO PrivilegedAccessAuditEvent rows as an attachment alongside the Platform trail (low priority — current bridge provides duplication that auditors can cross-check).

Open business decisions blocking final sign-off

  • Confirm operational owner for the Platform /internal/compliance-audit/events endpoint capacity and on-call.
  • Confirm whether xtrakt.base 10.0.17 (added by Task A2) should be cut from main immediately or held to align with the next planned xtrakt.base release window.
  • Confirm branch-protection role on every in-scope repo has admin access for the engineer running Task D1.