Documentation Index
Fetch the complete documentation index at: https://snakysec.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
LOGGING — Journalisation de preuve numérique
Spécification du système de journalisation de la plateforme MSSP SnakySec. Objectif : produire des journaux recevables comme preuve numérique au sens du Code civil art. 1366 et du règlement eIDAS 910/2014, sans jamais exposer de secret ou d’information sensible en clair.
Deux pipelines coexistent :
| Pipeline | Rôle | Recevabilité juridique |
|---|
PlatformAuditLog (DB) | Journal d’évidence horodaté + chaîné + signé | Oui — eIDAS / Code civil / NF Z42-020 |
| Pino / stdout / Sentry | Exploitation, debug, observabilité | Non — complément opérationnel |
1. Cadre normatif
| Norme / texte | Exigence | Implémentation |
|---|
| Code civil art. 1366 | Écrit électronique admis comme preuve si l’auteur est identifiable et l’intégrité préservée | Chaîne SHA-256 + signature Ed25519 + actor typé |
| Règlement eIDAS 910/2014 art. 3(35), 32-42 | Cachet électronique avancé/qualifié, horodatage qualifié | Signature Ed25519 (avancé) — ancrage TSA RFC 3161 (qualifié, Q3 2026) |
| RGS ANSSI v2.0 | Fonction de hachage : SHA-2 min. — signature : ECDSA/EdDSA | SHA-256 (chaîne) + Ed25519 (ancrages) |
| NF Z42-020 | Coffre-fort électronique : intégrité, traçabilité, pérennité | Chaîne immuable + ancrage quotidien + rétention ≥ 6 ans |
| ISO 27037 / 27041 / 27042 | Identification, collecte, préservation et analyse de preuves numériques | Schéma v1 figé, horodatage serveur, ordre strict seq |
| NIS 2 art. 21 (d) | Journalisation sécurité | Pipeline complet couvert |
| RGPD art. 5 (1.f) · art. 32 | Intégrité et confidentialité | Scrubbing obligatoire avant écriture |
2. Politique zéro donnée sensible
Règle absolue : aucune information sensible en clair ou exposable ne doit être journalisée. Le journal de preuve sert la recevabilité juridique (imputabilité + intégrité), pas la récupération de secrets.
Denylist (appliquée par le writer, non-contournable)
Source de vérité : platform/src/lib/log-redact.ts.
- Clés matching :
*secret*, *token*, *password*, *key (sauf allowlist), *auth*, cookie, authorization, bearer, vault*, encryption_key, pem, private_key, hmac_key, nonce, refresh_token, access_token, session, credential, client_secret
- Patterns valeur : JWT, PEM block, Vault token (
hvs.*, hvb.*), GitLab PAT (glpat-*), GitHub PAT (ghp_*, ghs_*), Slack token, base64 long (≥ 64 chars sans séparateurs)
- Allowlist identifiants publics :
keyId, keyVersion, publicKeyId, sessionId, tokenId, secretId, keyPrefix
Truncation
Blob > 2 KB → [TRUNCATED]. Profondeur objet > 8 niveaux → refus. Protection anti-bombe sérialisation.
Whitelist par resourceType
Chaque type de ressource déclare un jeu de champs loggables (RESOURCE_LOGGABLE_FIELDS). Tout champ hors whitelist est supprimé avant hachage. Rationale : limiter la surface d’exposition même en cas de régression du scrubber.
Scope unifié
Le même module log-redact.ts alimente :
PlatformAuditLog (journal d’évidence)
- Pino (logs stdout) via
PINO_REDACT_PATHS
- Sentry via
beforeSend hook
Aucune divergence possible entre les trois surfaces.
Version figée schemaVersion: "1". Toute modification = nouvelle version + migration explicite.
| Groupe | Champs | Sens |
|---|
| WHO | actorType, actorId, actorDisplay, actorIp, actorUserAgent, authMethod, impersonatedBy | Imputabilité forte. authMethod précise la preuve d’authentification (sso.entra, api-key, worker.vault-approle, system) |
| WHEN | timestamp (événement métier), recordedAt (insertion DB), seq (BigInt auto-incrémenté unique) | Deux horodatages distincts — le gap révèle les anomalies. seq garantit l’ordre strict |
| WHAT | action (énuméré domain.verb), outcome (SUCCESS/FAILURE/DENIED), severity | Taxonomie stable — voir ACTION_LABELS |
| WHERE | resourceType, resourceId, resourceLabel, clientId | Identification de la cible, rattachement tenant |
| HOW | sourceService (next-app, worker:chain, worker:digest, webhook-gitlab…), requestId, sessionId, route | Contexte d’exécution |
| CHANGE | changeSummary, before, after, reason | Diff sémantique — before/after passent par la whitelist resourceType |
| DESTINATION | destinationKind, destinationRef | Qui/quoi est impacté en aval (email envoyé, pipeline déclenchée, document généré) |
| INTEGRITY | previousHash, entryHash, chainHash, preChain | Chaîne de hachage — voir §4 |
auth.login · auth.logout · auth.mfa_challenge
client.create · client.update · client.delete · client.secret.rotate
audit.trigger · audit.cancel · audit.complete
control.review · control.override
grc.document.generate · report.download
vault.read · vault.rotate
audit_log.verify · audit_log.tampering_detected · audit_log.tampering_acknowledged
Exemple — MFA challenge réussi
{
"seq": 184221,
"timestamp": "2026-04-14T09:12:04.021Z",
"recordedAt": "2026-04-14T09:12:04.087Z",
"actorType": "USER",
"actorId": "user_01HR...",
"actorDisplay": "alice@snakysec.fr",
"actorIp": "203.0.113.42",
"actorUserAgent": "Mozilla/5.0 ...",
"authMethod": "sso.entra",
"action": "auth.mfa_challenge",
"outcome": "SUCCESS",
"severity": "INFO",
"resourceType": "User",
"resourceId": "user_01HR...",
"sourceService": "next-app",
"requestId": "req_01HR...",
"route": "/api/auth/callback/entra-id",
"previousHash": "a3f9...c21b",
"entryHash": "7d4e...0089",
"chainHash": "12af...ffab",
"preChain": false
}
Exemple — Rotation de secret client
{
"action": "client.secret.rotate",
"outcome": "SUCCESS",
"severity": "WARNING",
"resourceType": "ClientSecret",
"resourceId": "cs_01HR...",
"resourceLabel": "SnakySec — Graph cert",
"clientId": "cl_01HR...",
"changeSummary": "X.509 cert rotated",
"before": { "keyId": "kid_2025", "keyVersion": 3 },
"after": { "keyId": "kid_2026", "keyVersion": 4 },
"reason": "annual rotation",
"destinationKind": "VAULT",
"destinationRef": "mssp/clients/snakysec"
}
Aucune partie privée ou empreinte exploitable du cert n’apparaît — uniquement les identifiants publics keyId / keyVersion (allowlist).
4. Chaîne d’intégrité
Canonicalisation
Tout événement est sérialisé via RFC 8785 JCS (JSON Canonicalization Scheme) avant hachage. Garantit qu’une représentation unique existe pour un contenu donné, quel que soit l’ordre de champs ou l’encodage des nombres.
Hachage
entryHash = SHA-256( JCS(payload) )
chainHash = SHA-256( previousChainHash || entryHash )
- Genèse :
genesisHash( "mssp-platform" ) (ou process.env.DEPLOYMENT_ID)
Sérialisation des écritures
Postgres advisory lock (pg_advisory_xact_lock(4815162342008813)) acquis dans la transaction d’insertion. Garantit qu’une seule écriture produit le chainHash suivant — pas de course, pas de divergence.
Ancrage quotidien Ed25519
Worker audit-chain-worker (BullMQ, cron 15 0 * * * UTC) :
- Sélectionne toutes les entrées de J-1 (
firstSeq, lastSeq)
- Calcule
rootHash = SHA-256( chainHash(firstSeq) || chainHash(lastSeq) )
- Signe le message canonique
mssp-audit-anchor:v1|date|firstSeq|lastSeq|rootHash avec la clé privée Ed25519 chargée depuis Vault (mssp/platform/log_anchor_private_key)
- Insère une ligne
LogAnchor { anchorDate, firstSeq, lastSeq, rootHash, signature, publicKeyId, algo, tsaToken? }
- Écrit un événement
audit_log.anchor dans le journal (méta-trace)
Rationale Ed25519 vs HMAC : vérification par tiers possible sans partage de clé secrète. La clé publique est publiable (futur : transparency log public, ancrage blockchain optionnel).
Futur (Q3 2026) : ancrage sur TSA RFC 3161 qualifiée (ANSSI qualification) → tsaToken, passage de “cachet avancé” à “cachet qualifié” eIDAS.
5. Vérification
Worker automatique — vérification quotidienne
audit-chain-worker, cron 10 0 * * * UTC :
- Parcourt toutes les entrées dans l’ordre
seq asc
- Re-calcule
entryHash et chainHash de chaque ligne
- Compare à la valeur stockée
- Vérifie chaque
LogAnchor avec la clé publique Ed25519
- Rupture détectée → trigger lockdown (voir §6)
Endpoint manuel
POST /api/v1/admin/audit-log/verify
Authorization: Bearer <MSSP_ADMIN session>
Réponse :
{ "ok": true, "entriesVerified": 184221, "anchorsVerified": 97 }
En cas de rupture :
{
"ok": false,
"brokenAtSeq": 182014,
"expectedHash": "7d4e...",
"actualHash": "3b2a...",
"lockdown": true
}
Runbook — vérification à froid
# Export journal
docker exec -i platform-postgres-1 psql -U mssp -d mssp \
-c "COPY (SELECT * FROM \"PlatformAuditLog\" ORDER BY seq) TO STDOUT WITH CSV HEADER" \
> audit-log-$(date +%Y%m%d).csv
# Export ancres
docker exec -i platform-postgres-1 psql -U mssp -d mssp \
-c "COPY (SELECT * FROM \"LogAnchor\" ORDER BY \"anchorDate\") TO STDOUT WITH CSV HEADER" \
> log-anchors-$(date +%Y%m%d).csv
# Clé publique (à fournir au vérifieur tiers)
vault kv get -field=log_anchor_public_key mssp/platform > mssp-log-public.pem
# Vérification : script tiers utilise canonicalize + sha256 + ed25519-verify
6. Détection et gestion de tampering
Lockdown automatique
Sur rupture détectée, le worker :
- Upsert
PlatformState { key: "audit_log_lockdown", value: { locked: true, detectedAt, brokenAtSeq, expectedHash, actualHash } }
- Le writer
recordAuditEvent() vérifie isAuditLogLocked() avant chaque écriture et bloque (AuditLogLockedError) si verrouillé
- Une alerte Sentry critique est levée (
audit_log.tampering_detected)
Effet : toutes les écritures applicatives sont stoppées — aucune action métier ne peut masquer l’incident. Les utilisateurs voient un écran d’incident explicite.
Reconnaissance (acknowledgement)
Seul un MSSP_ADMIN peut reprendre la chaîne :
POST /api/v1/admin/audit-log/acknowledge-tampering
Content-Type: application/json
{
"reason": "Justification ≥ 10 caractères — cause identifiée, corrective action engagée"
}
Transaction atomique :
- Calcul
newGenesisHash = SHA-256( "ack:<userId>:<detectedAt>:<brokenAtSeq>" )
- Signature Ed25519 du bloc ack
- Insertion
AuditLogAcknowledgement { detectedAt, brokenAtSeq, expectedHash, actualHash, reason, acknowledgedById, newGenesisHash, signature }
- Déverrouillage
PlatformState.audit_log_lockdown.locked = false
- Écriture
audit_log.tampering_acknowledged comme première entrée de la nouvelle ère
Garanties :
- Les entrées pré-rupture restent immuables (leurs
chainHash d’origine ne changent pas)
- La rupture reste documentée (ligne
AuditLogAcknowledgement signée, traçable)
- La nouvelle ère démarre depuis un genesis déterministe dérivé de l’incident (pas d’effacement)
- L’ack lui-même est scellé dans la nouvelle chaîne
7. Rétention
| Classe | Durée | Justification |
|---|
PlatformAuditLog, LogAnchor, AuditLogAcknowledgement | ≥ 6 ans | Prescription quinquennale Code civil art. 2224 + marge 1 an. Prorogation sur contentieux actif |
ControlReviewHistory | 6 ans (idem) | Preuve de diligence — revues manuelles |
| Pino stdout | 30 j rotation | Exploitation uniquement |
| Sentry | 90 j (plan SaaS) | Observabilité uniquement |
Archivage froid : dump PostgreSQL chiffré (AES-256-GCM, clé en Vault) vers stockage WORM externe (SecNumCloud ou équivalent) — automatisé trimestriel P2.
8. Operational runbook
Rotation de la clé d’ancrage Ed25519
- Générer nouvelle paire (
openssl genpkey -algorithm ed25519)
vault kv put mssp/platform log_anchor_private_key=@new.pem log_anchor_public_key=@new.pub log_anchor_key_id=kid_YYYY-MM
- Redéploiement worker —
publicKeyId utilisé par l’ancrage suivant est le nouveau
- Ne jamais supprimer l’ancienne clé publique — la vérification rétroactive en dépend. Archivage sous
mssp/platform/archive/log_anchor_public_key_<kid>
Ajout / modification d’une action
- Étendre le type
AuditAction + ACTION_LABELS dans audit-log.ts
- Revue : scope du champ
before/after conforme à la whitelist RESOURCE_LOGGABLE_FIELDS ?
- Écrire au moins un call-site avec
recordAuditEvent — jamais passer par logAction legacy
Revue à froid d’un incident (forensic)
- Snapshot DB (backup point-in-time)
- Export journal CSV (cf §5)
- Vérification chaîne complète (worker ou script tiers)
- Filtrage
action, actorId, resourceId, timestamp range
- Corrélation avec journaux externes : Traefik access logs, GitLab pipeline logs, Sentry events
- Production du rapport d’analyse signé (DOCX + PDF via générateur GRC incident procedure)
9. Références implémentation
10. Limites connues
- Ancrage TSA non-qualifié — Ed25519 signé par SnakySec suffit pour eIDAS “avancé”. Pour passer “qualifié” : intégration RFC 3161 avec TSA qualifiée ANSSI (Q3 2026)
- Pas de transparency log public — la clé publique est publiée mais aucun registre indépendant n’atteste encore des ancres. Option Rekor / Trillian à évaluer
- Horodatage serveur — basé sur NTP OS. Pour exigences renforcées : horodatage TSA systématique par entrée (coût vs bénéfice à évaluer)
- Backfill pré-chaîne — entrées antérieures au déploiement du système portent
preChain: true et sont exclues de la vérification chaîne (voir migration backfill)