dorfteich/apps/api/src/audit/audit-actions.ts
Claude Fable 5 4c7f001cab
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m44s
CI / Build container images (pull_request) Successful in 4m42s
CI / Auth e2e pack (pull_request) Successful in 9m15s
CI / Import/export fidelity gate (pull_request) Successful in 59s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
#215: trusted reverse-proxy header / mTLS client-certificate path
For perimeters that authenticate before the application (ADR 0021 §4).
Off unless BOTH AUTH_PROXY_HEADER and AUTH_PROXY_TRUSTED_PEERS are set —
nothing about the header is guessed. The peer check runs against the TCP
peer address only (a forwarded header is attacker-influenced): a request
carrying the header from any other peer is rejected outright and audited
as auth.proxy_rejected (catalogue v1.4) — that is a spoof attempt, not a
misconfiguration — even when a valid session cookie rides along. From a
trusted peer the header IS the identity; a session cookie never
escalates beyond it; with the feature off the header is inert.

Mapping is explicit (AUTH_PROXY_MAP: username or e-mail); deliberately
no just-in-time creation — the header carries no verified address. The
mTLS variant (AUTH_PROXY_MODE=mtls-dn) maps the configured attribute
(default CN) out of the certificate subject DN the TLS terminator
forwards, under the same peer rules. Session-less proxy requests key the
read trail per user (user:<id>).

The trust boundary is stated in security.md (the section an assessor
reads closest), the VS-NfD security documentation and the hardening
guide's deploy table. Tests cover all four decisions: off = inert,
trusted peer authenticates (username and DN mapping), untrusted peer
rejected + audited, no escalation past a session cookie.

Refs #215.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 12:50:09 +02:00

67 lines
3.2 KiB
TypeScript

/**
* The audit event catalogue (issue #201): every action id the trail may
* carry, with the severity the stdout line is stamped with. This const is
* the CODE half of the published catalogue in
* `docs/architecture/audit-events.md` — `audit-catalogue.test.ts` fails
* whenever the two drift, so an id cannot be added, renamed, or removed
* without its documentation moving in the same commit.
*
* Compatibility promise (the reason this exists): ids are never repurposed.
* New events may be added (minor catalogue version); an id that stops being
* emitted is retired in the catalogue document, its meaning frozen forever —
* so an operator's SIEM rules survive our releases.
*/
export const AUDIT_EVENTS = {
'api.token_created': { severity: 'info' },
'api.token_revoked': { severity: 'info' },
'api.write': { severity: 'info' },
'audit.pruned': { severity: 'info' },
'auth.email_verified': { severity: 'info' },
'auth.identity_linked': { severity: 'notice' },
'auth.login_failed': { severity: 'warning' },
'auth.login_succeeded': { severity: 'info' },
'auth.password_reset': { severity: 'notice' },
'auth.proxy_rejected': { severity: 'warning' },
'auth.signup': { severity: 'info' },
'backup.restore_requested': { severity: 'warning' },
'backup.run_triggered': { severity: 'info' },
'backup.settings_changed': { severity: 'notice' },
'file.integrity_failed': { severity: 'critical' },
'grant.created': { severity: 'notice' },
'grant.deleted': { severity: 'notice' },
'job.triggered': { severity: 'info' },
'member.added': { severity: 'notice' },
'member.removed': { severity: 'notice' },
'member.role_changed': { severity: 'notice' },
'page.classification_lowered': { severity: 'warning' },
'page.classification_raised': { severity: 'notice' },
'plugin.installed': { severity: 'notice' },
'plugin.mode_set': { severity: 'notice' },
'plugin.pond_toggled': { severity: 'info' },
'plugin.uninstalled': { severity: 'notice' },
'pond.purged': { severity: 'notice' },
'quota.override_cleared': { severity: 'notice' },
'quota.override_set': { severity: 'notice' },
'read_trail.pruned': { severity: 'info' },
'settings.changed': { severity: 'notice' },
'setup.admin_created': { severity: 'notice' },
'setup.completed': { severity: 'info' },
'setup.preseeded': { severity: 'info' },
'setup.smtp_stored': { severity: 'info' },
'user.deleted': { severity: 'notice' },
'user.disabled_set': { severity: 'notice' },
'user.pseudonymized': { severity: 'notice' },
'user.site_admin_set': { severity: 'notice' },
'user.verification_resent': { severity: 'info' },
} as const satisfies Record<string, { severity: AuditSeverity }>;
/** Severity vocabulary of the catalogue — syslog-inspired, four levels are
* enough for rule routing (critical pages someone, warning feeds detection,
* notice is configuration drift, info is lifecycle noise). */
export type AuditSeverity = 'info' | 'notice' | 'warning' | 'critical';
/** A catalogued action id — the ONLY thing {@link AuditService.record}
* accepts, so an uncatalogued event cannot be emitted (compile-time), and
* the doc fence keeps the catalogue document in step (test-time). */
export type AuditAction = keyof typeof AUDIT_EVENTS;