Compliance Controls (NIS2 / ISO 27001)
This page is the traceability spine for Productify's audit & compliance layer. It maps each in-scope NIS2 / ISO 27001 Annex A requirement to the control that implements it and to the evidence — a named, runnable audit query or generated report — that proves the control operates.
The full requirement → control → evidence → owner matrix is in Controls matrix below. This section (Phase 1) first establishes the logging event catalogue: the security events the frameworks require us to record, and whether the current AuditLog substrate records them.
Related security docs:
- Access Model — the authorization model these controls audit.
- Session Policy & Logout — session lifecycle events.
Audit substrate
Two ent entities carry all audit evidence — no parallel compliance store:
| Entity | Scope | Written by |
|---|---|---|
AuditLog | Operator/customer domain events (auth, membership, credential, config, export) | Handlers, GraphQL resolvers, cron sweeps |
ESBAuditLog | Per-request ESB proxying (method, path, status, redacted headers/bodies) | internal/esb on every proxied request |
AuditLog.action is a fixed enum. Its Phase-1 baseline vocabulary is:
create | update | delete | view | export | import | login | logout | otherPhase 1 (Task 2) extends this with login_failed, rotate, revoke, and expire, and adds an actor_type (user / machine_user / system) so machine-user and cron-originated events are attributable without a User edge.
Logging events (framework requirement → coverage)
The following security events MUST be logged under ISO 27001 A.8.15 (logging) and A.8.16 (monitoring), and the corresponding NIS2 Art. 21 technical measures (incident handling, access control, cryptography). Each row carries a coverage verdict — every MUST-log row has one, which is the Task-1 checklist assertion.
| # | Security event | Framework | AuditLog.action | Verdict | Closed by |
|---|---|---|---|---|---|
| L1 | Authentication success (interactive login / OIDC) | A.8.15, NIS2 access-control | login | gap — enum exists, not emitted | Task 3 |
| L2 | Authentication failure | A.8.15, A.8.16 | login_failed (new) | gap — enum missing + not emitted | Task 2, Task 3 |
| L3 | Logout / session end | A.8.15 | logout | gap — enum exists, confirm emission | Task 3 |
| L4 | Credential created (PAT, machine-user) | A.8.15, A.5.17 | create | partial — CLI-login PAT mint audited (Wave 4); direct PAT/machine-user create not | Task 3 |
| L5 | Credential rotated | A.8.15, A.8.24 | rotate (new) | gap — enum missing + no rotation | Task 2, Task 11 |
| L6 | Credential revoked | A.8.15, A.5.17 | revoke (new) | gap — enum missing + not emitted | Task 2, Task 3 |
| L7 | Credential expired | A.8.15, A.5.17 | expire (new) | gap — enum missing; expiry enforced but not audited | Task 2, Task 12 |
| L8 | Membership / grant created | A.5.18, A.8.2 | create | covered — invite acceptance audited (Wave 4) | — |
| L9 | Membership / grant revoked | A.5.18, A.8.2 | delete | partial — confirm self-service revoke emits | Task 3 |
| L10 | Role / privilege change | A.8.2 | update | gap — not consistently emitted | Task 3 |
| L11 | Data export (audit / access-review) | A.8.15, incident evidence | export | gap — enum exists, no export path yet | Task 8, Task 9 |
| L12 | Admin configuration change | A.8.9, A.8.15 | update | partial — some config writes audited | Task 3 |
| L13 | Access review sign-off | A.5.18, A.8.2 | other | gap — no attestation record yet | Task 10 |
| L14 | Audit retention prune | A.8.15 (log integrity) | delete (actor_type=system) | gap — no retention sweep yet | Task 7 |
| L15 | Chain anchor / verification | A.8.15 (log integrity) | other (actor_type=system) | gap — no chain yet | Task 5, Task 6 |
Coverage summary of the current substrate — confirmed by grepping AuditLog.Create across manager/internal/:
internal/handlers/invites.go— invite acceptance →create(L8, covered).internal/handlers/cli_login.go— device-grant PAT mint →create(part of L4).internal/esb/audit.go— every proxied ESB request (separateESBAuditLog).
Everything else in the table above is a gap or partial and is closed by the Phase 1–3 tasks named in the "Closed by" column. The redaction guarantee (Task 4) applies to all of these writers.
Controls matrix
The spine of the compliance layer: each in-scope NIS2 / ISO 27001 requirement, the control that implements it (with the Wave-5 task that shipped it), the evidence — a named, runnable query from the Task-14 evidence registry (manager/internal/audit/evidence.go) or a named report/endpoint — and the owner. Every evidence name in the "Evidence" column resolves to a registered query; controls owned by an earlier security wave are marked referenced.
Cross-links: the threat model (auth-model.md, auth-security Task 13) frames the adversary these controls defend against; the access model defines the authorization vocabulary the access-review control (A.5.18 / A.8.2) reports over.
| Framework requirement | Control (Wave-5 task) | Evidence | Owner |
|---|---|---|---|
| ISO A.8.15 Logging — security events recorded | Complete event coverage: login/login_failed, credential create/rotate/revoke/expire, membership, export, config (Tasks 2–3); redaction of secrets before persist (Task 4) | privilege_changes_in_period, login_failures_in_period | Platform |
| ISO A.8.15 Log integrity — records tamper-evident | Append-only + per-row SHA256 hash chain (prev_hash/row_hash), in-place update rejected (Task 5); periodic verify + anchor (Task 6) | chain_verification_status | Platform |
| ISO A.8.16 Monitoring — anomalous auth detected | login_failed events + rate-limit lockout (Task 3); audit_chain_verification_failures_total alert metric (Task 6) | login_failures_in_period | Platform |
| ISO A.8.15 Retention — records kept on policy, deletion audited | Per-action retention windows + sweep that re-anchors then prunes and self-audits with a high-water hash (Task 7) | retention_prunes_in_period | Platform |
| Incident evidence / SIEM handoff (NIS2 Art. 23) | Time/entity-filtered JSON-lines + CEF export with row_hash for downstream re-verification; admin-gated (Task 8) | GET /api/audit/export (admin) | Platform |
| ISO A.5.18 / A.8.2 Access rights — reviewed periodically | Access-grant report from the user2* join tables (Task 9); recorded sign-off attestation over the report's content hash (Task 10) | last_access_review_per_scope, GET /api/access/review (admin) | Platform + reviewer |
| ISO A.5.17 Authentication information — credential lifecycle managed | Machine-user + backend registration-token rotation, enforced by a max-age sweep, returned once, audited rotate (Task 11); PAT expiry enforced + audited expire (Task 12) | credential_rotations_in_period, credential_expiries_in_period | Platform |
| ISO A.8.24 Cryptography — sensitive columns encrypted at rest | Referenced — AES-256-GCM ValueScanner on secret columns | auth-security Task 9 / manager-improvements Task 16 | |
| ISO A.8.15 No secret/PII in logs | Central redactor over metadata/changes + ESB header maps (Task 4); referenced ESB bearer-token redaction | Task 4 (this plan) + manager-improvements Task 9 |
CEF field mapping
The CEF export (Task 8) emits one event per row:
CEF:0|Productify|Manager|1.0|<action>|<action> on <entity_type>|<severity>|<extension>Severity is 6 for login_failed (a security-relevant failure), 3 otherwise. The extension carries rt (created_at, ms epoch), suid (actor id), src (ip), and cs1..cs4 = entity id / row_hash / prev_hash / actor_type. The row_hash and prev_hash let a SIEM re-verify the chain segment on ingest.
Referenced controls (owned by earlier waves)
These are not re-implemented here — the matrix references them so the requirement-to-evidence trace is complete:
- Encryption-at-rest for the machine-user / backend / PAT secret columns is owned by auth-security Task 9 / manager-improvements Task 16. Rotation (Task 11) stores rotated secrets into those already-encrypted columns.
- ESB bearer-token redaction is owned by manager-improvements Task 9; the Task-4 central redactor pairs with it (shared policy), it does not replace it.