Files
arboretum/packages/server/src/core/audit-log.ts
Johan LEROY 33295a7aaf feat: clean install (Gitea en dur, scan off) + durcissement sécurité entreprise
Gitea
- lien « Code source » hardcodé (REPO_SOURCE_URL) vers le dépôt, toujours visible
- retrait complet du réglage configurable (api.ts, route+store settings, SettingsView, i18n, help, tests)

Découverte des dépôts
- aucune racine de scan par défaut → pas de scan au premier démarrage (clean install)
- corrige la découverte des dépôts à l'INTÉRIEUR d'une racine qui est elle-même un repo
  (depth 0 = conteneur de scan, on descend ; depth > 0 = feuille)

Sécurité « enterprise-deployable »
- en-têtes HTTP durcis (CSP, X-Frame-Options, nosniff, Referrer-Policy, HSTS conditionnel, no-store API), header Server retiré
- permissions DB 0o600 / dossier de données 0o700 ; error handler sanitisé ; garde Content-Type sur les mutations
- chiffrement au repos AES-256-GCM des secrets (server_secret, clé privée VAPID) via SecretBox
- journal d'audit (migration #7) + endpoint /audit-logs + RGPD export/effacement + UI Réglages
- SECURITY.md, docs/ENTERPRISE_DEPLOYMENT.md, sections README EN/FR, SBOM CycloneDX en CI

CI
- pack-smoke packe depuis le contexte du package (cd packages/server) au lieu de « npm pack -w » :
  corrige l'embarquement de @arboretum/shared dans le tarball

Purge des données personnelles du dépôt public
- suppression de spikes/s4-discovery/result.json, reformulation du VERDICT
- chemin de test générique, anonymisation des 5 fixtures de dialogues
2026-06-18 16:44:25 +02:00

66 lines
2.5 KiB
TypeScript

// Journal d'audit : trace persistante des opérations sensibles (création/révocation de tokens,
// changements de réglages, génération de secrets, abonnements push, CRUD groupes). Exigence de
// conformité entreprise (GDPR/SOX/ISO 27001). Règle ABSOLUE : ne JAMAIS journaliser un secret en
// clair — `details` ne contient que des métadonnées non sensibles (ids, labels, compteurs).
import { randomUUID } from 'node:crypto';
import type { AuditLogEntry } from '@arboretum/shared';
import type { Db } from '../db/index.js';
export type AuditResult = 'ok' | 'denied' | 'error';
export interface AuditEntry {
/** tokenId de l'acteur, ou 'system' (opérations automatiques), ou 'anonymous' (avant auth). */
actor: string;
/** verbe.objet, ex. 'token.create', 'settings.update', 'login.failure'. */
action: string;
resourceId?: string | null;
details?: Record<string, unknown> | null;
result?: AuditResult;
}
/** Enregistre une entrée d'audit. Best-effort : une erreur d'écriture ne casse jamais l'opération métier. */
export function recordAudit(db: Db, e: AuditEntry): void {
try {
db.prepare(
'INSERT INTO audit_logs (id, ts, actor, action, resource_id, details, result) VALUES (?, ?, ?, ?, ?, ?, ?)',
).run(
randomUUID(),
new Date().toISOString(),
e.actor,
e.action,
e.resourceId ?? null,
e.details ? JSON.stringify(e.details) : null,
e.result ?? 'ok',
);
} catch {
/* l'audit ne doit jamais faire échouer l'action auditée */
}
}
/** Liste paginée par date décroissante (curseur `before` = ts strictement inférieur). */
export function listAudit(db: Db, opts: { limit: number; before?: string | null }): AuditLogEntry[] {
const limit = Math.min(Math.max(Math.trunc(opts.limit) || 50, 1), 200);
const rows = (
opts.before
? db
.prepare(
'SELECT id, ts, actor, action, resource_id AS resourceId, details, result FROM audit_logs WHERE ts < ? ORDER BY ts DESC LIMIT ?',
)
.all(opts.before, limit)
: db
.prepare(
'SELECT id, ts, actor, action, resource_id AS resourceId, details, result FROM audit_logs ORDER BY ts DESC LIMIT ?',
)
.all(limit)
) as Array<{ id: string; ts: string; actor: string; action: string; resourceId: string | null; details: string | null; result: string }>;
return rows.map((r) => ({ ...r, details: r.details ? safeParse(r.details) : null }));
}
function safeParse(s: string): unknown {
try {
return JSON.parse(s);
} catch {
return s;
}
}