Aller au contenu

PD-251 — Plan d'implémentation

1. Phase Go/No-Go

Hypothèse Vérification Résultat
MerkleTreeService (PD-237) src/modules/merkle/services/merkle-tree.service.ts GO
IntegrityVerifierService (PD-60) src/modules/upload/services/integrity-verifier.service.ts GO
TsaClientService (PD-39) src/modules/dual-validation/services/tsa-client.service.ts GO
BlockchainAnchorProcessor (PD-55) src/modules/anchor/processors/blockchain-anchor.processor.ts GO
HsmService (PD-36/37) src/modules/crypto/hsm/hsm.service.ts GO
S3Service + CRR (PD-6) src/modules/storage/s3.service.ts + src/config/storage.config.ts GO
BullMQ/Redis (PD-3/PD-21) src/modules/jobs/jobs.module.ts GO
Tables integrity Aucune table dédiée existante À CRÉER

Décision : GO — Toutes les dépendances fonctionnelles sont implémentées. Seules les tables de persistance d'intégrité sont à créer via migration TypeORM.


2. Architecture technique

2.1 Nouveau module : src/modules/integrity/

src/modules/integrity/
├── integrity.module.ts                    # Module NestJS
├── config/
│   └── integrity.config.ts               # Configuration validée (bornes contractuelles)
├── entities/
│   ├── integrity-run.entity.ts            # IntegrityRun
│   ├── integrity-check-attempt.entity.ts  # IntegrityCheckAttempt
│   ├── forensic-snapshot.entity.ts        # ForensicSnapshot
│   ├── restoration-attempt.entity.ts      # RestorationAttempt
│   ├── incident-report.entity.ts          # IncidentReport
│   ├── archive-version-link.entity.ts     # ArchiveVersionLink
│   └── integrity-journal-entry.entity.ts  # IntegrityJournalEntry (append-only)
├── enums/
│   ├── archive-integrity-state.enum.ts    # HEALTHY|SUSPECT|RESTORE_PENDING|RESTORED|CORRUPTED_CONFIRMED|CORRUPTED_ARCHIVED
│   ├── check-result.enum.ts               # OK|KO|INDETERMINATE
│   ├── check-method.enum.ts               # PRIMARY_GET|RECONNECT_GET|ALT_HEAD_GET
│   ├── run-status.enum.ts                 # SUCCESS|PARTIAL|FAILED
│   └── journal-event-type.enum.ts         # Tous les types d'événements
├── services/
│   ├── integrity-run-orchestrator.service.ts     # Orchestration d'un run complet
│   ├── archive-chain-verifier.service.ts         # Vérification 4 maillons
│   ├── double-verification.service.ts            # Double vérification avant gel
│   ├── archive-state-machine.service.ts          # Machine à états contractuelle
│   ├── forensic-snapshot.service.ts              # Phase 2 — snapshot signé HSM
│   ├── restoration.service.ts                    # Phase 3 — restauration CRR
│   ├── incident-report.service.ts                # Phase 4 — rapport JSON+PDF
│   ├── integrity-journal.service.ts              # Journal append-only signé
│   ├── priority-scorer.service.ts                # Calcul priorityScore
│   ├── integrity-config-validator.service.ts     # Validation bornes contractuelles
│   ├── integrity-metrics.service.ts              # Métriques Prometheus
│   └── integrity-notifier.service.ts             # Abstraction alerting ops + propriétaire
├── processors/
│   ├── periodic-run.processor.ts                 # Queue integrity.periodic.run
│   ├── archive-verify.processor.ts               # Queue integrity.archive.verify
│   ├── archive-restore.processor.ts              # Queue integrity.archive.restore
│   ├── incident-report.processor.ts              # Queue integrity.incident.report
│   └── reconciliation.processor.ts               # Queue integrity.reconciliation
├── controllers/
│   └── integrity.controller.ts                   # 5 endpoints API
├── dto/
│   ├── create-run.dto.ts
│   ├── run-response.dto.ts
│   ├── run-report.dto.ts
│   ├── incident-response.dto.ts
│   └── latest-report.dto.ts
├── guards/
│   └── archive-access.guard.ts                   # Blocage accès archives SUSPECT/CORRUPTED
├── interfaces/
│   ├── notifier.interface.ts                     # Contrat abstraction notifier
│   └── chain-link-result.interface.ts            # Résultat vérification par maillon
└── exceptions/
    └── integrity.exception.ts                    # Codes d'erreur métier

2.2 Modifications à d'autres modules

Module existant Modification Raison
jobs Ajouter 5 queues integrity.* dans QUEUE_NAMES Enregistrement BullMQ
documents Intégrer ArchiveAccessGuard sur routes de lecture/export (détail ci-dessous) INV-251-06 gel effectif
storage Ajouter méthode CRR Frankfurt pour restauration INV-251-08

2.2.1 Guard cross-module : routes protégées et jointure inter-schéma (COV-01)

Routes du module documents à protéger :

Route Controller Méthode Effet du guard
GET /documents/:id/download DocumentsController download() HTTP 403 si integrity_state ∈ {SUSPECT, CORRUPTED_CONFIRMED, CORRUPTED_ARCHIVED}
GET /documents/:id/export DocumentsController export() HTTP 403 idem
GET /documents/:id/content DocumentsController getContent() HTTP 403 idem
GET /documents/:id/preview DocumentsController getPreview() HTTP 403 idem

Exception investigation : Les routes GET /integrity/archives/:archiveId/metadata et GET /integrity/incidents/:archiveId du module integrity restent accessibles avec rôle INTEGRITY_INVESTIGATOR ou ADMIN (résolution SEC-01 de Gate 3).

Mécanisme de jointure inter-schéma :

Le guard ArchiveAccessGuard accède à integrity_state via une jointure cross-schéma :

// archive-access.guard.ts
@Injectable()
export class ArchiveAccessGuard implements CanActivate {
  constructor(
    @InjectRepository(Archive, 'vault_secure')
    private readonly archiveRepo: Repository<Archive>,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const archiveId = this.extractArchiveId(request);
    if (!archiveId) return true; // pas de document → pas de blocage

    // Requête cross-schéma : vault_secure.archives.integrity_state
    // La colonne integrity_state est ajoutée sur vault_secure.archives (migration §4.1)
    const archive = await this.archiveRepo.findOne({
      where: { id: archiveId },
      select: ['id', 'integrityState'],
    });

    if (!archive) return true; // archive non trouvée → laisser le controller gérer 404

    const blockedStates = ['SUSPECT', 'CORRUPTED_CONFIRMED', 'CORRUPTED_ARCHIVED'];
    if (blockedStates.includes(archive.integrityState)) {
      // Vérifier si l'utilisateur a un rôle d'investigation
      const user = request.user;
      const isInvestigator = user?.roles?.includes('INTEGRITY_INVESTIGATOR') || user?.roles?.includes('ADMIN');
      if (!isInvestigator) {
        throw new ForbiddenException('ERR_INTEG_ACCESS_BLOCKED');
      }
    }
    return true;
  }
}

Résolution de l'archiveId : Le guard extrait archiveId depuis le paramètre de route :id du module documents. La correspondance document.id → archive.id se fait via la table vault_secure.documents (FK archive_id). Si le document n'a pas d'archive associée, le guard laisse passer (pas de vérification d'intégrité).

Enregistrement : Le guard est déclaré comme APP_GUARD dans le DocumentsModule (pas globalement) pour éviter d'impacter les autres modules :

// Dans documents.module.ts — providers à ajouter
{ provide: APP_GUARD, useClass: ArchiveAccessGuard }

2.3 Flux d'un run nominal

[Scheduler BullMQ]
[periodic-run.processor]
    │ 1. Valider config (bornes)
    │ 2. Sélectionner scope (rotation/partition)
    │ 3. Calculer priorityScore pour chaque archive
    │ 4. Trier par priorité décroissante
    │ 5. Pour chaque batch de maxParallelChecks :
    │     └── Dispatcher jobs → integrity.archive.verify
    │ 6. Agréger résultats
    │ 7. Produire IntegrityRun + rapport
[archive-verify.processor]
    │ 1. Vérifier 4 maillons (SHA3, Merkle, TSA, Blockchain)
    │ 2. Si mismatch → double-verification.service
    │     └── Tentative 1 : reconnect S3
    │     └── Tentative 2 : alt endpoint HEAD+GET
    │ 3. Si confirmé → archive-state-machine.service (HEALTHY→SUSPECT)
    │ 4. Si SUSPECT → dispatcher jobs phases 2-4
    │ 5. Journal append-only chaque tentative
[Si SUSPECT détecté]
    ├── Phase 1 (synchrone dans verify) : SUSPECT + blocage accès
    ├── Phase 2 [forensic-snapshot.service]
    │     └── Hash attendu/observé, Merkle proof, TSA, blockchain
    │     └── Signature HSM obligatoire
    │     └── → RESTORE_PENDING
    ├── Phase 3 [archive-restore.processor]
    │     └── Fetch depuis CRR Frankfurt
    │     └── Recalcul hash post-restauration
    │     └── Si OK → RESTORED + ancienne version CORRUPTED_ARCHIVED
    │     └── Si KO après retries → CORRUPTED_CONFIRMED
    └── Phase 4 [incident-report.processor]
          └── JSON RFC 8785 canonicalisé + signé HSM
          └── PDF signé
          └── Notification ops + propriétaire (conditionnel)

3. Résolution des écarts résiduels Gate 3

3.1 AMB-02 — Formule priorityScore

Décision : formule linéaire pondérée, configurable.

priorityScore = w_legal * legalScore + w_age * ageScore + w_crit * criticalityScore
Critère Poids par défaut Calcul
legalScore (statut juridique) 0.40 PROBATORY_SEALED = 1.0, SIGNED = 0.8, CERTIFIED = 0.6, STANDARD = 0.3
ageScore (âge de l'archive) 0.25 min(1.0, daysSinceLastVerification / scopeRotationWindow_days)
criticalityScore (classe) 0.35 HIGH = 1.0, LOW = 0.4

Contraintes : - w_legal + w_age + w_crit = 1.0 (validé au chargement config) - Résultat clampé [0.00, 1.00] - Formule et poids loggés dans le journal du run pour auditabilité

Observable : TC-251-18 vérifie l'ordre de traitement. Le journal du run expose les scores calculés.

3.2 SEC-01 — Opérations d'investigation sur archives SUSPECT

Décision : whitelist explicite d'opérations autorisées.

Opération Autorisée sur SUSPECT ? Rôle requis
GET contenu document (lecture publique) NON — bloqué
Export document NON — bloqué
GET metadata d'archive (état, dates, hashes) OUI INTEGRITY_INVESTIGATOR ou ADMIN
GET incidents d'une archive OUI INTEGRITY_INVESTIGATOR ou ADMIN
GET forensic snapshot OUI INTEGRITY_INVESTIGATOR ou ADMIN
GET journal d'audit entries pour cette archive OUI INTEGRITY_INVESTIGATOR ou ADMIN
POST restauration manuelle OUI ADMIN uniquement

Mécanisme : ArchiveAccessGuard vérifie currentState de l'archive. Pour les archives SUSPECT ou CORRUPTED_*, seules les routes d'investigation avec le rôle approprié sont autorisées.

Observable : TC-251-07 vérifie le blocage lecture/export. Le guard est testable unitairement.

3.3 HYP-01 — Batch HSM en incident de masse

Décision : file d'attente HSM avec batching et rate limiting.

Mécanisme : 1. Les opérations de signature HSM transitent par une file interne (integrity.hsm.signatureQueue), pas d'appel direct 2. Traitement séquentiel avec maxHsmOpsPerMinute (défaut 30, configurable [10, 60]) 3. En cas de >10 snapshots simultanés : les signatures sont sérialisées, pas rejetées 4. Timeout par opération HSM : 10s (configurable [5, 30]) 5. Si HSM indisponible : les snapshots sont créés sans signature et marqués PENDING_SIGNATURE, puis un job de réconciliation signe le batch quand le HSM revient

Guard de transition SEC-01 (v2) : La transition SUSPECT → RESTORE_PENDING dans archive-state-machine.service vérifie obligatoirement que le snapshot forensique associé a signature_status = 'SIGNED'. Un snapshot PENDING_SIGNATURE bloque la restauration :

// Dans archive-state-machine.service.ts — guard de transition SUSPECT → RESTORE_PENDING
async transitionToRestorePending(archiveId: UUID): Promise<void> {
  const snapshot = await this.forensicSnapshotRepo.findOne({
    where: { archiveId },
    order: { createdAt: 'DESC' },
  });

  // Guard SEC-01 : snapshot signé obligatoire
  if (!snapshot) {
    throw new IntegrityException('ERR_INTEG_NO_SNAPSHOT', 'Forensic snapshot required before restoration');
  }
  if (snapshot.signatureStatus !== 'SIGNED' || !snapshot.hsmSignatureRef) {
    throw new IntegrityException('ERR_INTEG_UNSIGNED_SNAPSHOT',
      `Snapshot ${snapshot.snapshotId} has signature_status=${snapshot.signatureStatus} — restoration blocked until HSM signature completed`);
  }

  // Transition autorisée
  await this.transition(archiveId, 'SUSPECT', 'RESTORE_PENDING');
}

Conséquence : En cas d'indisponibilité HSM, l'archive reste en SUSPECT (bloquée en lecture) jusqu'à ce que le job de réconciliation HSM signe le snapshot. La restauration ne démarre jamais avec un snapshot non signé, conformément à INV-251-07.

Observable : La file HSM expose des métriques (queue depth, latency). TC-251-08 teste le cas nominal. NR-251-06 teste la dégradation.


4. Migration de données

4.1 Nouveau schéma : vault_integrity

-- Migration: CreateIntegritySchema
CREATE SCHEMA IF NOT EXISTS vault_integrity;

-- IntegrityRun
CREATE TABLE vault_integrity.integrity_runs (
  run_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ended_at TIMESTAMPTZ,
  scope_mode VARCHAR(20) NOT NULL, -- 'FULL' | 'PARTITIONED' | 'ROTATIVE'
  scope_size INTEGER NOT NULL,
  scope_filter JSONB,
  status VARCHAR(20) NOT NULL DEFAULT 'RUNNING', -- SUCCESS | PARTIAL | FAILED | RUNNING
  total_checked INTEGER NOT NULL DEFAULT 0,
  total_ok INTEGER NOT NULL DEFAULT 0,
  total_ko INTEGER NOT NULL DEFAULT 0,
  total_indeterminate INTEGER NOT NULL DEFAULT 0,
  total_suspect INTEGER NOT NULL DEFAULT 0,
  triggered_by VARCHAR(20) NOT NULL DEFAULT 'scheduler', -- scheduler | manual
  report_json JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- IntegrityCheckAttempt
CREATE TABLE vault_integrity.integrity_check_attempts (
  attempt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  archive_id UUID NOT NULL,
  run_id UUID NOT NULL REFERENCES vault_integrity.integrity_runs(run_id),
  attempt_no SMALLINT NOT NULL CHECK (attempt_no BETWEEN 1 AND 3),
  method VARCHAR(30) NOT NULL, -- PRIMARY_GET | RECONNECT_GET | ALT_HEAD_GET
  computed_hash_sha3 VARCHAR(128),
  expected_hash_sha3 VARCHAR(128) NOT NULL,
  result VARCHAR(20) NOT NULL, -- OK | KO | INDETERMINATE | ERROR
  chain_document VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  chain_merkle VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  chain_tsa VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  chain_blockchain VARCHAR(20) NOT NULL DEFAULT 'PENDING',
  error_reason TEXT,
  timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (archive_id, run_id, attempt_no)
);

CREATE INDEX idx_check_attempts_archive ON vault_integrity.integrity_check_attempts(archive_id);
CREATE INDEX idx_check_attempts_run ON vault_integrity.integrity_check_attempts(run_id);

-- ForensicSnapshot
CREATE TABLE vault_integrity.forensic_snapshots (
  snapshot_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  archive_id UUID NOT NULL,
  expected_hash_sha3 VARCHAR(128) NOT NULL,
  observed_hash_sha3 VARCHAR(128) NOT NULL,
  merkle_proof_ref JSONB,
  tsa_status VARCHAR(20) NOT NULL,
  blockchain_status VARCHAR(20) NOT NULL,
  hsm_signature_ref VARCHAR(256),
  signature_status VARCHAR(20) NOT NULL DEFAULT 'SIGNED', -- SIGNED | PENDING_SIGNATURE
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_forensic_archive ON vault_integrity.forensic_snapshots(archive_id);

-- RestorationAttempt
CREATE TABLE vault_integrity.restoration_attempts (
  restoration_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  archive_id UUID NOT NULL,
  source VARCHAR(30) NOT NULL DEFAULT 'CRR_FRANKFURT',
  attempt_no SMALLINT NOT NULL,
  result VARCHAR(20) NOT NULL, -- SUCCESS | FAILED | PENDING
  post_restore_hash VARCHAR(128),
  error_reason TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_restoration_archive ON vault_integrity.restoration_attempts(archive_id);

-- IncidentReport
CREATE TABLE vault_integrity.incident_reports (
  report_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  archive_id UUID NOT NULL,
  json_canonical_ref TEXT NOT NULL, -- path/URL to RFC 8785 JSON
  pdf_signed_ref TEXT NOT NULL,     -- path/URL to signed PDF
  hsm_signature_ref VARCHAR(256) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_incident_archive ON vault_integrity.incident_reports(archive_id);

-- ArchiveVersionLink
CREATE TABLE vault_integrity.archive_version_links (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  from_archive_version_id UUID NOT NULL, -- corrupted version
  to_archive_version_id UUID NOT NULL,   -- restored version
  link_hash VARCHAR(128) NOT NULL,       -- SHA3(from || to || timestamp)
  anchored_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (from_archive_version_id, to_archive_version_id)
);

-- IntegrityJournalEntry (append-only)
CREATE TABLE vault_integrity.integrity_journal_entries (
  entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  archive_id UUID NOT NULL,
  event_type VARCHAR(50) NOT NULL,
  payload JSONB NOT NULL,
  payload_digest VARCHAR(128) NOT NULL, -- SHA3 du payload canonicalisé
  signature_ref VARCHAR(256),
  timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_journal_archive ON vault_integrity.integrity_journal_entries(archive_id);
CREATE INDEX idx_journal_timestamp ON vault_integrity.integrity_journal_entries(timestamp);

-- Trigger append-only : interdire UPDATE et DELETE
CREATE OR REPLACE FUNCTION vault_integrity.prevent_modify_journal()
RETURNS TRIGGER AS $$
BEGIN
  RAISE EXCEPTION 'integrity_journal_entries is append-only: % not allowed', TG_OP;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_journal_no_update
  BEFORE UPDATE ON vault_integrity.integrity_journal_entries
  FOR EACH ROW EXECUTE FUNCTION vault_integrity.prevent_modify_journal();

CREATE TRIGGER trg_journal_no_delete
  BEFORE DELETE ON vault_integrity.integrity_journal_entries
  FOR EACH ROW EXECUTE FUNCTION vault_integrity.prevent_modify_journal();

-- Extension de la table archives existante
-- Ajouter colonne integrity_state si absente
-- ALTER TABLE vault_secure.archives ADD COLUMN IF NOT EXISTS integrity_state VARCHAR(30) NOT NULL DEFAULT 'HEALTHY';
-- ALTER TABLE vault_secure.archives ADD COLUMN IF NOT EXISTS last_integrity_check_at TIMESTAMPTZ;
-- Note: l'existence exacte de la table archives doit être vérifiée en implémentation

4.2 Rétention

La table integrity_runs a une rétention de runReportRetentionDays (défaut 3650 jours). Un job de purge (hors périmètre PD-251 sauf création du placeholder) nettoie les runs anciens.


5. Découpage en composants (tâches)

TASK-1 : Module Integrity + Config + Entities (fondation)

  • Créer integrity.module.ts avec imports nécessaires
  • Implémenter integrity.config.ts avec validation des bornes contractuelles
  • Créer les 7 entités TypeORM
  • Créer les enums
  • Créer la migration SQL
  • INV couverts : INV-251-04 (bornes), INV-251-10 (états terminaux via enum)
  • Tests : TC-251-05 (bornes config), TC-251-19 (fréquence/périmètre)
  • Fichiers : src/modules/integrity/integrity.module.ts, entities/, config/, enums/, migration

TASK-2 : Machine à états + Journal append-only (noyau)

  • Implémenter archive-state-machine.service.ts — transitions contractuelles strictes
  • Implémenter integrity-journal.service.ts — append-only signé, RFC 8785
  • INV couverts : INV-251-05, INV-251-09, INV-251-10
  • CA couverts : CA-251-08, CA-251-14, CA-251-16
  • Tests : TC-251-06 (journal), TC-251-11 (terminaux), TC-251-20 (transitions), TC-251-21 (non-suppression)
  • Fichiers : services/archive-state-machine.service.ts, services/integrity-journal.service.ts

TASK-3 : Vérification 4 maillons + Double vérification (coeur métier)

  • Implémenter archive-chain-verifier.service.ts — SHA3, Merkle, TSA, Blockchain
  • Implémenter double-verification.service.ts — reconnect S3, alt endpoint, tentatives bornées
  • Intégrer MerkleTreeService, TsaClientService, BlockchainAdapterService, S3Service
  • INV couverts : INV-251-02, INV-251-03, INV-251-04, INV-251-16
  • CA couverts : CA-251-02, CA-251-03, CA-251-04
  • Tests : TC-251-02, TC-251-03, TC-251-04, TC-251-05, TC-251-17
  • Fichiers : services/archive-chain-verifier.service.ts, services/double-verification.service.ts

TASK-4 : Snapshot forensique + Restauration (phase 2-3)

  • Implémenter forensic-snapshot.service.ts — Phase 2, signature HSM obligatoire
  • Implémenter restoration.service.ts — Phase 3, CRR Frankfurt, retries, backoff
  • Implémenter batch HSM (résolution HYP-01)
  • INV couverts : INV-251-07, INV-251-08, INV-251-17
  • CA couverts : CA-251-05, CA-251-06, CA-251-07, CA-251-11
  • Tests : TC-251-08, TC-251-09, TC-251-10, TC-251-15
  • Fichiers : services/forensic-snapshot.service.ts, services/restoration.service.ts

TASK-5 : Rapport incident + Notification (phase 4)

  • Implémenter incident-report.service.ts — JSON RFC 8785 + PDF signé HSM
  • Implémenter integrity-notifier.service.ts — abstraction ops + propriétaire
  • INV couverts : INV-251-11, INV-251-12
  • CA couverts : CA-251-09, CA-251-10, CA-251-12
  • Tests : TC-251-12, TC-251-13
  • Fichiers : services/incident-report.service.ts, services/integrity-notifier.service.ts, interfaces/notifier.interface.ts

TASK-6 : Orchestrateur de run + Priorisation + Processors BullMQ

  • Implémenter integrity-run-orchestrator.service.ts — orchestration run complet
  • Implémenter priority-scorer.service.ts — formule AMB-02 résolue
  • Implémenter les 5 processors BullMQ
  • Enregistrer les 5 queues dans JobsModule
  • INV couverts : INV-251-01, INV-251-13, INV-251-14, INV-251-15
  • CA couverts : CA-251-01, CA-251-12, CA-251-13, CA-251-16
  • Tests : TC-251-01, TC-251-14, TC-251-16, TC-251-18, TC-251-19
  • Fichiers : services/integrity-run-orchestrator.service.ts, services/priority-scorer.service.ts, processors/

TASK-7 : API Controller + Guard accès + Métriques Prometheus

  • Implémenter integrity.controller.ts — 5 endpoints
  • Implémenter archive-access.guard.ts — blocage SUSPECT/CORRUPTED (résolution SEC-01)
  • Implémenter integrity-metrics.service.ts — Prometheus counters/histograms
  • Intégrer le guard sur les routes de download/export du module documents
  • INV couverts : INV-251-06
  • CA couverts : CA-251-15
  • Tests : TC-251-07, TC-251-22
  • Fichiers : controllers/, guards/, services/integrity-metrics.service.ts

TASK-8 : Réconciliation Merkle + Tests d'intégration

8.1 Mécanisme de réconciliation Merkle (COV-02)

Problème résolu : Après un crash pré-commit ou post-commit, certaines opérations synchrones (transition d'état, journal append-only) ont réussi mais les opérations asynchrones (agrégation Merkle, ancrage blockchain) n'ont pas été déclenchées.

Détection des écarts post-crash :

Le ReconciliationProcessor est déclenché de 2 manières : 1. Au démarrage du module (onModuleInit) — rattrapage des événements orphelins au boot 2. Via la queue integrity.reconciliation — déclenché après chaque run complet ou manuellement

Algorithme de réconciliation :

1. SCAN: Lire integrity_journal_entries WHERE event_type IN (
     'STATE_TRANSITION', 'FORENSIC_SNAPSHOT_CREATED',
     'RESTORATION_COMPLETED', 'VERSION_LINK_CREATED'
   ) AND timestamp > lastReconciliationTimestamp

2. POUR CHAQUE journal_entry:
   a. Extraire archive_id et event_type du payload
   b. Vérifier si un leaf Merkle correspondant existe:
      - Appeler MerkleTreeService.getLeafByReference(archive_id, entry_id)
      - Si leaf absent → événement ORPHELIN

3. POUR CHAQUE événement ORPHELIN:
   a. Recalculer le hash SHA3 du payload canonicalisé (RFC 8785)
   b. Appeler MerkleTreeService.addLeaf(hash, { archiveId, entryId })
   c. Logger dans le journal: MERKLE_RECONCILIATION_APPLIED
   d. Si l'événement est un STATE_TRANSITION vers RESTORED:
      - Vérifier que l'ancrage blockchain a été déclenché
      - Si absent → dispatcher job vers integrity.archive.anchor

4. VÉRIFICATION D'INTÉGRITÉ POST-RÉCONCILIATION:
   a. Pour chaque leaf réconcilié, vérifier MerkleTreeService.verifyProof()
   b. Si proof invalide → log CRITICAL + alerte ops
   c. Mettre à jour lastReconciliationTimestamp

5. RAPPORT: Émettre un événement RECONCILIATION_COMPLETED avec:
   - orphans_found: nombre d'événements orphelins détectés
   - orphans_reconciled: nombre d'événements rattrapés
   - orphans_failed: nombre d'échecs de réconciliation
   - duration_ms: durée de la réconciliation

Persistance du checkpoint : lastReconciliationTimestamp est stocké dans la table vault_integrity.integrity_runs (champ last_reconciliation_at ajouté à la migration). Au boot, le processor lit le timestamp du dernier run réussi pour ne scanner que les événements postérieurs.

Gestion de concurrence : Le processor acquiert un lock Redis integrity:reconciliation:lock (TTL 5 min, SETNX simple) avant de scanner. Si le lock est déjà pris, le job est replanifié avec backoff de 30s.

  • Implémenter reconciliation.processor.ts selon l'algorithme ci-dessus
  • Tests d'intégration end-to-end (run complet)
  • Tests de non-régression (NR-251-01 à NR-251-06)
  • INV couverts : INV-251-15
  • CA couverts : CA-251-16
  • Tests : TC-251-16, NR-251-01 à NR-251-06
  • Fichiers : processors/reconciliation.processor.ts, tests e2e

5bis. Diagrammes Mermaid

5bis.1 Graphe de dépendances inter-composants

graph TD
    subgraph "Processors BullMQ"
        PRP[periodic-run.processor]
        AVP[archive-verify.processor]
        ARP[archive-restore.processor]
        IRP[incident-report.processor]
        RCP[reconciliation.processor]
    end

    subgraph "Services métier"
        IRO[integrity-run-orchestrator.service]
        ACV[archive-chain-verifier.service]
        DVS[double-verification.service]
        ASM[archive-state-machine.service]
        FSS[forensic-snapshot.service]
        RES[restoration.service]
        IRS[incident-report.service]
        IJS[integrity-journal.service]
        PSS[priority-scorer.service]
        ICV[integrity-config-validator.service]
        IMS[integrity-metrics.service]
        INS[integrity-notifier.service]
    end

    subgraph "API"
        CTL[integrity.controller]
        GRD[archive-access.guard]
    end

    subgraph "Dépendances externes (PD existants)"
        MTS[MerkleTreeService<br/>PD-237]
        TSA[TsaClientService<br/>PD-39]
        HSM[HsmService<br/>PD-36/37]
        S3S[S3Service + CRR<br/>PD-6]
        BLK[BlockchainAnchorProcessor<br/>PD-55]
    end

    PRP --> IRO
    IRO --> PSS
    IRO --> ICV
    IRO --> IMS
    PRP -->|dispatch| AVP

    AVP --> ACV
    ACV --> MTS
    ACV --> TSA
    ACV --> BLK
    ACV --> S3S
    AVP --> DVS
    DVS --> S3S
    AVP --> ASM
    AVP --> IJS

    ASM --> IJS
    ASM --> FSS

    FSS --> HSM
    FSS --> IJS

    ARP --> RES
    RES --> S3S
    RES --> ASM
    RES --> IJS

    IRP --> IRS
    IRS --> HSM
    IRP --> INS

    RCP --> MTS
    RCP --> IJS

    CTL --> IRO
    CTL --> RES
    CTL --> IRS
    GRD -.->|cross-module| CTL

    style PRP fill:#4a90d9,color:#fff
    style AVP fill:#4a90d9,color:#fff
    style ARP fill:#4a90d9,color:#fff
    style IRP fill:#4a90d9,color:#fff
    style RCP fill:#4a90d9,color:#fff
    style MTS fill:#7b8d8e,color:#fff
    style TSA fill:#7b8d8e,color:#fff
    style HSM fill:#7b8d8e,color:#fff
    style S3S fill:#7b8d8e,color:#fff
    style BLK fill:#7b8d8e,color:#fff

5bis.2 Diagramme de séquence — Run périodique avec détection de corruption

sequenceDiagram
    participant SCH as BullMQ Scheduler
    participant PRP as periodic-run.processor
    participant IRO as integrity-run-orchestrator
    participant PSS as priority-scorer
    participant AVP as archive-verify.processor
    participant ACV as archive-chain-verifier
    participant DVS as double-verification
    participant ASM as archive-state-machine
    participant IJS as integrity-journal
    participant FSS as forensic-snapshot
    participant HSM as HsmService (PD-36)
    participant S3 as S3Service (PD-6)
    participant MTS as MerkleTreeService (PD-237)
    participant TSA as TsaClientService (PD-39)
    participant BLK as BlockchainAnchor (PD-55)

    SCH->>PRP: trigger (cron)
    PRP->>IRO: startRun(config)
    IRO->>IRO: acquérir lock Redis
    IRO->>PSS: scoreArchives(scope)
    PSS-->>IRO: archives triées par priorité

    loop Pour chaque batch (maxParallelChecks)
        IRO->>AVP: dispatch job(archiveId)
        AVP->>ACV: verifyChain(archiveId)

        par Vérification 4 maillons
            ACV->>S3: getObject(archiveId)
            S3-->>ACV: contenu fichier
            ACV->>ACV: SHA3-512(contenu) vs hash attendu
            ACV->>MTS: getProofByLeaf(archiveId)
            MTS-->>ACV: merkle proof
            ACV->>TSA: verifyTimestamp(archiveId)
            TSA-->>ACV: TSA status
            ACV->>BLK: verifyAnchor(archiveId)
            BLK-->>ACV: blockchain status
        end

        ACV-->>AVP: chainResult{doc, merkle, tsa, blockchain}

        alt Mismatch détecté (hash KO)
            AVP->>DVS: doubleVerify(archiveId)
            DVS->>S3: reconnect + GET (tentative 2)
            S3-->>DVS: contenu fichier
            DVS->>DVS: SHA3-512 recompute

            alt Confirmé KO après double vérification
                DVS-->>AVP: confirmed SUSPECT
                AVP->>ASM: transition(HEALTHY → SUSPECT)
                ASM->>IJS: appendEntry(STATE_TRANSITION)
                IJS->>IJS: SHA3(payload) + sign HSM

                AVP->>FSS: createSnapshot(archiveId)
                FSS->>HSM: sign(snapshotData)
                HSM-->>FSS: signature
                FSS->>IJS: appendEntry(FORENSIC_SNAPSHOT_CREATED)
                FSS-->>AVP: snapshot signé

                Note over AVP: Dispatch phases 2-3-4 (async)
            else Faux positif transitoire
                DVS-->>AVP: resolved OK
                AVP->>IJS: appendEntry(FALSE_POSITIVE)
            end
        else Tous maillons OK
            AVP->>IJS: appendEntry(CHECK_OK)
        end

        AVP-->>IRO: checkResult
    end

    IRO->>IRO: agréger résultats
    IRO->>IRO: produire IntegrityRun + rapport
    IRO->>IRO: libérer lock Redis
    IRO-->>PRP: runReport

5bis.3 Machine à états — Transitions integrity_state

stateDiagram-v2
    [*] --> HEALTHY

    HEALTHY --> SUSPECT : double vérification confirmée<br/>(archive-state-machine)
    SUSPECT --> RESTORE_PENDING : snapshot forensique signé HSM<br/>(guard SEC-01 v2)
    RESTORE_PENDING --> RESTORED : restauration CRR réussie<br/>(restoration.service)
    RESTORE_PENDING --> SUSPECT : CRR indisponible / échec transitoire<br/>(retry ultérieur dans SLA)
    RESTORE_PENDING --> CORRUPTED_CONFIRMED : retries max épuisés<br/>(restoration.service)
    SUSPECT --> CORRUPTED_CONFIRMED : SLA dépassé sans restauration

    CORRUPTED_CONFIRMED --> [*] : état terminal — aucune transition sortante
    CORRUPTED_ARCHIVED --> [*] : état terminal — aucune transition sortante

    RESTORED --> HEALTHY : prochain run périodique OK<br/>(archive-chain-verifier)

    note right of SUSPECT : Accès bloqué (ArchiveAccessGuard)<br/>sauf INTEGRITY_INVESTIGATOR/ADMIN
    note right of CORRUPTED_CONFIRMED : Notification propriétaire<br/>+ rapport incident signé HSM
    note left of CORRUPTED_ARCHIVED : Ancienne version archivée<br/>après restauration réussie

6. Mapping INV → Mécanismes techniques

Invariant Mécanisme Service Observable
INV-251-01 BullMQ job scheduler avec getJobSchedulers() periodic-run.processor Cron job actif, logs de déclenchement
INV-251-02 archive-chain-verifier.service vérifie 4 maillons, chaque résultat stocké dans IntegrityCheckAttempt archive-chain-verifier.service 4 colonnes chain_* dans attempt, statut INDETERMINATE si absent
INV-251-03 double-verification.service : reconnect S3 + alt endpoint, min 2 tentatives avant SUSPECT double-verification.service Entries journal avec chaque tentative, transition refusée si attempts < 2
INV-251-04 Config validée au démarrage : verificationAttemptsMax ∈ [2,3], rejet hors bornes integrity-config-validator.service Exception au démarrage si hors bornes, log de validation
INV-251-05 Table integrity_journal_entries avec trigger BEFORE UPDATE/DELETE → RAISE EXCEPTION + signature HSM integrity-journal.service Trigger PostgreSQL, signature vérifiable
INV-251-06 ArchiveAccessGuard sur routes GET/export, vérifie integrity_state != SUSPECT/CORRUPTED_* archive-access.guard HTTP 403 sur accès interdit, log d'audit
INV-251-07 archive-state-machine.service : précondition snapshot.signature_status = 'SIGNED' ET snapshot.hsm_signature_ref IS NOT NULL avant transition SUSPECT→RESTORE_PENDING (SEC-01 v2) archive-state-machine.service Guard dans la machine à états, snapshot signé vérifiable
INV-251-08 restoration.service : 3 issues possibles (RESTORED, SUSPECT, CORRUPTED_CONFIRMED), aucune autre archive-state-machine.service Transitions contraintes dans la FSM
INV-251-09 archive-state-machine.service : DELETE interdit sur CORRUPTED_*, guard + trigger SQL archive-state-machine.service Tentative DELETE → exception métier ERR_INTEG_DELETE_FORBIDDEN
INV-251-10 FSM : aucune transition sortante depuis CORRUPTED_CONFIRMED/CORRUPTED_ARCHIVED archive-state-machine.service Exception TERMINAL_STATE_VIOLATION
INV-251-11 integrity-notifier.service : conditions state === CORRUPTED_CONFIRMED \|\| slaExceeded integrity-notifier.service Log de décision notify/skip, mock testable
INV-251-12 incident-report.service : JSON canonicalisé RFC 8785 via json-canonicalize + signature HSM + PDF incident-report.service Artefacts en stockage, signature vérifiable
INV-251-13 integrity-run-orchestrator.service : calcul durée run, comparaison SLA, alerte si dépassement integrity-run-orchestrator.service Métriques Prometheus, alerte ops tracée
INV-251-14 integrity-run-orchestrator.service : rapport systématique même sans anomalie integrity-run-orchestrator.service IntegrityRun.report_json toujours non-null en fin de run
INV-251-15 Transaction PostgreSQL pour les écritures synchrones, journal et Merkle en async post-commit, réconciliation reconciliation.processor Run status PARTIAL si async en retard, réconciliation loggée
INV-251-16 Tout catch produit un IntegrityRun.status = PARTIAL/FAILED, alerte ops integrity-run-orchestrator.service Aucun run sans statut, métriques Prometheus
INV-251-17 restoration.service crée ArchiveVersionLink + nouvelle archive + ancre blockchain restoration.service archive_version_links en base, ancrage vérifiable

7. Mapping CA → Mécanismes + Tests

CA Mécanisme Tâche Tests
CA-251-01 BullMQ scheduler + scope rotation TASK-6 TC-251-01, TC-251-19
CA-251-02 archive-chain-verifier.service : 4 résultats explicites TASK-3 TC-251-02, TC-251-17
CA-251-03 double-verification.service : faux positif transitoire → pas de gel TASK-3 TC-251-03
CA-251-04 double-verification.service + archive-state-machine.service : → SUSPECT + blocage TASK-3 TC-251-04, TC-251-05, TC-251-07
CA-251-05 forensic-snapshot.service : précondition snapshot signé HSM TASK-4 TC-251-08
CA-251-06 restoration.service : RESTORED + CORRUPTED_ARCHIVED + ArchiveVersionLink TASK-4 TC-251-08, TC-251-09
CA-251-07 restoration.service : retries max → CORRUPTED_CONFIRMED TASK-4 TC-251-10
CA-251-08 archive-state-machine.service : transitions interdites depuis terminaux TASK-2 TC-251-11, TC-251-20
CA-251-09 incident-report.service : JSON RFC 8785 + PDF signés TASK-5 TC-251-13
CA-251-10 integrity-notifier.service : alerte ops incident + SLA TASK-5 TC-251-14
CA-251-11 integrity-notifier.service : pas de notif propriétaire en SUSPECT/RESTORE_PENDING/RESTORED TASK-5 TC-251-15
CA-251-12 integrity-notifier.service : notif propriétaire si CORRUPTED_CONFIRMED ou > restoreSla TASK-5 TC-251-12, TC-251-18
CA-251-13 integrity-config-validator.service : bornes batch/parallel/timeout/window TASK-6 TC-251-19
CA-251-14 archive-state-machine.service + trigger SQL : suppression refusée TASK-2 TC-251-21
CA-251-15 integrity-metrics.service : 4 familles Prometheus TASK-7 TC-251-22
CA-251-16 Transaction ACID + réconciliation async + FSM stricte TASK-2, TASK-8 TC-251-11, TC-251-16, TC-251-20

8. Gestion d'erreurs

Erreur Code Réaction Statut run
S3 timeout (lecture document) ERR_INTEG_S3_TIMEOUT Retry dans double vérification, INDETERMINATE si tous KO PARTIAL
HSM indisponible ERR_INTEG_HSM_UNAVAIL Snapshot créé PENDING_SIGNATURE, job réconciliation PARTIAL
CRR Frankfurt indisponible ERR_INTEG_CRR_UNAVAIL RESTORE_PENDING → SUSPECT (retry ultérieur dans SLA) PARTIAL
Merkle proof introuvable ERR_INTEG_MERKLE_MISS chain_merkle = INDETERMINATE, alerte ops PARTIAL
TSA timeout ERR_INTEG_TSA_TIMEOUT chain_tsa = INDETERMINATE PARTIAL
Blockchain indisponible ERR_INTEG_CHAIN_DOWN chain_blockchain = INDETERMINATE PARTIAL
Config hors bornes (rejet) ERR_INTEG_CONFIG_INVALID Exception au démarrage, run non lancé FAILED
Config hors bornes (clamp) Clamp silencieux + log warning
Transition d'état invalide ERR_INTEG_FSM_VIOLATION Exception + journal, aucune modification
Suppression archive corrompue ERR_INTEG_DELETE_FORBIDDEN Exception métier + journal audit
Run timeout dépassé ERR_INTEG_RUN_TIMEOUT Arrêt graceful, statut PARTIAL, alerte ops PARTIAL
Accès archive SUSPECT ERR_INTEG_ACCESS_BLOCKED HTTP 403 + log audit

9. Contraintes techniques

9.1 Dépendances inter-PD

Story Statut Nature
PD-237 (MerkleTreeService) DONE Import getProofByLeaf()
PD-60 (IntegrityVerifierService) DONE Pattern de référence (hash verification)
PD-39 (TsaClientService) DONE Import verifyTimestamp()
PD-55 (BlockchainAnchorProcessor) DONE Pattern BullMQ processor, AnchorBatchService
PD-36/37 (HsmService) DONE Import sign(), verify()
PD-6 (CRR Frankfurt) DONE Extension S3Service (config CRR region)
PD-3/21 (BullMQ) DONE Extension QUEUE_NAMES + patterns

9.2 Framework de test

  • Runner : Jest (cohérent avec le projet existant)
  • Tests unitaires : mocks des services dépendants (HsmService, S3Service, MerkleTreeService, TsaClientService)
  • Tests d'intégration : PostgreSQL réel (via testcontainers ou DB de test), Redis réel pour BullMQ
  • Variables CI : DATABASE_URL, REDIS_URL, CI=true

9.3 Compatibilité ESM/CJS

  • Dépendances ESM-only identifiées : json-canonicalize (RFC 8785) — vérifier si ESM-only
  • Si ESM-only : utiliser import() dynamique ou wrapper CJS
  • Runner : Jest avec ts-jest (configuration existante du projet)

9.4 Dépendance npm additionnelle

  • json-canonicalize : canonicalisation RFC 8785 (si pas déjà installé)
  • pdfkit ou @react-pdf/renderer : génération PDF (vérifier existant)
  • prom-client : métriques Prometheus (vérifier existant)

10. Sécurité

10.1 Rate limiting (SEC-02 v2)

  • opsRateLimit = 60 op/min [10, 300] — appliqué sur les endpoints suivants :
  • POST /integrity/runs — déclenchement run manuel
  • POST /integrity/archives/:id/restore — restauration manuelle
  • POST /integrity/reconciliation — déclenchement réconciliation manuelle
  • Mécanisme : ThrottlerGuard NestJS avec décorateur @Throttle(opsRateLimit, 60) sur chaque endpoint ops
  • Scope : par utilisateur (IP + userId) — un admin ne peut pas saturer le système même avec un token valide
  • Refus au-delà du seuil : HTTP 429 + log audit

10.2 Canonicalisation RFC 8785

  • Toute donnée JSON signée via HSM DOIT être canonicalisée avec json-canonicalize avant signature
  • Points d'application : journal entries, incident reports, forensic snapshots
  • Learning PD-40 : ne jamais signer du JSON non canonicalisé

10.3 HSM

  • Labels de test : pv-test-integrity-* (prefixe pv-test- obligatoire)
  • Labels de production : pv-master-integrity-signing-YYYY
  • Utiliser crypto.randomUUID() pour tous les identifiants (learning PD-63)

10.4 RLS

  • Les tables vault_integrity.* n'ont pas de RLS car elles sont accessibles uniquement via le service NestJS (pas d'accès direct utilisateur)
  • Le guard applicatif (ArchiveAccessGuard) assure le contrôle d'accès

11. Hypothèses

  1. La table archives existe dans vault_secure avec un champ identifiant (UUID) utilisable comme foreign key. Si absente, un stub avec TODO tracé sera créé.
  2. Le service S3Service peut être configuré pour accéder à la région CRR Frankfurt (OVH GRA + FRA).
  3. Le module crypto/hsm expose une méthode sign(data: Buffer, keyLabel: string): Promise<Buffer> utilisable directement.
  4. prom-client est déjà installé dans le projet (utilisé par d'autres modules de monitoring).
  5. La génération PDF utilise une librairie existante du projet (sinon pdfkit sera ajouté).
  6. Les archives ont un champ owner_id permettant de résoudre le propriétaire pour notification.

12. Points de vigilance

  1. BullMQ v5 : utiliser getJobSchedulers() et removeJobScheduler(), pas les méthodes deprecated.
  2. PostgreSQL index partiels : pas de subquery dans la clause WHERE (learning PD-55).
  3. Trigger append-only : le trigger SQL sur integrity_journal_entries bloque UPDATE/DELETE mais pas les requêtes ORM — vérifier que TypeORM ne fait pas de soft-delete.
  4. Crash post-commit : le processor reconciliation doit être lancé au démarrage du module pour rattraper les événements orphelins.
  5. SLA mixité unités : stocker les SLA en millisecondes en interne, convertir à l'affichage uniquement.
  6. Concurrence runs : un seul run actif à la fois via lock Redis distribué (integrity:run:lock). Mécanisme : SETNX simple avec TTL de runTimeoutMinutes + 5min (marge de sécurité). Si le lock est déjà acquis, le nouveau run est rejeté (pas d'attente) avec statut FAILED et code ERR_INTEG_CONCURRENT_RUN. Le lock est libéré explicitement en fin de run (SUCCESS/PARTIAL/FAILED) et expire automatiquement par TTL en cas de crash.
  7. Idempotence : chaque processor BullMQ doit être idempotent (reprocesser un job ne doit pas créer de doublons).