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
This commit is contained in:
65
packages/server/src/core/audit-log.ts
Normal file
65
packages/server/src/core/audit-log.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user