Enterprise Audit System
Das Enterprise Audit System schreibt Audit-Logs, in denen Manipulationen erkennbar werden: Jedes Event ist per SHA-256-Hash mit seinem Vorgänger verkettet, personenbezogene Daten werden automatisch geschwärzt, der Zustand der Kette ist jederzeit per API abrufbar und die vollständige Prüfung aller Ketten läuft jede Nacht automatisch. Die API unterstützt Compliance-Prüfungen und forensische Auswertungen.
SHA-256 Hash-Chain Funktionsweise
Das Audit-System verkettet Events kryptografisch, sodass jede Manipulation erkennbar wird:
┌─────────────────────────────────────────────────────────────┐
│ HASH-CHAIN STRUKTUR │
└─────────────────────────────────────────────────────────────┘
Event 1 (Genesis):
┌──────────────────────────────────────┐
│ Sequence: 1 │
│ PrevHash: NULL │
│ Data: { action: "LOGIN", ... } │
│ Hash: sha256(data) = abc123... │
└──────────────────────────────────────┘
│
│ (prevHash)
▼
Event 2:
┌──────────────────────────────────────┐
│ Sequence: 2 │
│ PrevHash: abc123... ← Event 1 Hash │
│ Data: { action: "CREATE_TICKET", ...}│
│ Hash: sha256(data) = def456... │
└──────────────────────────────────────┘
│
│ (prevHash)
▼
Event 3:
┌──────────────────────────────────────┐
│ Sequence: 3 │
│ PrevHash: def456... ← Event 2 Hash │
│ Data: { action: "APPROVE", ... } │
│ Hash: sha256(data) = ghi789... │
└──────────────────────────────────────┘
Manipulations-Erkennung:
❌ Versuch, Event 2 zu ändern: • Neu berechneter Hash = xyz999... (≠ def456...) • Event 3 zeigt weiterhin PrevHash = def456... • → Kette gebrochen, Manipulation erkannt
❌ Versuch, Event 2 zu löschen: • Event 3 zeigt PrevHash = def456... • Ein Event mit Hash def456... existiert nicht mehr • → Kette gebrochen, Manipulation erkannt
Hash-Berechnung (PostgreSQL Trigger)
-- DB trigger computes hash AUTOMATICALLY on every INSERT
CREATE TRIGGER audit_event_hash_trigger
BEFORE INSERT ON "AuditEvent"
FOR EACH ROW
EXECUTE FUNCTION audit_event_set_hash();
-- Hash calculation:
-- 1. Lock ChainHead with FOR UPDATE (per org)
-- 2. Fetch prev hash + sequence
-- 3. Build canonical payload (JSONB, keys sorted alphabetically)
-- 4. Compute SHA-256 hash
-- 5. Update ChainHead (last_hash, last_sequence)
-- Immutability trigger:
CREATE TRIGGER audit_event_no_modify
BEFORE UPDATE OR DELETE ON "AuditEvent"
FOR EACH ROW
EXECUTE FUNCTION audit_event_immutable();
-- UPDATE only with SET LOCAL audit.allow_purge = 'true'
-- AND chain/identity columns unchanged (hash, sequence,
-- prev_hash, occurred_at, actor_id, entity_id ...);
-- purged_at, once set, can never be removed.
-- Two legitimate forms:
-- a) retention content purge (changes/metadata/... → NULL, purged_at set)
-- b) Art. 17 erasure scrub (actor_name/actor_ip/actor_user_agent/entity_name,
-- which are not part of the hash payload)
-- Every other UPDATE stays forbidden.
-- DELETE only at the chain end (retention stage 2):
-- SET LOCAL audit.allow_delete = 'true'
Chain-Head Tabelle & Purge-Anker
-- One row per organization (performance optimization)
AuditChainHead {
org_id: String (UNIQUE, '__global__' for single-tenant)
last_hash: String (hash of the last event)
last_sequence: BigInt (sequence of the last event)
purged_through_sequence: BigInt -- retention stage 2: how far the
purge_anchor_hash: String? -- prefix was hard-deleted (anchor)
updated_at: DateTime
}
-- Updated by the trigger (FOR UPDATE lock).
-- The purge anchor lets verification check that the first remaining
-- event links to purge_anchor_hash — so a prefix delete is provable
-- and distinguishable from tampering.
API Endpoints
Audit Events Query
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/audit/enterprise/events | Alle Events abrufen (mit Filtering) |
GET | /api/audit/enterprise/events/:id | Einzelnes Event (mit changes, metadata) |
GET | /api/audit/enterprise/statistics | Statistiken (Counts by Domain, Severity) inkl. chainStatus |
GET | /api/audit/enterprise/domains | Alle Domains abrufen |
GET | /api/audit/enterprise/categories | Alle Categories abrufen |
Audit-Domains
| Domain | Beschreibung | Beispiel-Actions |
|---|---|---|
AUTH | Authentifizierung & Sessions | LOGIN, OAUTH_LOGIN, AUTH_CHECK, LOGOUT, REFRESH, PASSWORD_RESET_REQUESTED, PASSWORD_RESET_COMPLETED |
ENTITY | CRUD-Operationen auf Entities | CREATE, UPDATE, DELETE, ARCHIVE, RESTORE, SUBSTITUTE_BYPASS, SUBSTITUTE_UNAVAILABLE |
ADMIN | Administrative Actions | USER_CREATED, ROLE_CHANGED, USER_LOCK_KEPT, SETTINGS_UPDATED, PAUSE_WORKERS |
SECURITY | Security-Events | PERMISSION_DENIED, BRUTE_FORCE_DETECTED, SSRF_BLOCKED, SUBSTITUTE_BLOCKED |
SLA | SLA-Monitoring | SLA_WARNING, SLA_BREACH, SLA_CRITICAL, SLA_MET |
WORKFLOW | Workflow-Execution | WORKFLOW_STARTED, STEP_COMPLETED, APPROVAL_GRANTED |
SYSTEM | System-Events | CRONJOB_EXECUTED, WORKER_PAUSED, BACKUP_CREATED |
DATA_ACCESS | Datenzugriff (GDPR) | EXPORT, BULK_DOWNLOAD, PII_ACCESSED |
NOTIFICATION | Notification-System | NOTIFICATION_SENT, NOTIFICATION_SUPPRESSED, TEMPLATE_UPDATED, TYPE_ENFORCED |
Severity-Levels
| Severity | Beschreibung | Beispiele |
|---|---|---|
DEBUG | Entwicklung & Fehleranalyse | Detaillierte System-Logs |
INFO | Normale Operationen | LOGIN, CREATE_TICKET, WORKFLOW_COMPLETED |
WARNING | Verdächtig, aber nicht kritisch | LOGIN_FAILED (3x), SLA_WARNING, WORKER_PAUSED |
ERROR | Fehler, die Aufmerksamkeit brauchen | SLA_BREACH, PERMISSION_DENIED, WEBHOOK_FAILED |
CRITICAL | Kritische Security-Events | BRUTE_FORCE_DETECTED, SSRF_BLOCKED, SLA_CRITICAL |
API-Beispiele
Events abrufen (mit Filtering)
GET /api/audit/enterprise/events?domain=SECURITY&severity=CRITICAL&fromDate=2026-01-01T00:00:00Z&limit=50&offset=0&sortBy=occurredAt&sortOrder=desc
Response
{
"success": true,
"data": [
{
"id": "clx...",
"occurredAt": "2026-01-27T15:30:00Z",
"ingestedAt": "2026-01-27T15:30:00.123Z",
"sequence": "12345",
"hash": "a3f2c1d4e5b6...89f0",
"domain": "SECURITY",
"category": "ACCESS",
"action": "PERMISSION_DENIED",
"severity": "ERROR",
"outcome": "DENIED",
"actorType": "USER",
"actorId": "clx...",
"actorName": "John Doe",
"actorIp": "192.168.1.100",
"entityType": "TICKET",
"entityId": "clx...",
"entityName": "TKT-2026-000123",
"description": "User attempted to delete ticket without permission",
"containsPII": false
},
{
"id": "clx...",
"occurredAt": "2026-01-27T14:15:00Z",
"sequence": "12344",
"hash": "b4e3f2g5h6i7...12a3",
"domain": "SECURITY",
"category": "SSRF",
"action": "SSRF_BLOCKED",
"severity": "CRITICAL",
"outcome": "DENIED",
"actorType": "USER",
"actorId": "clx...",
"entityType": "CRONJOB",
"entityId": "clx...",
"description": "Webhook URL blocked: private IP 192.168.1.1",
"containsPII": false
}
],
"pagination": {
"total": 2,
"limit": 50,
"offset": 0,
"hasMore": false
}
}
Einzelnes Event mit Details
GET /api/audit/enterprise/events/:id
Response (mit changes & metadata)
{
"success": true,
"data": {
"id": "clx...",
"occurredAt": "2026-01-27T15:30:00Z",
"ingestedAt": "2026-01-27T15:30:00.123Z",
"sequence": "12345",
"hash": "a3f2c1d4e5b6...89f0",
"prevHash": "b4e3f2g5h6i7...12a3",
"hashVersion": 1,
"domain": "ENTITY",
"category": "TICKET",
"action": "UPDATE",
"severity": "INFO",
"outcome": "SUCCESS",
"actorType": "USER",
"actorId": "clx...",
"actorName": "Jane Smith",
"actorIp": "192.168.1.50",
"actorUserAgent": "Mozilla/5.0...",
"entityType": "TICKET",
"entityId": "clx...",
"entityName": "TKT-2026-000123: Printer not working",
"changes": {
"status": {
"old": "OPEN",
"new": "IN_PROGRESS"
},
"assignedAgentId": {
"old": null,
"new": "clx..."
}
},
"metadata": {
"method": "PATCH",
"path": "/api/tickets/clx...",
"durationMs": 45
},
"description": "Ticket status changed from OPEN to IN_PROGRESS",
"containsPII": false,
"redactedFields": [],
"correlationId": "req-abc123",
"sessionId": "ses-def456",
"orgId": null
}
}
Ketten-Integrität: Schnellcheck und Voll-Verifikation
Die Integrität der Kette wird auf zwei Wegen belegt:
| Prüfung | Wann | Umfang |
|---|---|---|
| Schnellcheck | jederzeit per API | Der Kettenkopf wird gegen das letzte gespeicherte Event gehalten. Das Ergebnis steht als chainStatus in der Statistik-Antwort und antwortet unabhängig von der Anzahl der Events sofort. Es erkennt ein abgeschnittenes Kettenende, nicht aber eine Änderung mitten in der Kette. |
| Voll-Verifikation | jede Nacht automatisch | Der Built-in-Job „Audit Chain Verify" rechnet jedes Event nach: Hash, Ketten-Kontinuität, Purge-Anker und Purge-Plausibilität — je Organisations-Kette und über den gesamten Bestand. Das Ergebnis steht in der Job-Historie. |
Schnellcheck abrufen
GET /api/audit/enterprise/statistics
{
"chainStatus": {
"intact": true,
"message": "Chain intact up to sequence 125340"
}
}
Ergebnis der nächtlichen Voll-Verifikation
Der Job prüft jede Kette in Abschnitten und fasst sie je Kette zusammen. Die Zusammenfassung steht im Ergebnis des Laufs:
{
"action": "audit_chain_verify",
"status": "success",
"reason": "Chain verification clean (1 chain(s), 735568 events)",
"metadata": {
"windowSize": 100000,
"truncated": false,
"problems": [],
"summary": [
{
"orgId": null,
"totalEvents": 735568,
"invalidEvents": 0,
"purgedEvents": 0,
"purgeWarnings": 0,
"chainBroken": false,
"anchorValid": true,
"firstBrokenAt": null,
"windows": 8
}
]
}
}
| Feld | Bedeutung |
|---|---|
orgId | Die geprüfte Kette; null ist die globale Kette |
totalEvents · windows | Geprüfte Events und Anzahl der Abschnitte, in denen das geschah |
invalidEvents · firstBrokenAt | Events, deren Hash nicht zu ihrem Inhalt oder deren Verkettung nicht zum Vorgänger passt, und die Sequenz des ersten Fundes |
anchorValid | Das erste verbliebene Event passt zum Purge-Anker — eine unautorisierte Löschung am Kettenanfang fällt damit auf |
purgedEvents · purgeWarnings | Inhalts-gepurgte Events und davon jene, die nach ihrer Aufbewahrungs-Policy noch nicht fällig gewesen wären |
truncated | Ein Schutz gegen Endlosläufe hat gegriffen: mindestens eine Kette wurde nur teilweise geprüft. Der Lauf sagt es, statt still aufzuhören. |
Warum in Abschnitten: Ein Audit-Trail wächst auf Millionen Events. Der Job prüft deshalb Abschnitt für Abschnitt; die Abschnittsgröße steht im Job-Parameter windowSize (Standard 100.000 Events, erlaubt 1.000 bis 250.000). Die Abschnitte sind lückenlos aneinandergesetzt: Der letzte Hash eines Abschnitts wird als Vorgänger des nächsten geprüft. Die Aufteilung macht die Prüfung also nicht schwächer, sie hält nur jeden einzelnen Schritt kurz — der gesamte Bestand wird in einem Lauf geprüft.
Meldung bei einem Befund: Jeder Lauf schreibt genau ein Audit-Event SYSTEM/AUDIT/CHAIN_VERIFY mit der Zusammenfassung aller Ketten — sauber als INFO, teilweise geprüft als WARNING, mit Befund als CRITICAL. Bei einem Bruch, einem ungültigen Anker oder einer Purge-Warnung erhält zusätzlich jeder Träger von audit.enterpriseView einen CRITICAL-Alert (höchstens einmal pro Tag und Empfänger). So bleibt eine Manipulation nicht bis zur nächsten Sichtung unbemerkt.
Statistiken abrufen
GET /api/audit/enterprise/statistics
Response
{
"totalEvents": 125340,
"last30Days": {
"byDomain": [
{ "domain": "AUTH", "count": 5234 },
{ "domain": "ENTITY", "count": 8921 },
{ "domain": "SECURITY", "count": 123 },
{ "domain": "SLA", "count": 456 }
],
"bySeverity": [
{ "severity": "INFO", "count": 12000 },
{ "severity": "WARNING", "count": 1500 },
{ "severity": "ERROR", "count": 234 },
{ "severity": "CRITICAL", "count": 12 }
],
"byOutcome": [
{ "outcome": "SUCCESS", "count": 13000 },
{ "outcome": "FAILURE", "count": 500 },
{ "outcome": "DENIED", "count": 246 }
]
},
"recentCriticalEvents": [
{
"id": "clx...",
"occurredAt": "2026-01-27T22:45:00Z",
"domain": "SECURITY",
"category": "SSRF",
"action": "SSRF_BLOCKED",
"severity": "CRITICAL",
"description": "Webhook URL blocked: private IP 10.0.0.1"
}
],
"chainStatus": {
"intact": true,
"message": "Chain intact up to sequence 125340"
}
}
PII-Scrubbing
Der Audit-Scrubber entfernt sensible Daten automatisch aus changes und metadata: Zugangsdaten UND personenbezogene Felder werden geschwärzt und in redactedFields vermerkt:
Denylist (IMMER redacted): • Credentials: password, secret, token, apikey, authorization • Sessions: cookie, session, jwt, otp, pin • Keys: private_key, encryption_key, signing_key, client_secret • Financial: cvv, cvc, ssn • Access: access_token, refresh_token, bearer PII-Keys (redacted + in redactedFields markiert): • Contact: email, phone, address, street, city, zip • Personal: birthdate, ssn, tax_id, national_id • Financial: iban, bic, bank_account, credit_card • Location: ip_address, gps, latitude, longitude Value-Pattern-Erkennung: • JWT-Tokens: eyJ... Pattern • Bearer-Tokens: "Bearer xyz..." • Base64-Secrets: > 100 characters • API-Keys: 40+ alphanumeric chars • AWS-Keys: AKIA... Pattern • Private Keys: "-----BEGIN PRIVATE KEY-----"
Scrubbing-Beispiel
// Original Data:
{
"username": "john.doe@company.com",
"password": "SecretPassword123!",
"email": "john.doe@company.com",
"apiKey": "sk_live_51H3Kq2eZvKYlo2C...",
"ticketNumber": "TKT-2026-000123"
}
// After Scrubbing:
{
"username": "john.doe@company.com",
"password": "[REDACTED]", // ← Denylist
"email": "[PII_REDACTED]", // ← PII
"apiKey": "[REDACTED]", // ← Denylist
"ticketNumber": "TKT-2026-000123"
}
// Stored in DB:
{
"containsPII": true, // ← event touched PII fields (redacted)
"redactedFields": ["password", "email", "apiKey"],
"changes": { ... scrubbed data ... }
}
Der Wert eines PII-Feldes landet nicht im Log; redactedFields dokumentiert nur, DASS sich das Feld geändert hat. Das reicht für forensische Zwecke und unterstützt die Datensparsamkeit nach DSGVO. entityName bleibt bewusst ein anzeigbarer Klartext-Snapshot (bei USER-Events der Anzeige-NAME, nicht die E-Mail) — dorthin kommt nur, was angezeigt werden darf. changes und metadata eines geschriebenen Events werden nicht nachträglich geschwärzt (das bräche die Hash-Kette); sie werden über die Aufbewahrungsfristen bereinigt.
Retention: zweistufiger Purge
Aufbewahrungsfristen kollidieren mit der Unveränderbarkeit: Ein Event einfach zu löschen würde die Hash-Kette zerreißen, und ein einzelnes 7-Jahres-Compliance-Event früh in der Kette würde die Löschung aller nachfolgenden 90-Tage-Events blockieren. Deshalb wird bei einem fälligen Event der Inhalt gelöscht, während seine Hash-Verkettung erhalten bleibt. Der nächtliche retention_purge (siehe Privacy-Seite) arbeitet zweistufig:
| Stufe | Wirkung |
|---|---|
| 1 — Inhalts-Purge (innerhalb der Kette) | Ein nach seiner Policy fälliges Event (90d/365d/7y je nach Kategorie; aktive Legal Holds ausgenommen) wird ENTLEERT: changes/metadata/description/actor_name/actor_ip/actor_user_agent/entity_name → NULL, purged_at gesetzt. Es BLEIBEN: id, sequence, hash, prev_hash, domain/category/action/outcome/severity, occurred_at, entity_type/id — ein anonymes Skelett. Statistiken funktionieren weiter, die Kette bleibt byte-identisch verkettet. |
| 2 — Zeilen-Delete (am Kettenanfang) | Events älter als die LÄNGSTE Policy (7 Jahre) werden als zusammenhängender Ketten-Präfix hart gelöscht; der Purge-Anker (purged_through_sequence + purge_anchor_hash) wird fortgeschrieben, damit das erste verbleibende Event weiter verifizierbar verkettet ist. |
Für die Chain-Verifikation: Bei einem Event mit purged_at wird die Payload-Hash-Neuberechnung übersprungen (der Inhalt ist entleert — der gespeicherte hash dient weiter der Ketten-Kontinuität). Ein purged_at auf einem Event, das laut Policy noch NICHT fällig wäre, wird als WARNUNG geflaggt — das erschwert, das Purge-Flag als Manipulations-Versteck zu missbrauchen. Jeder Lauf schreibt zudem ein Selbst-Event SYSTEM/AUDIT/RETENTION_PURGE mit Zählwerten.
Hash-Berechnung (Detailliert)
Der Hash wird vom PostgreSQL-Trigger berechnet und ist identisch reproduzierbar:
Schritt 1: Kanonisches Payload erstellen
{
"action": "CREATE_TICKET",
"actorId": "clx-user-123",
"actorType": "USER",
"category": "TICKET",
"changes": {"status": {"old": null, "new": "OPEN"}},
"domain": "ENTITY",
"entityId": "clx-ticket-456",
"entityType": "TICKET",
"hashVersion": 1,
"metadata": {"method": "POST", "path": "/api/tickets"},
"occurredAt": "2026-01-27T15:30:00.123Z",
"orgId": "",
"outcome": "SUCCESS",
"prevHash": "def456...",
"sequence": "12345"
}
Schritt 2: JSONB sortiert Keys alphabetisch
→ deterministische Serialisierung
Schritt 3: JSON-String erstellen
payload_string = JSON.stringify(payload)
Schritt 4: SHA-256 Hash (UTF-8)
hash = sha256(payload_string, 'utf8')
= "a3f2c1d4e5b6789f0..."
Schritt 5: ChainHead aktualisieren
UPDATE AuditChainHead
SET last_hash = "a3f2c1d4e5b6789f0...",
last_sequence = 12345
WHERE org_id = '__global__'
Eigenschaften der Kette
🔐 Sicherheits-Eigenschaften
1. Änderungsschutz:
- • DB-Trigger verhindert UPDATE/DELETE — ausgenommen Aufbewahrungs-Bereinigung und Anonymisierung, bei denen die Kettenfelder unverändert bleiben
- • DELETE nur mit Session-Flag (für Retention nach Jahren)
2. Manipulations-Erkennung:
- • Event ändern → Hash stimmt nicht mehr
- • Event löschen → Chain gebrochen (prevHash zeigt auf gelöschtes Event)
- • Event einfügen → Sequence-Lücke erkennbar
3. Reihenfolge nachweisbar:
- • Sequence-Nummer (global, auto-increment)
- • prevHash verlinkt zum Vorgänger
- • Zeitliche Reihenfolge nachweisbar
4. Multi-Org Isolation:
- • Separate Chain pro Organisation
- • ChainHead mit FOR UPDATE Lock (verhindert Race-Conditions)
Query-Filtering
Filter-Parameter
| Parameter | Beschreibung |
|---|---|
domain | AUTH, ENTITY, ADMIN, SECURITY, SLA, WORKFLOW, SYSTEM, DATA_ACCESS, NOTIFICATION |
category | Sub-Kategorie (z.B. LOGIN, TICKET, SSRF) |
action | Spezifische Action (z.B. LOGIN_FAILED, CREATE) |
severity | DEBUG, INFO, WARNING, ERROR, CRITICAL |
outcome | SUCCESS, FAILURE, DENIED, PARTIAL, SKIPPED |
actorType | USER, SYSTEM, API_KEY, WORKFLOW, CRONJOB, EXTERNAL |
actorId | User-ID oder System-Identifier |
entityType | TICKET, INCIDENT, PROBLEM, ASSET, etc. |
entityId | Entity-ID (z.B. Ticket-ID) |
search | Volltextsuche (action, actionDetail, description, category, entityName, entityType, actorName) |
fromDate | Datum-Filter (ISO-8601) |
toDate | Datum-Filter (ISO-8601) |
limit | Items pro Seite (1-100, Default: 50) |
offset | Pagination-Offset |
sortBy | occurredAt, severity, category, action |
sortOrder | asc oder desc (Default: desc) |
Beispiel-Queries
# All failed logins in the last 24h
GET /api/audit/enterprise/events?domain=AUTH&action=LOGIN&outcome=FAILURE&fromDate=2026-01-26T23:00:00Z
# All critical security events
GET /api/audit/enterprise/events?domain=SECURITY&severity=CRITICAL&sortBy=occurredAt&sortOrder=desc
# All changes to a specific ticket
GET /api/audit/enterprise/events?entityType=TICKET&entityId=clx-ticket-123&category=TICKET
# All actions by a user
GET /api/audit/enterprise/events?actorId=clx-user-456&fromDate=2026-01-01T00:00:00Z
# Full-text search
GET /api/audit/enterprise/events?search=password+reset&limit=20
Automatische Protokollierung kritischer Aktionen
Kritische Aktionen werden automatisch protokolliert: erlaubte Ausführungen als SUCCESS, abgelehnte Versuche als DENIED (mit Grund).
Kritische Aktionen (automatisch protokolliert)
| Feature | Kritische Actions |
|---|---|
tickets | delete, restore, bulk, createAsEmail, changeMailbox |
incidents | delete, restore, declareMajor, approveClosure, approveReopen, acknowledgeDataBreach |
problems | delete, restore |
changes | delete, restore, approve, reject |
assets | delete, restore, export, bulkEdit, bulkDelete, manageTypes, manageTypePermissions, manageCategories, manageLocations, managePolicies, manageClusters |
inventory · knowledgeBase · elibrary | restore · delete, publish, restore · delete |
licenses | viewKeys, restore, bulkUpdate, bulkLinkToContract, bulkUnlinkFromContract |
contracts | delete, restore, export, bulkUpdate |
workflows | startWorkflow, publishTemplates, editTemplates, deleteTemplates, restoreTemplates |
cronjobs | delete, restore, retry, hideWorkers, pauseWorkers |
users | create, edit, archive, erase, dataExport, manageRoles, manageManagers, reset2FA |
settings | viewEmail, editEmail, viewIntegrations, editIntegrations, viewSecurity, editSecurity, editGeneral, editNumbering, editSLA, manageRoles, manageCategories, sendBroadcast |
approvals · agents · inboundMailboxes | manageConfigs, manageGroups, manageMemberships · manageGroups, manageGroupAccess · delete, manageAccess |
costCenters · customReports · emailSignatures | merge, import, delete · deleteAll · delete |
audit | enterpriseView |
Permissions
| Permission | Beschreibung |
|---|---|
audit.enterpriseView | Audit-Events, Statistik, Domains und Kategorien lesen |
audit.activityView | Aktivitäts-Historie der Workflows anzeigen |
audit.enterpriseView ist eine kritische Aktion: Das Recht wird bei jedem Zugriff frisch aus der Datenbank geprüft statt aus dem Rollen-Cache. Ein Rechte-Entzug wirkt damit sofort und nicht erst nach Ablauf des Caches — bei der Einsicht in den Audit-Trail ist genau das der Punkt.
Die Enterprise-Routen stehen nicht nur angemeldeten Benutzern offen: Auch API-Keys dürfen lesen, deren Rolle das jeweilige Recht trägt — etwa für ein SIEM, das den Trail regelmäßig abholt. Maßgeblich ist allein die Rolle, nicht die Art des Aufrufers.
Use-Cases
Use-Case 1: Compliance-Audit
Prüfer möchte alle Ticket-Löschungen der letzten 90 Tage sehen:
GET /api/audit/enterprise/events?domain=ENTITY&category=TICKET&action=DELETE&fromDate=2025-10-28T00:00:00Z&sortBy=occurredAt&sortOrder=desc
Use-Case 2: Security-Incident Investigation
Security-Team untersucht verdächtige Aktivitäten eines Users:
# 1. All actions by the user
GET /api/audit/enterprise/events?actorId=clx-suspicious-user&fromDate=2026-01-27T00:00:00Z
# 2. Failed permissions
GET /api/audit/enterprise/events?actorId=clx-suspicious-user&outcome=DENIED
# 3. All security events
GET /api/audit/enterprise/events?domain=SECURITY&fromDate=2026-01-27T00:00:00Z
Use-Case 3: Forensische Analyse
Ein Ticket wurde unerwartet gelöscht - wer war es?:
# 1. Find DELETE event
GET /api/audit/enterprise/events?entityType=TICKET&entityId=clx-ticket-123&action=DELETE
# Response shows:
{
"actorId": "clx-user-789",
"actorName": "Admin User",
"actorIp": "192.168.1.50",
"occurredAt": "2026-01-27T14:30:00Z",
"metadata": {
"method": "DELETE",
"path": "/api/tickets/clx-ticket-123",
"userAgent": "Mozilla/5.0..."
}
}
# 2. All actions by this user at the same time
GET /api/audit/enterprise/events?actorId=clx-user-789&fromDate=2026-01-27T14:00:00Z&toDate=2026-01-27T15:00:00Z
Use-Case 4: Chain-Verification
Vor einer Compliance-Prüfung: den Zustand der Audit-Kette belegen:
# Quick check (chain head vs. last event)
GET /api/audit/enterprise/statistics
# Response (excerpt):
{
"chainStatus": {
"intact": true,
"message": "Chain intact up to sequence 125340"
}
}
# Full verification of every event: the nightly job. Its result per run
# (events, sections, findings per chain) is in the job history, and every
# run is itself recorded as SYSTEM/AUDIT/CHAIN_VERIFY:
GET /api/audit/enterprise/events?domain=SYSTEM&category=AUDIT&action=CHAIN_VERIFY&limit=30
Die letzten dreißig CHAIN_VERIFY-Events belegen damit lückenlos, dass die Kette in jeder Nacht vollständig nachgerechnet wurde — Prüfumfang und Befund stehen in den metadata des jeweiligen Events.
Database Schema
-- Audit event (immutable, append-only)
AuditEvent {
id: String (UUID, PK)
-- Chain fields (set automatically by the trigger)
sequence: BigInt (UNIQUE, global, auto-increment)
hash: String (64 chars, SHA-256 Hex)
prevHash: String? (64 chars, hash of the predecessor)
hashVersion: Int (Default: 1)
-- Timestamps
occurredAt: DateTime (when the action happened)
ingestedAt: DateTime (when it was logged, Default: NOW())
-- Classification
domain: Enum (AUTH, ENTITY, ADMIN, SECURITY, SLA, WORKFLOW, SYSTEM, DATA_ACCESS, NOTIFICATION)
category: String (sub-category, e.g. LOGIN, TICKET, SSRF)
action: String (CREATE, UPDATE, DELETE, LOGIN_FAILED, etc.)
actionDetail: String?
severity: Enum (DEBUG, INFO, WARNING, ERROR, CRITICAL)
-- Outcome
outcome: Enum (SUCCESS, FAILURE, DENIED, PARTIAL, SKIPPED)
errorCode: String?
errorMessage: String?
-- Actor
actorType: Enum (USER, SYSTEM, API_KEY, WORKFLOW, CRONJOB, EXTERNAL)
actorId: String? (user ID, job ID, etc.)
actorName: String?
actorIp: String?
actorUserAgent: String?
-- Entity
entityType: String? (TICKET, INCIDENT, PROBLEM, ASSET, etc.)
entityId: String?
entityName: String?
-- Data (JSONB)
changes: JSON? (old vs. new values)
metadata: JSON? (additional info)
description: String?
-- PII tracking
containsPII: Boolean (Default: false)
redactedFields: String[]? (array of redacted field paths)
-- Retention
purgedAt: DateTime? (set by the retention content purge)
-- Correlation
correlationId: String? (request ID)
sessionId: String? (session ID)
parentEventId: String? (FK AuditEvent)
-- Multi-org
orgId: String? (NULL = global/single-tenant)
}
-- Indices for performance
CREATE INDEX idx_audit_event_domain ON AuditEvent(domain, occurredAt DESC);
CREATE INDEX idx_audit_event_actor ON AuditEvent(actorId, occurredAt DESC);
CREATE INDEX idx_audit_event_entity ON AuditEvent(entityType, entityId, occurredAt DESC);
CREATE INDEX idx_audit_event_severity ON AuditEvent(severity, occurredAt DESC) WHERE severity IN ('ERROR', 'CRITICAL');
CREATE INDEX idx_audit_event_sequence ON AuditEvent(sequence DESC);
-- Immutability via trigger (no RLS):
-- audit_event_immutable() blocks every UPDATE except the retention
-- purge / erasure scrub under audit.allow_purge (see above),
-- DELETE only if current_setting('audit.allow_delete') = 'true'
CREATE TRIGGER audit_event_no_modify
BEFORE UPDATE OR DELETE ON "AuditEvent"
FOR EACH ROW EXECUTE FUNCTION audit_event_immutable();
-- Chain head (per organization)
AuditChainHead {
org_id: String (PK, '__global__' for single-tenant)
last_hash: String (hash of the last event)
last_sequence: BigInt (sequence of the last event)
purged_through_sequence: BigInt (Default: 0, purge anchor)
purge_anchor_hash: String? (purge anchor)
updated_at: DateTime
}
-- Updated by the trigger (with FOR UPDATE lock)
-- Enables O(1) quick check
Redis-Fallback & Recovery
Routine-Events (INFO mit Ergebnis SUCCESS) werden gesammelt und alle 5 Sekunden geschrieben; Events ab WARNING sowie fehlgeschlagene oder abgelehnte Vorgänge (FAILURE/DENIED) werden sofort geschrieben. Das Protokollieren blockiert keine API-Anfrage. Ist die Datenbank nicht erreichbar, landen die Events in Redis (Schlüssel audit:backup:events:{orgId}, Aufbewahrung bis zu 7 Tage). Beim nächsten Start werden sie nachgetragen, und die Wiederherstellung wird selbst als Event SYSTEM/AUDIT/RECOVER protokolliert.
- Ausfallsicherung: Events überstehen DB-Ausfälle von bis zu 7 Tagen
- Transparenz: Die Wiederherstellung erscheint selbst im Audit-Trail
Best Practices
💡 Tipps
- • audit.enterpriseView nur an Rollen vergeben, die den Audit-Trail wirklich brauchen
- • Das Ergebnis des nächtlichen audit_chain_verify beobachten: Bei einem Befund erhalten alle Träger von audit.enterpriseView einen CRITICAL-Alert
- • Für die Anbindung eines SIEM einen API-Key mit einer Rolle nutzen, die nur audit.enterpriseView trägt
- • Datenbank-Backups extern speichern und nach einem Restore das Erasure-Replay ausführen (siehe Privacy & DSGVO)
- • Für forensische Abfragen die correlationId nutzen: Sie verbindet alle Events einer Anfrage
Error-Handling
| Error Code | HTTP Status | Beschreibung |
|---|---|---|
NOT_FOUND | 404 | Event-ID existiert nicht |
FORBIDDEN | 403 | Fehlende Permission (audit.enterpriseView) |
VALIDATION_ERROR | 400 | Ungültige Query-Parameter |
Monitoring & Alerts
Die Kette wird jede Nacht vom Built-in-Job audit_chain_verify vollständig geprüft (siehe oben); bei einem Befund erhalten alle Träger von audit.enterpriseView einen CRITICAL-Alert. Zusätzlich liefert GET /api/audit/enterprise/statistics jederzeit den Schnellcheck als chainStatus, etwa vor einer Prüfung.
Compliance-Unterstützung
DSGVO-Unterstützung
- ✅ PII-Schwärzung: automatisch, immer aktiv
- ✅ PII-Markierung: containsPII-Flag + redactedFields-Liste
- ✅ Protokollierung von Datenexporten: Jeder Datenexport nach Art. 15/20 wird als DATA_ACCESS-Event protokolliert
- ✅ Aufbewahrung: zweistufige Bereinigung nach Policy (90 Tage / 365 Tage / 7 Jahre), siehe oben
- ✅ Recht auf Löschung: Bei der Anonymisierung eines Benutzers werden Name, IP und User-Agent in seinen Audit-Events geleert (siehe Privacy & DSGVO)
Unterstützung für ISO-27001- und SOC-2-Audits
- ✅ Änderungsschutz: Ein DB-Trigger blockiert nachträgliche Änderungen; Manipulationen werden über die Hash-Kette erkennbar
- ✅ Integritätsnachweis: SHA-256-Kette mit nächtlicher Voll-Verifikation
- ✅ Zugriffskontrolle: Kritische Aktionen werden protokolliert, erlaubte wie abgelehnte (SUCCESS + DENIED)
- ✅ Security-Events: SSRF, Brute-Force, Permission-Denied
- ✅ Forensik: Wer, was, wann und von wo nachvollziehbar
Technische Details
Hash-Algorithmus
// JavaScript implementation (identical to the DB trigger)
function calculateHash(event) {
// 1. Canonical payload
const payload = {
action: event.action,
actorId: event.actorId || '',
actorType: event.actorType,
category: event.category,
changes: event.changes || {},
domain: event.domain,
entityId: event.entityId || '',
entityType: event.entityType || '',
hashVersion: event.hashVersion,
metadata: event.metadata || {},
occurredAt: event.occurredAt.toISOString(),
orgId: event.orgId || '',
outcome: event.outcome,
prevHash: event.prevHash || '',
sequence: event.sequence.toString()
};
// 2. Sort keys (recursively)
const sortedPayload = sortObjectKeys(payload);
// 3. JSON string
const payloadString = JSON.stringify(sortedPayload);
// 4. SHA-256 hash
return crypto
.createHash('sha256')
.update(payloadString, 'utf8')
.digest('hex');
}
Sequence-Vergabe
-- PostgreSQL sequence (global, thread-safe)
CREATE SEQUENCE audit_event_seq START 1;
-- In the trigger:
v_sequence := nextval('audit_event_seq');
NEW."sequence" := v_sequence;
-- Properties:
-- - Uniqueness (no duplicates)
-- - Monotonically increasing
-- - Thread-safe (even with multi-instance)
Lock-Mechanismus
-- Lock ChainHead with FOR UPDATE (per org)
SELECT "last_hash", "last_sequence"
FROM "AuditChainHead"
WHERE "org_id" = v_org_key
FOR UPDATE;
-- Prevents:
-- - Race conditions (2 events at once)
-- - Chain inconsistency (prevHash conflicts)
-- - Sequence duplicates
-- Lock duration: Only during INSERT (< 1ms)