Eviworx
Docs

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.

🔒
Funktionen
✓ SHA-256-Hash-Kette über alle Entitäten
✓ Änderungsschutz per Datenbank-Trigger
✓ PII-Schwärzung (E-Mail, Telefon, IBAN)
✓ Nächtliche Prüfung der gesamten Kette
✓ Eigene Kette je Organisation
✓ Redis-Puffer bei DB-Ausfall (bis 7 Tage)
✓ 9 Audit-Domains (AUTH, ENTITY, SLA …)
✓ 5 Severity-Levels (DEBUG bis CRITICAL)
✓ Volltextsuche (Action, Entität, Akteur)
✓ Mehrsprachiges Aktivitätsprotokoll (inkl. Jobs)

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/eventsAlle Events abrufen (mit Filtering)
GET/api/audit/enterprise/events/:idEinzelnes Event (mit changes, metadata)
GET/api/audit/enterprise/statisticsStatistiken (Counts by Domain, Severity) inkl. chainStatus
GET/api/audit/enterprise/domainsAlle Domains abrufen
GET/api/audit/enterprise/categoriesAlle Categories abrufen

Audit-Domains

Domain Beschreibung Beispiel-Actions
AUTHAuthentifizierung & SessionsLOGIN, OAUTH_LOGIN, AUTH_CHECK, LOGOUT, REFRESH, PASSWORD_RESET_REQUESTED, PASSWORD_RESET_COMPLETED
ENTITYCRUD-Operationen auf EntitiesCREATE, UPDATE, DELETE, ARCHIVE, RESTORE, SUBSTITUTE_BYPASS, SUBSTITUTE_UNAVAILABLE
ADMINAdministrative ActionsUSER_CREATED, ROLE_CHANGED, USER_LOCK_KEPT, SETTINGS_UPDATED, PAUSE_WORKERS
SECURITYSecurity-EventsPERMISSION_DENIED, BRUTE_FORCE_DETECTED, SSRF_BLOCKED, SUBSTITUTE_BLOCKED
SLASLA-MonitoringSLA_WARNING, SLA_BREACH, SLA_CRITICAL, SLA_MET
WORKFLOWWorkflow-ExecutionWORKFLOW_STARTED, STEP_COMPLETED, APPROVAL_GRANTED
SYSTEMSystem-EventsCRONJOB_EXECUTED, WORKER_PAUSED, BACKUP_CREATED
DATA_ACCESSDatenzugriff (GDPR)EXPORT, BULK_DOWNLOAD, PII_ACCESSED
NOTIFICATIONNotification-SystemNOTIFICATION_SENT, NOTIFICATION_SUPPRESSED, TEMPLATE_UPDATED, TYPE_ENFORCED

Severity-Levels

Severity Beschreibung Beispiele
DEBUGEntwicklung & FehleranalyseDetaillierte System-Logs
INFONormale OperationenLOGIN, CREATE_TICKET, WORKFLOW_COMPLETED
WARNINGVerdächtig, aber nicht kritischLOGIN_FAILED (3x), SLA_WARNING, WORKER_PAUSED
ERRORFehler, die Aufmerksamkeit brauchenSLA_BREACH, PERMISSION_DENIED, WEBHOOK_FAILED
CRITICALKritische Security-EventsBRUTE_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
orgIdDie geprüfte Kette; null ist die globale Kette
totalEvents · windowsGeprüfte Events und Anzahl der Abschnitte, in denen das geschah
invalidEvents · firstBrokenAtEvents, deren Hash nicht zu ihrem Inhalt oder deren Verkettung nicht zum Vorgänger passt, und die Sequenz des ersten Fundes
anchorValidDas erste verbliebene Event passt zum Purge-Anker — eine unautorisierte Löschung am Kettenanfang fällt damit auf
purgedEvents · purgeWarningsInhalts-gepurgte Events und davon jene, die nach ihrer Aufbewahrungs-Policy noch nicht fällig gewesen wären
truncatedEin 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:

StufeWirkung
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
domainAUTH, ENTITY, ADMIN, SECURITY, SLA, WORKFLOW, SYSTEM, DATA_ACCESS, NOTIFICATION
categorySub-Kategorie (z.B. LOGIN, TICKET, SSRF)
actionSpezifische Action (z.B. LOGIN_FAILED, CREATE)
severityDEBUG, INFO, WARNING, ERROR, CRITICAL
outcomeSUCCESS, FAILURE, DENIED, PARTIAL, SKIPPED
actorTypeUSER, SYSTEM, API_KEY, WORKFLOW, CRONJOB, EXTERNAL
actorIdUser-ID oder System-Identifier
entityTypeTICKET, INCIDENT, PROBLEM, ASSET, etc.
entityIdEntity-ID (z.B. Ticket-ID)
searchVolltextsuche (action, actionDetail, description, category, entityName, entityType, actorName)
fromDateDatum-Filter (ISO-8601)
toDateDatum-Filter (ISO-8601)
limitItems pro Seite (1-100, Default: 50)
offsetPagination-Offset
sortByoccurredAt, severity, category, action
sortOrderasc 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
ticketsdelete, restore, bulk, createAsEmail, changeMailbox
incidentsdelete, restore, declareMajor, approveClosure, approveReopen, acknowledgeDataBreach
problemsdelete, restore
changesdelete, restore, approve, reject
assetsdelete, restore, export, bulkEdit, bulkDelete, manageTypes, manageTypePermissions, manageCategories, manageLocations, managePolicies, manageClusters
inventory · knowledgeBase · elibraryrestore · delete, publish, restore · delete
licensesviewKeys, restore, bulkUpdate, bulkLinkToContract, bulkUnlinkFromContract
contractsdelete, restore, export, bulkUpdate
workflowsstartWorkflow, publishTemplates, editTemplates, deleteTemplates, restoreTemplates
cronjobsdelete, restore, retry, hideWorkers, pauseWorkers
userscreate, edit, archive, erase, dataExport, manageRoles, manageManagers, reset2FA
settingsviewEmail, editEmail, viewIntegrations, editIntegrations, viewSecurity, editSecurity, editGeneral, editNumbering, editSLA, manageRoles, manageCategories, sendBroadcast
approvals · agents · inboundMailboxesmanageConfigs, manageGroups, manageMemberships · manageGroups, manageGroupAccess · delete, manageAccess
costCenters · customReports · emailSignaturesmerge, import, delete · deleteAll · delete
auditenterpriseView

Permissions

Permission Beschreibung
audit.enterpriseViewAudit-Events, Statistik, Domains und Kategorien lesen
audit.activityViewAktivitä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_FOUND404Event-ID existiert nicht
FORBIDDEN403Fehlende Permission (audit.enterpriseView)
VALIDATION_ERROR400Ungü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)