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
66 lines
2.5 KiB
TypeScript
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;
|
|
}
|
|
}
|