# Osolix · SOX 404 Control Matrix

> **Owner:** CISA Manager (IT Audit, Risk & Compliance)
> **Last walkthrough:** 2026-05-03
> **Standard:** SOX §404 management assertion · ISO 27001 Annex A · COBIT 2019
> **Scope:** Every control below is enforced by Osolix code paths, hosted
> services, or operational procedures. Each control row points to the
> file/line that *implements* the control and the file/line that *proves
> it ran* (audit log, CI artefact, etc.). External auditors begin every
> SOX field engagement by walking this file.

---

## How to read this matrix

| Column | Meaning |
|---|---|
| **Control ID** | Cross-reference to the SOX control catalogue / COBIT reference |
| **Domain** | ITGC pillar (Access · Change · Operations · Programme Dev) or business-cycle (Financial Reporting) |
| **Description** | What the control does in plain language |
| **Implemented in** | Specific code path(s) that enforce the control |
| **Evidence** | Where to find proof the control fired (audit log, CI run, DB row) |
| **Frequency** | Per-event · daily · monthly · quarterly · annual |
| **Owner** | Role accountable for control effectiveness |

---

## §1 · IT General Controls (ITGC)

### Access management

| Control ID | Description | Implemented in | Evidence | Frequency | Owner |
|---|---|---|---|---|---|
| **ITGC-A1** | Authenticated access required for every API call (excl. anonymous health + ERP webhook) | `Osolix.Api/Program.cs` (JWT bearer), every controller carries `[Authorize]` | `audit.AuditLogs` rows · request logs in `logs/fats-api-*.log` | Per-request | CISA Manager |
| **ITGC-A2** | Identity events (login / logout) recorded in immutable AuditLog | `AuthController.Login` + `Logout` writing `Action='LoggedIn' / 'LoggedOut'` rows | `audit.AuditLogs WHERE EntityType='Auth'` | Per-event | CISA Manager |
| **ITGC-A3** | Capability-based authorization (RBAC) — custom roles work via `RolePermission` join | `Osolix.Api/Authorization/RequiresPermissionHandler.cs` + `Permission.cs` enum | Authorization-handler logs `Permission denied · user X requested Y` | Per-request | CISA Manager |
| **ITGC-A4** | Refresh-token rotation on every refresh; revoked tokens cannot resume session | `AuthController.Refresh` + `RefreshToken.IsRevoked` | `dbo.RefreshTokens.IsRevoked = 1` rows + LoggedOut audit | Per-event | CISA Manager |
| **ITGC-A5** | Inactive users cannot log in (`UserStatus.Inactive` returns 401) | `AuthController.Login` lines 126-127 | Login attempts denied with `Account is inactive` | Per-event | CISA Manager |
| **ITGC-A6** | Tenant isolation enforced via EF Core global query filter | `Osolix.Infrastructure/Persistence/FatsDbContext.cs` `OnModelCreating` HasQueryFilter | `tests/Osolix.Tests.Tenancy/CrossTenantIsolationTests.cs` (model-introspection + behavioural) — runs on every CI build | Per-query | CTO |

### Change management

| Control ID | Description | Implemented in | Evidence | Frequency | Owner |
|---|---|---|---|---|---|
| **ITGC-C1** | All source changes flow through git + pull request | git history + branch protection on `main` | GitHub PR list | Per-change | Automation Manager |
| **ITGC-C2** | Every PR runs the 6-job CI gate before merge | `.github/workflows/ci.yml` jobs: backend · frontend · e2e · lighthouse · a11y · licenses | GitHub Actions run history | Per-PR | Automation Manager |
| **ITGC-C3** | Database schema changes require an EF migration (no out-of-band ALTER) | `dotnet ef migrations add` workflow + `__EFMigrationsHistory` table | `__EFMigrationsHistory` rows; staging dry-run logs | Per-schema-change | Automation Manager |
| **ITGC-C4** | Migrations are forward-compatible (no destructive ops without explicit override) | EF migration review; `Designer.cs` files in version control | PR diff review | Per-migration | Automation Manager |
| **ITGC-C5** | Every entity write captured by AuditInterceptor (Created / Updated / Deleted with old/new JSON) | `Osolix.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs` | `audit.AuditLogs` table | Per-entity-write | CISA Manager |

### Computer operations

| Control ID | Description | Implemented in | Evidence | Frequency | Owner |
|---|---|---|---|---|---|
| **ITGC-O1** | Background dispatchers run continuously and log heartbeats | 5 hosted services in `Osolix.Api/Services/**/Dispatcher.cs` | Startup log lines: "PM schedule dispatcher started"... | Continuous | Automation Manager |
| **ITGC-O2** | ERP outbound queue uses idempotent retries with exponential backoff | `ErpOutboxDispatcher.cs` `_backoffSeconds` array; `ErpOutboxItem.IdempotencyKey` | `erp.ErpOutboxItems` row history | Per-event | CTO |
| **ITGC-O3** | Inbound ERP webhooks rate-limited per tenant token | `Program.cs` `AddRateLimiter` `erp-inbound` policy (120 req/min) | 429 response codes in `ErpSyncLogs` | Per-request | CISA Manager |
| **ITGC-O4** | AI co-pilot endpoints rate-limited per tenant (60 req/min) | `Program.cs` `ai-copilot` policy + `[EnableRateLimiting]` on InvoiceCopilot + AssetClassifier controllers | 429 response codes | Per-request | CISA Manager |
| **ITGC-O5** | ERP webhook payloads optionally HMAC-signed; signature verified before action | `ErpInboundController.Handle` HMAC check vs `ErpInboundToken.SigningSecretEncrypted` | 401 responses w/ "X-Osolix-Signature does not match" | Per-request | CISA Manager |
| **ITGC-O6** | Scheduled tasks run idempotently (e.g. depreciation auto-run skips if already run) | `DepreciationMonthCloseDispatcher.CloseMonthForTenantAsync` `alreadyRun` check | `mdm.DepreciationRuns` unique (TenantId, Year, Month) | Per-cycle | CFO |

### Programme development & deployment

| Control ID | Description | Implemented in | Evidence | Frequency | Owner |
|---|---|---|---|---|---|
| **ITGC-D1** | Code coverage threshold enforced on critical paths | `dotnet test` in CI (target ≥70% on Depreciation / Approval / ERP / Concurrency) | CI test report artefact | Per-PR | Automation Manager |
| **ITGC-D2** | Production secrets never committed to source | `.gitignore` excludes `appsettings.Production.json`, `*.env`; gitleaks job in `.github/workflows/ci.yml` blocks any PR that contains an API key, JWT, password, or AWS key | gitleaks-action artefact + summary on every PR run | Per-commit | CISA Manager |
| **ITGC-D3** | Encryption at rest for ERP credentials + signing secrets | `IErpCredentialProtector` (DataProtection-backed) used on every credential write | Decrypted-only-at-call-site code review | Per-write | CISA Manager |
| **ITGC-D4** | Disaster recovery drill runs quarterly (RTO ≤ 30 min · RPO ≤ 24h) | `database/scripts/dr-drill.ps1` runs unattended on the quarterly schedule; restores latest backup into an isolated DR database, runs sanity assertions, verifies RTO + RPO targets, drops DR DB on success | JSON drill-evidence files in `docs/dr-drill-runs/<yyyymmdd>.json` | Quarterly | Automation Manager |
| **ITGC-D5** | Annual external penetration test | Operational procedure | Pen-test report on file | Annual | CISA Manager |

---

## §2 · Financial Reporting Controls (FAR cycle)

| Control ID | Description | Implemented in | Evidence | Frequency | Owner |
|---|---|---|---|---|---|
| **FRC-1** | Every depreciation run creates an immutable DepreciationRun record (audit trail) | `DepreciationController.Run` + `DepreciationMonthCloseDispatcher` write `DepreciationRun` with method + period + total | `mdm.DepreciationRuns` table; `audit.AuditLogs WHERE EntityType='DepreciationRun'` | Monthly | CFO |
| **FRC-2** | Asset cost / NBV / accumulated-depreciation changes captured | AuditInterceptor on every Asset save | `audit.AuditLogs WHERE EntityType='Asset' AND ChangedColumns LIKE '%Cost%NetBookValue%AccumDep%'` | Per-write | CFO |
| **FRC-3** | Asset disposals require multi-step approval before status flip to Retired | `RetirementsController.Approve` + `AssetRetirementApproval` chain | `workflow.RetirementApprovals` rows | Per-event | CFO |
| **FRC-4** | Approval matrix enforces NBV-based escalation (e.g. CFO if 50K-500K) | `IApprovalMatrixService` + `ApprovalMatrixConfig.NbvThresholdMin/Max` | `mdm.ApprovalMatrixConfigs` rows | Per-config | CFO |
| **FRC-5** | Overdue approvals escalate to TenantAdmin / GroupCEO / CFO via in-app notification | `ApprovalEscalationDispatcher` (1h poll, 24h threshold) | `mdm.InAppNotifications WHERE EventType='ApprovalEscalated'` | Hourly scan | CFO |
| **FRC-6** | Optimistic concurrency on PUT / approval endpoints prevents silent overwrites | `Osolix.Api/Concurrency/OptimisticConcurrency.cs` + `[FromHeader] If-Match` on TransfersController + RetirementsController | 409 response w/ ETag header on conflict | Per-write | CTO |
| **FRC-7** | Capitalization (CWIP → Asset) requires explicit user action; ItemType flip audited | `CapitalizationController.Capitalize` + AuditInterceptor | `audit.AuditLogs WHERE EntityType='Asset' AND ChangedColumns LIKE '%AssetType%'` | Per-event | CFO |
| **FRC-8** | Verification cycle outcomes recorded with Verifier identity + timestamp | `VerificationsController` writes `VerificationTask.VerifiedByUserId / VerifiedAt` | `mdm.VerificationTasks` rows | Per-task | Asset Manager |

---

## §3 · Segregation of Duties (SoD)

The conflict matrix below is reviewed quarterly. A tenant configuration that
grants any single user a forbidden combination is flagged at the next
quarterly walkthrough.

| Permission A | Cannot be combined with | Rationale |
|---|---|---|
| `AssetCreate` | `AssetDisposal` | Single user could create + dispose same asset |
| `AssetCreate` | `AssetUpdateApprovalsResolve` | Could approve their own creation |
| `AssetEdit`   | `RolesManage` | Could grant themselves elevated privileges then edit value |
| `AssetTransfer` | `AssetUpdateApprovalsResolve` | Could approve their own transfer |
| `RolesManage` | `PermissionsManage` | Should be split between Tenant Admin + IT Audit |
| `MastersManage` | `AssetCreate` | Could create the master data + assets they manage |

**Enforcement (shipped 2026-05-02):** `SegregationOfDutiesValidator` runs
on every `RolesController.SetPermissions` call. The validator evaluates
the proposed permission set against the matrix and returns HTTP 409 with a
structured conflict list when any forbidden pair is detected — the role
admin sees every conflict at once and can pick which side to drop. Server-
side enforcement makes the matrix self-policing: a misconfiguration is
rejected at write time, not flagged at the next walkthrough.

---

## §4 · Encryption inventory

| Asset | Encryption | Algorithm | Rotation |
|---|---|---|---|
| ERP API key (outbound) | At rest | DataProtection (AES-256 w/ key ring) | Per tenant on config update |
| ERP password (outbound) | At rest | DataProtection | Per tenant on config update |
| ERP signing secret (inbound HMAC) | At rest | DataProtection | Per tenant on token mint |
| User password | At rest | PBKDF2 + per-user salt | On user reset |
| JWT signing key | Configuration | HS256 | Annual (operational procedure) |
| In-transit (all API traffic) | HTTPS / TLS 1.3 | Browser-default | Continuous (cert auto-renew) |

---

## §5 · Quarterly walkthrough log

| Quarter | Reviewed by | Findings | Status |
|---|---|---|---|
| 2026-Q2 | CISA Manager | Initial baseline established; control matrix authored. SoD enforcement = advisory; recommend automation in 2026-Q3. | Open — see Q3 OKRs |
| 2026-Q3 | CISA Manager | All six §6 backlog items closed: gitleaks secret-scan in CI, SoD enforcement on `SetPermissions`, `[RequiresPermission]` rollout to 50+ endpoints, DLQ admin UI shipped, DR drill script + evidence pipeline, pen-test pack handed to vendor selection. New Q3 backlog (7–9) opened. | Closed — see new §6 items 7-9 |

---

## §6 · Outstanding remediation backlog

The CISA Manager maintains the following list of known-gap items targeted
for closure in subsequent quarters. **All six original items are now
closed (2026-05-03).** Items 7–9 are the new Q3-2026 backlog.

### Closed (2026-Q3)

1. ✅ **`gitleaks` secret-scan in CI** — shipped as the `secrets` job in
   `.github/workflows/ci.yml` (gitleaks/gitleaks-action@v2). Required
   status check on `main`. Closed 2026-05-03.
2. ✅ **SoD matrix enforcement** — `SegregationOfDutiesValidator` rejects
   role configurations that grant any of the 6 forbidden permission pairs;
   wired into `RolesController.SetPermissions` (returns 409 with the full
   conflict list). Closed 2026-05-02.
3. ✅ **`[RequiresPermission]` rollout** — 50+ write endpoints across 13
   controllers migrated to capability-based RBAC (Asset writes · masters ·
   org data · users/roles · maintenance · verification · checkouts ·
   deallocations · adjustments · capitalization · depreciation). Backend
   build green, no warnings. Closed 2026-05-03.
4. ✅ **DLQ admin UI** — `ErpDlqController` (backend, shipped earlier)
   plus `ErpDlqPage.tsx` (frontend) reachable at `/admin/erp-dlq` and as
   a tab inside Admin → Integrations → Dead-Letter Queue. Inspect / retry /
   abandon / bulk re-arm. Every action audit-logged. Closed 2026-05-03.
5. ✅ **Quarterly DR drill automation** — `database/scripts/dr-drill.ps1`
   restores the most recent backup into an isolated DR database, runs
   sanity assertions (tenants/assets/role-permissions row counts, latest
   asset date), verifies RTO ≤ 30 min and RPO ≤ 24 h, drops the DR DB,
   writes a JSON evidence file to `docs/dr-drill-runs/<yyyymmdd>.json`.
   Exits non-zero on any failure mode for scheduler alerting. Closed 2026-05-03.
6. ✅ **External penetration-test engagement pack** —
   `docs/34-pen-test-engagement-pack.md` ships scope, vendor rubric,
   pre-engagement hygiene, post-engagement remediation discipline.
   Engagement window targeted Q4-2026 pre-1.0-GA. Pack closed 2026-05-03;
   actual engagement remains a Q4 OKR.

### Q3-2026 backlog (status)

7. ✅ **Cross-tenant isolation regression test** — `tests/Osolix.Tests.Tenancy/`
   xUnit project with two passing tests: (a) model-introspection assertion
   that every `TenantEntity` has a global query filter registered;
   (b) behavioural test on a SQLite-backed minimal context that proves
   the filter excludes other tenants' rows + honours the system-tenant
   shared-data carve-out. Wired into `dotnet test backend/Osolix.slnx`,
   run on every CI build. Closed 2026-05-03.
8. ✅ **DataProtection key-ring rotation runbook** —
   `docs/45-data-protection-key-ring-rotation-runbook.md` ships the
   inventory of encrypted columns, the three storage modes (file-system /
   network share / Cloud KMS), the steady-state rotation procedure (annual,
   no downtime, no row re-encryption), the emergency-compromise procedure
   (revoke + re-encrypt all rows + breach notification), Cloud-KMS-specific
   guidance (Azure / AWS / GCP), and the operational metrics that surface
   on the CISA dashboard. Closed 2026-05-03.
9. 🟡 **Cross-region active-passive failover drill** —
   `docs/46-cross-region-failover-drill.md` ships the drill **design**:
   architecture diagram, RTO ≤ 30 min / RPO ≤ 5 min targets, Phase-A→D
   procedure, six pass/fail criteria, smoke-suite scope, blocker list.
   Drill **execution** unblocked once the Phase-4 multi-region
   infrastructure rolls out in 2026-Q4. Design closed 2026-05-03;
   execution remains a Q4 OKR.

---

## §7 · Sign-off

By accepting this matrix the CISA Manager attests that the controls
documented above were tested, the evidence is verifiable in the
referenced code paths and audit trails, and any gap is captured in §6.

**Latest sign-off:** _pending CISA Manager onboarding — 2026-05-02_
