332 lines
11 KiB
Markdown
332 lines
11 KiB
Markdown
# OpenSpec Change: enterprise-audit-logging
|
|
|
|
- **Change ID**: enterprise-audit-logging
|
|
- **Package**: `@suaveplan/audit-logging`
|
|
- **Category**: universal
|
|
- **Target path**: `universal/auth/audit-logging`
|
|
- **Status**: PENDING
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/audit-logging.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/audit-logging` on the genesis branch from scratch at enterprise quality. The package provides a tamper-proof, GDPR-compliant audit trail: `AuditLogger` with cryptographic chain-integrity signing, pluggable storage adapters, retention policy enforcement, GDPR data export, and a rich event taxonomy spanning auth, authorization, data access, and system events. The main-branch implementation was tightly coupled to SurrealDB and Node's `node:crypto`; the genesis rewrite is database-agnostic with a `StorageAdapter` interface, uses `@suaveplan/crypto` for HMAC signing, adds structured errors via `@suaveplan/error`, Zod schemas, an injectable clock and ID generator, OTel instrumentation, property-based tests, and mitata benchmarks.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
The package does not exist on genesis yet — this is a fresh build.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | Base class for `AuditError`; `defineCodes` for `AUDIT.*` error codes |
|
|
| `@suaveplan/types` (`workspace:^`) | `Clock` (injectable timestamps), `IdGenerator` (event IDs), `LoggerLike`, `StorageLike` |
|
|
| `@suaveplan/events` (`workspace:^`) | `EventBusLike` for audit lifecycle events |
|
|
| `@suaveplan/crypto` (`workspace:^`) | HMAC-SHA256 for chain-integrity signing, `zeroize` for secret clearing |
|
|
| `@suaveplan/logger` (`workspace:^`) | Structured logging with trace correlation |
|
|
| `@suaveplan/serialize` (`workspace:^`) | Deterministic JSON serialization for HMAC signing stability |
|
|
| `zod` (`catalog:forms`) | Zod schemas for all public option types and event shapes |
|
|
| `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms |
|
|
| `tslib` (`catalog:build`) | TypeScript runtime helpers |
|
|
|
|
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`).
|
|
|
|
---
|
|
|
|
## Preserved public API surface (from main branch)
|
|
|
|
### Event taxonomy enums
|
|
|
|
```ts
|
|
enum AuditEventType {
|
|
// Authentication
|
|
AUTH_LOGIN, AUTH_LOGOUT, AUTH_REGISTER, AUTH_PASSWORD_CHANGE,
|
|
AUTH_PASSWORD_RESET, AUTH_MFA_ENABLE, AUTH_MFA_DISABLE,
|
|
AUTH_SESSION_CREATE, AUTH_SESSION_REVOKE,
|
|
AUTH_API_KEY_CREATE, AUTH_API_KEY_REVOKE,
|
|
// Authorization
|
|
AUTHZ_PERMISSION_CHECK, AUTHZ_PERMISSION_GRANT, AUTHZ_PERMISSION_REVOKE,
|
|
AUTHZ_ROLE_ASSIGN, AUTHZ_ROLE_UNASSIGN,
|
|
// Data access
|
|
DATA_CREATE, DATA_READ, DATA_UPDATE, DATA_DELETE, DATA_EXPORT, DATA_IMPORT,
|
|
// System
|
|
SYSTEM_CONFIG_CHANGE, SYSTEM_SECURITY_ALERT, SYSTEM_RATE_LIMIT, SYSTEM_ERROR,
|
|
// GDPR
|
|
GDPR_DATA_EXPORT, GDPR_DATA_DELETE, GDPR_CONSENT_CHANGE
|
|
}
|
|
|
|
enum AuditSeverity {
|
|
DEBUG = "debug",
|
|
INFO = "info",
|
|
WARNING = "warning",
|
|
ERROR = "error",
|
|
CRITICAL = "critical"
|
|
}
|
|
```
|
|
|
|
### Core types
|
|
|
|
```ts
|
|
interface AuditActor {
|
|
userId?: string
|
|
sessionId?: string
|
|
apiKeyId?: string
|
|
ipAddress?: string
|
|
userAgent?: string
|
|
tenantId?: string
|
|
}
|
|
|
|
interface AuditTarget {
|
|
type: string
|
|
id: string
|
|
metadata?: Record<string, unknown>
|
|
}
|
|
|
|
interface AuditEvent {
|
|
id: string
|
|
type: AuditEventType | string
|
|
severity: AuditSeverity
|
|
actor: AuditActor
|
|
target?: AuditTarget
|
|
details?: Record<string, unknown>
|
|
timestamp: Date
|
|
previousHash?: string // chain-integrity: hash of the previous event
|
|
hash: string // HMAC-SHA256 of this event's canonical form
|
|
tenantId?: string
|
|
correlationId?: string
|
|
}
|
|
|
|
interface AuditLoggerOptions {
|
|
storage: AuditStorageAdapter
|
|
secretKey: string
|
|
tableName?: string
|
|
retentionPolicy?: AuditRetentionPolicy
|
|
enableChainIntegrity?: boolean
|
|
clock?: Clock
|
|
idGenerator?: IdGenerator
|
|
logger?: LoggerLike
|
|
bus?: EventBusLike
|
|
}
|
|
|
|
interface AuditQueryOptions {
|
|
eventTypes?: AuditEventType[]
|
|
userId?: string
|
|
sessionId?: string
|
|
severity?: AuditSeverity | AuditSeverity[]
|
|
targetType?: string
|
|
targetId?: string
|
|
startTime?: Date
|
|
endTime?: Date
|
|
limit?: number
|
|
offset?: number
|
|
order?: "asc" | "desc"
|
|
tenantId?: string
|
|
}
|
|
|
|
interface AuditRetentionPolicy {
|
|
retentionDays: number
|
|
archiveBeforeDelete: boolean
|
|
archiveLocation?: string
|
|
}
|
|
|
|
interface GDPRDataExport {
|
|
userId: string
|
|
exportedAt: Date
|
|
auditEvents: AuditEvent[]
|
|
summary: {
|
|
totalEvents: number
|
|
eventTypes: Record<string, number>
|
|
dateRange: { first: Date; last: Date }
|
|
}
|
|
}
|
|
```
|
|
|
|
### AuditLogger class
|
|
|
|
```ts
|
|
class AuditLogger {
|
|
constructor(options: AuditLoggerOptions)
|
|
log(event: Omit<AuditEvent, 'id' | 'timestamp' | 'hash' | 'previousHash'>): Promise<AuditEvent>
|
|
query(options: AuditQueryOptions): Promise<AuditEvent[]>
|
|
verifyChainIntegrity(events: AuditEvent[]): Promise<ChainVerificationResult>
|
|
exportGDPRData(userId: string, signal?: AbortSignal): Promise<GDPRDataExport>
|
|
applyRetentionPolicy(signal?: AbortSignal): Promise<RetentionResult>
|
|
}
|
|
|
|
interface ChainVerificationResult {
|
|
valid: boolean
|
|
brokenAt?: number // index in the events array where chain breaks
|
|
reason?: string
|
|
}
|
|
|
|
interface RetentionResult {
|
|
eventsDeleted: number
|
|
eventsArchived: number
|
|
errors: string[]
|
|
}
|
|
```
|
|
|
|
### Storage adapter interface
|
|
|
|
```ts
|
|
interface AuditStorageAdapter {
|
|
save(event: AuditEvent): Promise<void>
|
|
query(options: AuditQueryOptions): Promise<AuditEvent[]>
|
|
getLatestEvent(tenantId?: string): Promise<AuditEvent | null>
|
|
deleteExpired(before: Date): Promise<number>
|
|
archive(before: Date, location: string): Promise<number>
|
|
count(options?: Partial<AuditQueryOptions>): Promise<number>
|
|
}
|
|
|
|
// In-memory adapter for tests and single-node use
|
|
class MemoryAuditAdapter implements AuditStorageAdapter
|
|
```
|
|
|
|
### Error codes
|
|
|
|
```ts
|
|
type AuditErrorCode =
|
|
| "AUDIT_STORAGE_ERROR"
|
|
| "AUDIT_CHAIN_BROKEN"
|
|
| "AUDIT_INVALID_EVENT"
|
|
| "AUDIT_EXPORT_FAILED"
|
|
| "AUDIT_RETENTION_FAILED"
|
|
| "AUDIT_SECRET_KEY_MISSING"
|
|
|
|
class AuditError extends SuaveplanError<AuditErrorCode>
|
|
function registerAuditErrorCodes(): void
|
|
```
|
|
|
|
---
|
|
|
|
## New additions for genesis
|
|
|
|
### 1. Database-agnostic `AuditStorageAdapter`
|
|
|
|
Replaces the hard SurrealDB coupling. `MemoryAuditAdapter` ships in-package. Database-specific adapters (SurrealDB, PostgreSQL, SQLite) live in separate packages.
|
|
|
|
### 2. Chain integrity via `@suaveplan/crypto`
|
|
|
|
HMAC-SHA256 signing uses `@suaveplan/crypto`'s `hmac` (delegates to `@suaveplan/crypto-hash`) instead of `node:crypto`. The secret key is `zeroize`d on `AuditLogger` disposal. Canonical JSON serialization for HMAC input uses `@suaveplan/serialize`'s deterministic encoder.
|
|
|
|
### 3. Clock and IdGenerator injection
|
|
|
|
`AuditLogger` accepts `clock?: Clock` and `idGenerator?: IdGenerator`. All `Date.now()` and `randomUUID()` calls delegated to injected instances so audit events have deterministic timestamps and IDs in tests.
|
|
|
|
### 4. AbortSignal on long-running operations
|
|
|
|
`exportGDPRData` and `applyRetentionPolicy` accept an optional `AbortSignal`; abort mid-iteration throws `AuditError` with `AUDIT_EXPORT_FAILED` or `AUDIT_RETENTION_FAILED`.
|
|
|
|
### 5. OTel instrumentation
|
|
|
|
- `audit.log` span + `audit.events_logged_total` counter (label `event_type`, `severity`)
|
|
- `audit.query` span + histogram `audit.query_duration_ms`
|
|
- `audit.chain_verify` span + `audit.chain_verify_total` counter (label `valid: bool`)
|
|
- `audit.gdpr_export` span + `audit.gdpr_export_total` counter
|
|
- `audit.retention_apply` span + `audit.events_deleted_total` / `audit.events_archived_total` counters
|
|
|
|
### 6. Zod schemas
|
|
|
|
`AuditEventSchema`, `AuditActorSchema`, `AuditTargetSchema`, `AuditQueryOptionsSchema`, `AuditLoggerOptionsSchema`, `AuditRetentionPolicySchema`, `GDPRDataExportSchema`.
|
|
|
|
### 7. EventBus integration
|
|
|
|
Optional `bus?: EventBusLike` on `AuditLoggerOptions`. Emits:
|
|
|
|
| Event | Payload |
|
|
|---|---|
|
|
| `audit:event:logged` | `{ id, type, severity, tenantId }` |
|
|
| `audit:chain:broken` | `{ brokenAt, reason }` |
|
|
| `audit:retention:applied` | `{ deleted, archived }` |
|
|
|
|
### 8. Property-based tests (fast-check)
|
|
|
|
- HMAC stability: same event data always produces the same hash regardless of object property order.
|
|
- Chain integrity: any mutation of any event in a sequence breaks verification at the mutated index.
|
|
- Query filter correctness: events returned by `query({ userId: X })` all have `actor.userId === X`.
|
|
- Retention idempotency: applying retention policy twice with the same cutoff date deletes zero additional events on the second run.
|
|
|
|
### 9. Benchmark suite
|
|
|
|
- `AuditLogger.log` — 10 000 iterations with chain-integrity enabled vs disabled.
|
|
- `AuditLogger.query` — 10 000 queries on a MemoryAuditAdapter with 10 000 pre-loaded events.
|
|
- `verifyChainIntegrity` — on 100, 1 000, 10 000 event chains.
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
universal/auth/audit-logging/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/
|
|
index.ts # AuditEventType, AuditSeverity
|
|
types/
|
|
audit-logging.types.ts
|
|
audit-logging.types.md
|
|
adapters/
|
|
storage-adapter/
|
|
storage-adapter.ts # AuditStorageAdapter interface
|
|
storage-adapter.md
|
|
storage-adapter.types.ts
|
|
index.ts
|
|
memory-adapter/
|
|
memory-adapter.ts
|
|
memory-adapter.md
|
|
memory-adapter.test.ts
|
|
index.ts
|
|
core/
|
|
audit-logger/
|
|
audit-logger.ts # AuditLogger class
|
|
audit-logger.md
|
|
audit-logger.types.ts
|
|
audit-logger.test.ts
|
|
index.ts
|
|
chain-integrity/
|
|
chain-integrity.ts # computeHash, verifyChain
|
|
chain-integrity.md
|
|
chain-integrity.test.ts
|
|
index.ts
|
|
gdpr/
|
|
gdpr-export.ts # exportGDPRData logic
|
|
gdpr-export.md
|
|
gdpr-export.test.ts
|
|
index.ts
|
|
retention/
|
|
retention-policy.ts # applyRetentionPolicy logic
|
|
retention-policy.md
|
|
retention-policy.test.ts
|
|
index.ts
|
|
schemas/
|
|
audit-logging.schemas.ts
|
|
audit-logging.schemas.md
|
|
audit-logging.schemas.test.ts
|
|
__benchmarks__/
|
|
audit-logging.bench.ts
|
|
tests/
|
|
integration.test.ts
|
|
property.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
**Downstream packages**: auth-related packages and API gateways will write audit events. The storage adapter pattern allows a SurrealDB adapter to be shipped separately.
|
|
|
|
**Breaking changes from main-branch API**: `AuditLoggerOptions.database` (SurrealDB) is replaced by `AuditLoggerOptions.storage` (adapter interface). All other exported symbols are preserved. No `node:crypto` import in the public interface.
|
|
|
|
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/crypto`, `@suaveplan/logger`, `@suaveplan/serialize` must be published to Verdaccio before this package can be built or tested.
|