feat(email): finaliser le pivot EML forensique
This commit is contained in:
parent
d00d149bef
commit
db9be54a1a
20 changed files with 781 additions and 249 deletions
39
README.md
39
README.md
|
|
@ -180,6 +180,37 @@ Le socle actuel comprend notamment :
|
||||||
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
||||||
avec confirmation explicite, création d'entité ou rattachement à une entité
|
avec confirmation explicite, création d'entité ou rattachement à une entité
|
||||||
existante, sans déplacement du fichier produit ;
|
existante, sans déplacement du fichier produit ;
|
||||||
|
- pivot forensique EML : contrôle SHA-256 avant analyse, lecture des en-têtes,
|
||||||
|
extraction MIME récursive, inventaire des pièces jointes et analyse locale
|
||||||
|
facultative par PDF, OCR et ExifTool ;
|
||||||
|
- propositions bancaires issues du texte ou de l'OCR, avec validation IBAN/BIC
|
||||||
|
avant toute conservation ;
|
||||||
|
- observations EML persistantes dans la fiche de preuve, sans création
|
||||||
|
automatique d'un nœud ;
|
||||||
|
- promotion facultative et explicite d'une observation vers une entité du
|
||||||
|
graphe, puis retrait réversible conservant l'observation.
|
||||||
|
- propriété persistante V13 des rattachements preuve-entité : un retrait EML
|
||||||
|
ne supprime que la source de l'observation concernée.
|
||||||
|
|
||||||
|
### Pivot EML
|
||||||
|
|
||||||
|
L'action « Analyser l'e-mail » vérifie d'abord que l'empreinte SHA-256 de la
|
||||||
|
preuve correspond à celle enregistrée. Le pipeline lit ensuite les en-têtes,
|
||||||
|
parcourt la structure MIME et présente les pièces jointes, textes PDF ou OCR,
|
||||||
|
métadonnées ExifTool et propositions bancaires disponibles.
|
||||||
|
|
||||||
|
Une proposition cochée avec « Conserver dans la fiche » devient une
|
||||||
|
observation persistante liée à la preuve. Cette confirmation normale ne crée
|
||||||
|
ni entité, ni nœud, ni rattachement `preuve_entites`. « Promouvoir en entité »
|
||||||
|
est une décision séparée, facultative et désactivée par défaut. Une promotion
|
||||||
|
peut ensuite être retirée du graphe sans supprimer l'observation ; une entité
|
||||||
|
encore utilisée par une autre observation, une autre preuve ou une relation
|
||||||
|
est conservée.
|
||||||
|
|
||||||
|
Les outils documentaires sont optionnels : leur absence produit un résultat
|
||||||
|
partiel sans empêcher la lecture des en-têtes ni l'extraction MIME. Les
|
||||||
|
métadonnées sensibles, notamment GPS, sont signalées et ne sont jamais
|
||||||
|
promues automatiquement.
|
||||||
|
|
||||||
Les outils actuellement présents dans le catalogue initial sont :
|
Les outils actuellement présents dans le catalogue initial sont :
|
||||||
|
|
||||||
|
|
@ -285,7 +316,7 @@ Les paquets AUR ne devront jamais devenir une dépendance obligatoire du futur p
|
||||||
Depuis la racine du dépôt :
|
Depuis la racine du dépôt :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make -j8
|
||||||
```
|
```
|
||||||
|
|
||||||
Le binaire produit est :
|
Le binaire produit est :
|
||||||
|
|
@ -315,15 +346,15 @@ Le projet est compilé en C17 avec les avertissements traités comme des erreurs
|
||||||
Lancer tous les tests :
|
Lancer tous les tests :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make -j8 test
|
||||||
```
|
```
|
||||||
|
|
||||||
Vérifications recommandées avant chaque commit :
|
Vérifications recommandées avant chaque commit :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make clean
|
make clean
|
||||||
make
|
make -j8
|
||||||
make test
|
make -j8 test
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,3 +212,29 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_evidence_entity_observations_semantic
|
||||||
ON evidence_entity_observations(
|
ON evidence_entity_observations(
|
||||||
evidence_id,entity_type,value_normalized,role,source_header,
|
evidence_id,entity_type,value_normalized,role,source_header,
|
||||||
occurrence,provenance_kind,COALESCE(extraction_id,''));
|
occurrence,provenance_kind,COALESCE(extraction_id,''));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS preuve_entite_sources
|
||||||
|
(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
preuve_id TEXT NOT NULL,
|
||||||
|
entite_id TEXT NOT NULL,
|
||||||
|
source_kind TEXT NOT NULL CHECK (
|
||||||
|
source_kind IN ('manual', 'legacy_manual', 'eml_observation')
|
||||||
|
),
|
||||||
|
source_uuid TEXT,
|
||||||
|
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
|
||||||
|
FOREIGN KEY (preuve_id, entite_id)
|
||||||
|
REFERENCES preuve_entites(preuve_id, entite_id) ON DELETE CASCADE,
|
||||||
|
CHECK (
|
||||||
|
(source_kind = 'eml_observation' AND source_uuid IS NOT NULL) OR
|
||||||
|
(source_kind <> 'eml_observation' AND source_uuid IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_preuve_entite_sources_unique
|
||||||
|
ON preuve_entite_sources(
|
||||||
|
preuve_id, entite_id, source_kind, COALESCE(source_uuid, '')
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_entity
|
||||||
|
ON preuve_entite_sources(entite_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_source
|
||||||
|
ON preuve_entite_sources(source_kind, source_uuid);
|
||||||
|
|
|
||||||
54
database/schema_v13.sql
Normal file
54
database/schema_v13.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* Schéma SQLite V13 — propriété des associations preuve-entité.
|
||||||
|
******************************************************************************/
|
||||||
|
CREATE TABLE preuve_entite_sources
|
||||||
|
(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
preuve_id TEXT NOT NULL,
|
||||||
|
entite_id TEXT NOT NULL,
|
||||||
|
source_kind TEXT NOT NULL CHECK (
|
||||||
|
source_kind IN ('manual', 'legacy_manual', 'eml_observation')
|
||||||
|
),
|
||||||
|
source_uuid TEXT,
|
||||||
|
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
|
||||||
|
FOREIGN KEY (preuve_id, entite_id)
|
||||||
|
REFERENCES preuve_entites(preuve_id, entite_id) ON DELETE CASCADE,
|
||||||
|
CHECK (
|
||||||
|
(source_kind = 'eml_observation' AND source_uuid IS NOT NULL) OR
|
||||||
|
(source_kind <> 'eml_observation' AND source_uuid IS NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_preuve_entite_sources_unique
|
||||||
|
ON preuve_entite_sources(
|
||||||
|
preuve_id, entite_id, source_kind, COALESCE(source_uuid, '')
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_preuve_entite_sources_entity
|
||||||
|
ON preuve_entite_sources(entite_id);
|
||||||
|
CREATE INDEX idx_preuve_entite_sources_source
|
||||||
|
ON preuve_entite_sources(source_kind, source_uuid);
|
||||||
|
|
||||||
|
/* Les rattachements antérieurs sont conservés de façon prudente. */
|
||||||
|
INSERT INTO preuve_entite_sources(
|
||||||
|
id, preuve_id, entite_id, source_kind, source_uuid, created_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-a' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||||
|
preuve_id, entite_id, 'legacy_manual', NULL,
|
||||||
|
strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||||
|
FROM preuve_entites;
|
||||||
|
|
||||||
|
/* Une promotion V12 identifiable reçoit aussi sa justification précise. */
|
||||||
|
INSERT OR IGNORE INTO preuve_entite_sources(
|
||||||
|
id, preuve_id, entite_id, source_kind, source_uuid, created_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-a' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||||
|
evidence_id, entity_id, 'eml_observation', id,
|
||||||
|
COALESCE(promoted_at, integrated_at)
|
||||||
|
FROM evidence_entity_observations
|
||||||
|
WHERE entity_id IS NOT NULL;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
> **Version :** 3.0
|
> **Version :** 3.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Schéma SQLite courant :** V10
|
> **Schéma SQLite courant :** V13
|
||||||
> **Statut :** architecture courante
|
> **Statut :** architecture courante
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -301,7 +301,7 @@ validation du chemin
|
||||||
↓
|
↓
|
||||||
création de l'arborescence
|
création de l'arborescence
|
||||||
↓
|
↓
|
||||||
initialisation transactionnelle de SQLite V10
|
initialisation transactionnelle de SQLite V13
|
||||||
↓
|
↓
|
||||||
création de l'identité de l'enquête
|
création de l'identité de l'enquête
|
||||||
↓
|
↓
|
||||||
|
|
@ -492,43 +492,168 @@ sont proches visuellement.
|
||||||
|
|
||||||
## 13. Pivot EML
|
## 13. Pivot EML
|
||||||
|
|
||||||
Statut :
|
### 13.1 Parcours
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PARTIEL
|
preuve EML
|
||||||
|
↓ contrôle d'intégrité SHA-256
|
||||||
|
tâche asynchrone
|
||||||
|
↓
|
||||||
|
analyse des en-têtes
|
||||||
|
↓
|
||||||
|
extraction MIME récursive vers un staging
|
||||||
|
↓
|
||||||
|
outils documentaires optionnels
|
||||||
|
↓
|
||||||
|
propositions temporaires
|
||||||
|
↓ confirmation explicite
|
||||||
|
observations persistantes dans la fiche
|
||||||
|
↓ promotion facultative
|
||||||
|
entités canoniques du graphe
|
||||||
```
|
```
|
||||||
|
|
||||||
La branche contient des briques d'analyse EML, d'extraction MIME, d'IBAN, OCR,
|
La preuve originale n'est jamais modifiée. Le staging et ses pièces jointes
|
||||||
ExifTool, vocabulaire contrôlé, propositions bancaires et pipeline de tâche.
|
extraites sont supprimés après rejet, annulation, erreur ou intégration.
|
||||||
|
L'absence d'ExifTool, Tesseract ou Poppler conserve un résultat partiel :
|
||||||
|
l'analyse des en-têtes et MIME reste utilisable.
|
||||||
|
|
||||||
Le flux cible est :
|
### 13.2 Responsabilités
|
||||||
|
|
||||||
```text
|
- `EmlAnalyzer` lit les en-têtes, conserve leurs occurrences et qualifie les
|
||||||
EML original
|
adresses, domaines et IP.
|
||||||
↓
|
- `EmlMimeExtractor` parcourt les parties imbriquées, y compris
|
||||||
en-têtes et MIME
|
`message/rfc822`, décode Base64, quoted-printable, RFC 2047 et RFC 2231,
|
||||||
↓
|
assainit les noms et applique les limites de profondeur, nombre et taille.
|
||||||
pièces jointes dérivées
|
- `EmlPipelineTask` orchestre en arrière-plan l'analyse, le staging, les outils
|
||||||
↓
|
documentaires et les propositions bancaires.
|
||||||
empreintes et métadonnées
|
- `DocumentToolRunner` lance les programmes avec `GSubprocess`, sans shell,
|
||||||
↓
|
draine simultanément `stdout` et `stderr`, borne les sorties et propage
|
||||||
OCR et indicateurs
|
l'annulation.
|
||||||
↓
|
- les modules ExifTool, OCR et PDF structurent les résultats sans modifier la
|
||||||
valeurs brutes + normalisées + dérivées
|
source. Le PDF privilégie le texte natif puis utilise l'OCR page par page.
|
||||||
↓
|
- `BankProposal` conserve les valeurs bancaires détectées, leur normalisation,
|
||||||
révision humaine
|
leur validation et une éventuelle correction OCR distincte.
|
||||||
↓
|
- `EmlAnalysisDialog` présente les résultats et collecte séparément la
|
||||||
intégration transactionnelle
|
conservation et la promotion.
|
||||||
```
|
- `EmlIntegration`, les DAO et `EvidenceObservation` assurent l'écriture
|
||||||
|
transactionnelle, la déduplication, la promotion et le retrait.
|
||||||
|
- `Application`, `MainWindow` et `Workspace` raccordent la tâche au contexte
|
||||||
|
GTK principal, rafraîchissent le graphe et rechargent la fiche depuis SQLite.
|
||||||
|
|
||||||
Les valeurs brutes restent immuables.
|
### 13.3 Proposition, observation et entité
|
||||||
|
|
||||||
Une IP de relais SMTP ne doit pas être présentée comme l'adresse personnelle
|
Une **proposition** est un résultat temporaire. Elle peut être rejetée,
|
||||||
d'un suspect.
|
invalidée, corrigée, conservée ou accompagnée d'une demande de promotion.
|
||||||
|
|
||||||
Une donnée bancaire observée ne prouve pas l'identité de l'auteur d'une fraude.
|
Une **observation** est une information confirmée liée à une preuve. La table
|
||||||
|
`evidence_entity_observations` conserve son UUID, son type, ses valeurs brute,
|
||||||
|
normalisée et corrigée éventuelle, son rôle, l'en-tête et son occurrence, sa
|
||||||
|
provenance, son statut, ses dates et une association facultative à une entité.
|
||||||
|
Une observation peut donc exister durablement sans nœud de graphe.
|
||||||
|
|
||||||
Le ticket Forgejo #107 reste la référence fonctionnelle du chantier.
|
Une **entité** est un objet canonique réutilisable de `entites`. Elle n'est
|
||||||
|
créée ou réutilisée que si « Promouvoir en entité » est explicitement coché.
|
||||||
|
`preuve_entites` fournit alors le rattachement nécessaire à la projection du
|
||||||
|
graphe. Une même entité peut servir plusieurs rôles, observations, preuves ou
|
||||||
|
relations.
|
||||||
|
|
||||||
|
### 13.4 MIME et outils documentaires
|
||||||
|
|
||||||
|
L'extracteur accepte un EML de 50 Mio au maximum. Il limite une partie décodée
|
||||||
|
à 8 Mio, le total décodé à 32 Mio, le message à 128 parties, la profondeur à
|
||||||
|
12 niveaux et un nom produit à 240 octets. Les chemins MIME sont conservés,
|
||||||
|
les fichiers inline et `Content-ID` sont inventoriés, les traversées de chemin
|
||||||
|
sont neutralisées et les écritures passent par un temporaire renommé après
|
||||||
|
succès. Cette prise en charge volontairement bornée ne prétend pas couvrir
|
||||||
|
l'intégralité des RFC MIME.
|
||||||
|
|
||||||
|
Une analyse documentaire accepte au maximum 50 Mio, 8 Mio de `stdout`,
|
||||||
|
256 Kio de `stderr`, 100 pages PDF et 128 documents par pipeline. Un PDF
|
||||||
|
chiffré n'est pas contourné. `pdfinfo` inspecte le document, `pdftotext`
|
||||||
|
fournit en priorité le texte natif et `pdftoppm` rend les pages nécessitant un
|
||||||
|
OCR. L'ordre des pages, les résultats partiels et la méthode utilisée sont
|
||||||
|
conservés ; les images temporaires sont nettoyées.
|
||||||
|
|
||||||
|
Tesseract reçoit `fra+eng` dans le pipeline. Son texte brut n'est pas corrigé
|
||||||
|
silencieusement : une correction OCR proposée reste distincte. Les arguments,
|
||||||
|
la version et l'état de l'exécution documentent la provenance.
|
||||||
|
|
||||||
|
ExifTool est appelé en sortie JSON avec les groupes de tags. Les champs connus
|
||||||
|
sont normalisés et les tags inconnus sont conservés avec leur groupe, leur nom
|
||||||
|
et leur valeur brute. La version de l'outil est attachée à l'exécution. Les
|
||||||
|
coordonnées GPS sont marquées sensibles et ne créent jamais automatiquement
|
||||||
|
une entité.
|
||||||
|
|
||||||
|
Les propositions bancaires peuvent contenir IBAN brut et normalisé,
|
||||||
|
validation MOD-97, BIC, banque, titulaire déclaré, adresse, éléments de RIB et
|
||||||
|
correction OCR distincte. Une donnée invalide n'est pas intégrable. Un
|
||||||
|
titulaire déclaré dans un document n'établit ni identité certaine ni
|
||||||
|
attribution pénale : la donnée reste une proposition puis une observation
|
||||||
|
tant que l'enquêteur ne choisit pas de la promouvoir.
|
||||||
|
|
||||||
|
### 13.5 Migrations V11, V12 et V13
|
||||||
|
|
||||||
|
V11 crée le premier modèle `evidence_entity_observations`, où chaque
|
||||||
|
observation est obligatoirement liée à une entité.
|
||||||
|
|
||||||
|
V12 donne un UUID propre à l'observation, rend `entity_id` nullable, ajoute les
|
||||||
|
valeurs corrigées, l'extraction, les avertissements, les dates d'observation,
|
||||||
|
d'intégration et de promotion ainsi que `promotion_kind`. Les lignes V11 sont
|
||||||
|
reprises avec `promotion_kind = 'legacy'`. L'index sémantique assure la
|
||||||
|
déduplication, y compris lorsque `extraction_id` est nul.
|
||||||
|
|
||||||
|
V13 ajoute `preuve_entite_sources`. Chaque rattachement matérialisé possède
|
||||||
|
une justification `manual`, `legacy_manual` ou `eml_observation`. La migration
|
||||||
|
protège les lignes historiques par `legacy_manual` et reprend les promotions
|
||||||
|
V12 identifiables.
|
||||||
|
|
||||||
|
### 13.6 Promotion et retrait
|
||||||
|
|
||||||
|
La conservation seule écrit l'observation et l'affiche dans la fiche, sans
|
||||||
|
créer `entites` ni `preuve_entites`. La promotion explicite crée ou réutilise
|
||||||
|
une entité, l'associe à l'observation et ajoute le rattachement nécessaire.
|
||||||
|
L'opération est transactionnelle et idempotente.
|
||||||
|
|
||||||
|
« Retirer du graphe » efface uniquement la source `eml_observation` portant
|
||||||
|
l'UUID de l'observation et conserve
|
||||||
|
l'observation. Le nœud n'est supprimé que si les références connues
|
||||||
|
(observations, preuves, relations, tags, recherches, chronologie, hypothèses,
|
||||||
|
OSINT, comptes sociaux et rôles de personne) sont absentes.
|
||||||
|
|
||||||
|
### 13.7 Qualification et provenance
|
||||||
|
|
||||||
|
Les rôles couvrent `From`, `Sender`, `Reply-To`, `Return-Path`, `To`, `Cc`,
|
||||||
|
`Bcc`, les relais `Received` et le domaine de `Message-ID`. Une même adresse
|
||||||
|
peut conserver plusieurs rôles sans multiplier l'entité canonique.
|
||||||
|
`192.0.2.10` et `198.51.100.20` sont des IP, jamais des domaines ;
|
||||||
|
`MIME-Version: 1.0` ne produit pas de domaine intégrable.
|
||||||
|
|
||||||
|
### 13.8 Asynchronisme et sécurité
|
||||||
|
|
||||||
|
`BackgroundTask`, `TaskManager` et `GCancellable` portent l'état, la
|
||||||
|
progression, l'annulation et la remise du résultat au contexte principal.
|
||||||
|
Le worker ne manipule aucun widget. Avant de présenter le résultat,
|
||||||
|
`Application` vérifie que la session attendue est toujours active ; un
|
||||||
|
changement d'enquête rend le résultat caduc et déclenche le nettoyage.
|
||||||
|
|
||||||
|
Tous les arguments externes sont séparés, sans shell. `stdout` et `stderr`
|
||||||
|
sont drainés en parallèle pour éviter un interblocage. Les processus sont
|
||||||
|
forcés à terminer lors d'une annulation, puis les répertoires temporaires sont
|
||||||
|
supprimés.
|
||||||
|
|
||||||
|
### 13.9 Limites connues
|
||||||
|
|
||||||
|
- la promotion est disponible dans le dialogue d'analyse, pas directement
|
||||||
|
depuis la fiche ;
|
||||||
|
- la fiche affiche la valeur canonique et les codes persistés, mais pas encore
|
||||||
|
la valeur brute distincte ni l'UUID de l'entité associée ;
|
||||||
|
- le runner borne les sorties et gère l'annulation, mais ne possède pas de
|
||||||
|
délai maximal autonome ;
|
||||||
|
- les pièces jointes analysées dans le staging ne sont pas persistées comme
|
||||||
|
dérivés confirmés par ce parcours ;
|
||||||
|
- les rattachements antérieurs à V13 restent volontairement protégés par
|
||||||
|
`legacy_manual`, faute de provenance historique plus précise ;
|
||||||
|
- la couverture visuelle du dialogue et de la fiche reste manuelle.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Conventions de développement
|
# Conventions de développement
|
||||||
|
|
||||||
> **Version :** 2.0
|
> **Version :** 2.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Projet :** Labfy Investigation
|
> **Projet :** Labfy Investigation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -265,7 +265,7 @@ présentation graphique.
|
||||||
La version courante du schéma est :
|
La version courante du schéma est :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
V10
|
V13
|
||||||
```
|
```
|
||||||
|
|
||||||
Les scripts versionnés sont conservés dans :
|
Les scripts versionnés sont conservés dans :
|
||||||
|
|
@ -273,7 +273,7 @@ Les scripts versionnés sont conservés dans :
|
||||||
```text
|
```text
|
||||||
database/schema_v1.sql
|
database/schema_v1.sql
|
||||||
...
|
...
|
||||||
database/schema_v10.sql
|
database/schema_v12.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
Le complément idempotent du schéma courant est :
|
Le complément idempotent du schéma courant est :
|
||||||
|
|
@ -614,3 +614,35 @@ HISTORIQUE
|
||||||
|
|
||||||
La priorité reste la qualité, la traçabilité et la compréhension durable du
|
La priorité reste la qualité, la traçabilité et la compréhension durable du
|
||||||
projet.
|
projet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Propositions, observations et entités
|
||||||
|
|
||||||
|
Ces objets ne sont pas interchangeables :
|
||||||
|
|
||||||
|
- une proposition est temporaire et ne constitue pas un fait persistant ;
|
||||||
|
- une observation confirmée décrit ce qui a été relevé dans une preuve ;
|
||||||
|
- une entité est un objet canonique du graphe, créé ou réutilisé uniquement
|
||||||
|
après une promotion explicite.
|
||||||
|
|
||||||
|
Une conservation normale ne crée jamais automatiquement d'entité, de nœud ou
|
||||||
|
de rattachement `preuve_entites`. La promotion est facultative et désactivée
|
||||||
|
par défaut.
|
||||||
|
|
||||||
|
Une observation conserve obligatoirement une provenance et un rôle
|
||||||
|
compréhensible dans son contexte. Les codes issus d'un vocabulaire contrôlé
|
||||||
|
sont utilisés à la place de chaînes libres. Valeur brute, valeur normalisée et
|
||||||
|
correction utilisateur restent dans des champs distincts ; aucune correction
|
||||||
|
ne réécrit silencieusement la valeur observée.
|
||||||
|
|
||||||
|
Le retrait d'une promotion conserve l'observation et retire exclusivement la
|
||||||
|
source `eml_observation` identifiée par son UUID. Une entité partagée avec
|
||||||
|
d'autres observations, preuves ou relations ne doit jamais être supprimée
|
||||||
|
aveuglément. Le service doit également préserver tout rattachement indépendant
|
||||||
|
de la promotion. Les sources `manual` et `legacy_manual` persistent
|
||||||
|
indépendamment et protègent le rattachement matérialisé.
|
||||||
|
|
||||||
|
Ces règles s'ajoutent aux interdictions générales : aucune modification d'une
|
||||||
|
preuve originale, aucun shell dynamique et aucun accès SQLite direct depuis
|
||||||
|
un widget GTK.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
Ce fichier recense les outils utilisés par LabFy Investigation et leur
|
Ce fichier recense les outils utilisés par LabFy Investigation et leur
|
||||||
installation. Il sera complété à chaque intégration d’un nouvel outil.
|
installation. Il sera complété à chaque intégration d’un nouvel outil.
|
||||||
|
|
||||||
|
La migration SQLite V13 et la gestion des propriétaires de
|
||||||
|
`preuve_entites` n'ajoutent aucune dépendance système.
|
||||||
|
|
||||||
## Principe d’installation
|
## Principe d’installation
|
||||||
|
|
||||||
Les dépendances système sont installées avec le gestionnaire de paquets de la
|
Les dépendances système sont installées avec le gestionnaire de paquets de la
|
||||||
|
|
@ -27,8 +30,9 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
|
||||||
|
|
||||||
| Fonction | Exécutable | Arch Linux (pacman) | Ubuntu/Debian (apt) |
|
| Fonction | Exécutable | Arch Linux (pacman) | Ubuntu/Debian (apt) |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| OCR | `tesseract` | `sudo pacman -S tesseract tesseract-data-fra` | `sudo apt install tesseract-ocr tesseract-ocr-fra` |
|
| OCR français et anglais | `tesseract` | `sudo pacman -S tesseract tesseract-data-fra tesseract-data-eng` | `sudo apt install tesseract-ocr tesseract-ocr-fra tesseract-ocr-eng` |
|
||||||
| Métadonnées | `exiftool` | `sudo pacman -S perl-image-exiftool` | `sudo apt install libimage-exiftool-perl` |
|
| Métadonnées | `exiftool` | `sudo pacman -S perl-image-exiftool` | `sudo apt install libimage-exiftool-perl` |
|
||||||
|
| Inspection, texte et rendu PDF | `pdfinfo`, `pdftotext`, `pdftoppm` | `sudo pacman -S poppler` | `sudo apt install poppler-utils` |
|
||||||
| Recherche DNS | `dig` | `sudo pacman -S bind` | `sudo apt install dnsutils` |
|
| Recherche DNS | `dig` | `sudo pacman -S bind` | `sudo apt install dnsutils` |
|
||||||
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
|
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
|
||||||
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
|
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
|
||||||
|
|
@ -40,7 +44,7 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
|
||||||
Après installation, vérifier les outils avec :
|
Après installation, vérifier les outils avec :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
command -v tesseract exiftool dig host whois curl openssl qpdf john pdf2john
|
command -v tesseract exiftool pdfinfo pdftotext pdftoppm dig host whois curl openssl qpdf john pdf2john
|
||||||
```
|
```
|
||||||
|
|
||||||
## Outils OSINT Python
|
## Outils OSINT Python
|
||||||
|
|
@ -83,8 +87,9 @@ vérifiée avec `command -v`, puis avec son option de version (`--version` ou
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Ne pas installer ces outils dans le dépôt Git.
|
- Ne pas installer ces outils dans le dépôt Git.
|
||||||
|
- ExifTool, Tesseract et Poppler sont optionnels pour le pivot EML. Leur
|
||||||
|
absence laisse disponibles l'analyse des en-têtes et l'extraction MIME.
|
||||||
- Ne jamais stocker de jetons, mots de passe ou données privées dans ce
|
- Ne jamais stocker de jetons, mots de passe ou données privées dans ce
|
||||||
fichier.
|
fichier.
|
||||||
- Les versions peuvent varier selon la distribution ; noter ici toute
|
- Les versions peuvent varier selon la distribution ; noter ici toute
|
||||||
commande particulière nécessaire à Ubuntu des forces de l’ordre.
|
commande particulière nécessaire à Ubuntu des forces de l’ordre.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Guide de développement
|
# Guide de développement
|
||||||
|
|
||||||
> **Version :** 2.0
|
> **Version :** 2.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Projet :** Labfy Investigation
|
> **Projet :** Labfy Investigation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -86,6 +86,22 @@ système de build ne les impose pas.
|
||||||
Les outils OSINT, OCR, métadonnées ou PDF sont documentés séparément et restent
|
Les outils OSINT, OCR, métadonnées ou PDF sont documentés séparément et restent
|
||||||
optionnels sauf décision explicite.
|
optionnels sauf décision explicite.
|
||||||
|
|
||||||
|
### 3.1 Dépendances documentaires optionnelles
|
||||||
|
|
||||||
|
Le pivot EML appelle directement, lorsqu'ils sont installés :
|
||||||
|
|
||||||
|
- `exiftool` pour les métadonnées JSON ;
|
||||||
|
- `tesseract` pour l'OCR, avec la demande `fra+eng` dans le pipeline ;
|
||||||
|
- `pdfinfo`, `pdftotext` et `pdftoppm` fournis par Poppler pour l'inspection,
|
||||||
|
le texte natif et le rendu des pages PDF.
|
||||||
|
|
||||||
|
Sous Debian ou Ubuntu, les paquets usuels déjà identifiés par le projet sont
|
||||||
|
`libimage-exiftool-perl`, `tesseract-ocr`, `tesseract-ocr-fra`,
|
||||||
|
`tesseract-ocr-eng` et `poppler-utils`. Ils ne sont pas nécessaires à la
|
||||||
|
compilation ni au démarrage. Un exécutable absent produit un état
|
||||||
|
« indisponible » ou un résultat partiel ; les en-têtes et MIME restent
|
||||||
|
consultables. L'application ne les installe jamais automatiquement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Récupération du dépôt
|
## 4. Récupération du dépôt
|
||||||
|
|
@ -195,6 +211,42 @@ make tests/test_eml_pipeline_task
|
||||||
./tests/test_eml_pipeline_task
|
./tests/test_eml_pipeline_task
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Tests ciblés du pivot EML :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make -j8 \
|
||||||
|
tests/test_eml_analyzer \
|
||||||
|
tests/test_eml_mime_extractor \
|
||||||
|
tests/test_eml_pipeline_task \
|
||||||
|
tests/test_document_tool_runner \
|
||||||
|
tests/test_exiftool_analysis \
|
||||||
|
tests/test_ocr_analysis \
|
||||||
|
tests/test_pdf_analysis \
|
||||||
|
tests/test_bank_proposal \
|
||||||
|
tests/test_eml_integration \
|
||||||
|
tests/test_evidence_entity_dao \
|
||||||
|
tests/test_database
|
||||||
|
|
||||||
|
./tests/test_eml_analyzer
|
||||||
|
./tests/test_eml_mime_extractor
|
||||||
|
./tests/test_eml_pipeline_task
|
||||||
|
./tests/test_document_tool_runner
|
||||||
|
./tests/test_exiftool_analysis
|
||||||
|
./tests/test_ocr_analysis
|
||||||
|
./tests/test_pdf_analysis
|
||||||
|
./tests/test_bank_proposal
|
||||||
|
./tests/test_eml_integration
|
||||||
|
./tests/test_evidence_entity_dao
|
||||||
|
./tests/test_database
|
||||||
|
```
|
||||||
|
|
||||||
|
La fixture manuelle est
|
||||||
|
`tests/fixtures/eml/manual_smoke_test.eml`. Elle est exclusivement
|
||||||
|
synthétique. Pour la validation GTK, créer une enquête neuve dans un
|
||||||
|
répertoire temporaire choisi pour le test, importer cette fixture, puis suivre
|
||||||
|
`docs/testing/EML_PIVOT_MANUAL_TEST.md`. Ne jamais réutiliser une base ou une
|
||||||
|
preuve réelle.
|
||||||
|
|
||||||
Le nom exact d'une cible doit être vérifié dans le Makefile.
|
Le nom exact d'une cible doit être vérifié dans le Makefile.
|
||||||
|
|
||||||
### 6.3 Validation avant intégration
|
### 6.3 Validation avant intégration
|
||||||
|
|
@ -345,7 +397,7 @@ Lire :
|
||||||
docs/database/DATABASE_ARCHITECTURE.md
|
docs/database/DATABASE_ARCHITECTURE.md
|
||||||
docs/database/SCHEMA_AUDIT_CURRENT.md
|
docs/database/SCHEMA_AUDIT_CURRENT.md
|
||||||
database/schema_current.sql
|
database/schema_current.sql
|
||||||
database/schema_v10.sql
|
database/schema_v12.sql
|
||||||
src/database/database.c
|
src/database/database.c
|
||||||
src/database/schema.c
|
src/database/schema.c
|
||||||
tests/test_database.c
|
tests/test_database.c
|
||||||
|
|
@ -353,21 +405,24 @@ tests/test_database.c
|
||||||
|
|
||||||
### 9.2 Nouvelle version de schéma
|
### 9.2 Nouvelle version de schéma
|
||||||
|
|
||||||
Pour créer V11, par exemple :
|
Pour créer une nouvelle version après V13, par exemple V14 :
|
||||||
|
|
||||||
1. ajouter `database/schema_v11.sql` ;
|
1. ajouter `database/schema_v13.sql` ;
|
||||||
2. déclarer et implémenter `schema_install_v11()` ;
|
2. déclarer et implémenter `schema_install_v13()` ;
|
||||||
3. ajouter `database_migrate_v10_to_v11()` ;
|
3. ajouter `database_migrate_v12_to_v13()` ;
|
||||||
4. raccorder la migration dans la boucle vers la version courante ;
|
4. raccorder la migration dans la boucle vers la version courante ;
|
||||||
5. mettre à jour les constantes de version ;
|
5. mettre à jour les constantes de version ;
|
||||||
6. installer V11 lors de la création d'une base neuve ;
|
6. installer V13 lors de la création d'une base neuve ;
|
||||||
7. adapter `schema_current.sql` si nécessaire ;
|
7. adapter `schema_current.sql` si nécessaire ;
|
||||||
8. ajouter une fixture V10 vers V11 ;
|
8. ajouter une fixture V13 vers V14 ;
|
||||||
|
|
||||||
|
La fixture V12 vers V13 vérifie le backfill `legacy_manual`. Les tests EML
|
||||||
|
couvrent aussi le retrait isolé d'une source face à un rattachement manuel.
|
||||||
9. tester une base neuve ;
|
9. tester une base neuve ;
|
||||||
10. tester le rollback ;
|
10. tester le rollback ;
|
||||||
11. vérifier l'intégrité et les clés étrangères ;
|
11. vérifier l'intégrité et les clés étrangères ;
|
||||||
12. mettre à jour l'audit courant ;
|
12. mettre à jour l'audit courant ;
|
||||||
13. créer l'audit versionné de V11.
|
13. créer l'audit versionné de V13.
|
||||||
|
|
||||||
Ne pas réécrire une ancienne migration publiée pour changer sa signification.
|
Ne pas réécrire une ancienne migration publiée pour changer sa signification.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **État du projet :** développement actif
|
> **État du projet :** développement actif
|
||||||
> **Schéma SQLite courant :** V10
|
> **Schéma SQLite courant :** V13
|
||||||
> **Usage opérationnel :** non prêt pour la production
|
> **Usage opérationnel :** non prêt pour la production
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -116,7 +116,7 @@ La branche `main` contient notamment :
|
||||||
|
|
||||||
- création, validation et ouverture d'enquêtes ;
|
- création, validation et ouverture d'enquêtes ;
|
||||||
- session d'enquête remplaçable proprement ;
|
- session d'enquête remplaçable proprement ;
|
||||||
- infrastructure SQLite et migrations jusqu'à V10 ;
|
- infrastructure SQLite et migrations jusqu'à V13 ;
|
||||||
- couche Database, DAO et services métier ;
|
- couche Database, DAO et services métier ;
|
||||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||||
- vérification d'intégrité et reclassement des preuves ;
|
- vérification d'intégrité et reclassement des preuves ;
|
||||||
|
|
@ -135,7 +135,8 @@ La branche `main` contient notamment :
|
||||||
- types canoniques de relations ;
|
- types canoniques de relations ;
|
||||||
- vocabulaire contrôlé ;
|
- vocabulaire contrôlé ;
|
||||||
- table V10 `bank_account_entities` ;
|
- table V10 `bank_account_entities` ;
|
||||||
- premiers composants du pipeline EML.
|
- pivot EML raccordé à GTK, observations persistantes indépendantes des
|
||||||
|
entités, promotion facultative et retrait réversible.
|
||||||
|
|
||||||
Cette liste est une synthèse et non un contrat de stabilité.
|
Cette liste est une synthèse et non un contrat de stabilité.
|
||||||
|
|
||||||
|
|
@ -145,15 +146,13 @@ Cette liste est une synthèse et non un contrat de stabilité.
|
||||||
|
|
||||||
### Ticket #107 — Pivot e-mail forensique
|
### Ticket #107 — Pivot e-mail forensique
|
||||||
|
|
||||||
Statut :
|
Statut technique :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PARTIEL
|
IMPLÉMENTÉ — VALIDATION DOCUMENTAIRE ET CORRECTION BLOQUANTE RESTANTES
|
||||||
```
|
```
|
||||||
|
|
||||||
Les briques préparatoires existent, mais le flux complet reste à terminer.
|
Le parcours livré est :
|
||||||
|
|
||||||
Objectif principal :
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
preuve EML
|
preuve EML
|
||||||
|
|
@ -171,30 +170,29 @@ OCR et détection d'indicateurs
|
||||||
normalisation sans perte de la valeur brute
|
normalisation sans perte de la valeur brute
|
||||||
↓
|
↓
|
||||||
interface de révision
|
interface de révision
|
||||||
↓
|
↓ conservation explicite
|
||||||
confirmation explicite
|
observation persistante dans la fiche
|
||||||
↓
|
↓ promotion facultative
|
||||||
intégration transactionnelle
|
entité créée ou réutilisée dans le graphe
|
||||||
↓
|
|
||||||
rafraîchissement des vues et du graphe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Priorités immédiates :
|
Les tests automatiques EML, MIME, outils documentaires, PDF, OCR, ExifTool,
|
||||||
|
banque, intégration et migration sont présents. Le parcours GTK dispose d'une
|
||||||
|
fixture et d'un guide manuel ; la couverture visuelle demeure manuelle.
|
||||||
|
|
||||||
1. terminer l'extraction MIME robuste ;
|
V13 distingue désormais chaque propriétaire du rattachement
|
||||||
2. conserver les pièces jointes comme fichiers dérivés ;
|
`preuve_entites`. Un retrait EML enlève sa seule source
|
||||||
3. réunir EML, OCR, IBAN et ExifTool dans un pipeline unique ;
|
`eml_observation`; les sources manuelles, historiques et celles des autres
|
||||||
4. terminer l'interface de révision ;
|
observations restent protégées.
|
||||||
5. persister uniquement les propositions confirmées ;
|
|
||||||
6. réutiliser ou créer les entités correspondantes ;
|
|
||||||
7. créer les relations canoniques avec leur provenance ;
|
|
||||||
8. garantir un rollback complet en cas d'échec ;
|
|
||||||
9. rafraîchir la barre latérale et le graphe ;
|
|
||||||
10. compléter les tests de migration et d'intégration ;
|
|
||||||
11. mettre à jour la documentation avec l'état réellement livré.
|
|
||||||
|
|
||||||
Le ticket reste ouvert tant que son flux complet et ses critères d'acceptation
|
Améliorations suivantes :
|
||||||
ne sont pas validés.
|
|
||||||
|
1. proposer la promotion directement depuis la fiche ;
|
||||||
|
2. afficher les libellés contrôlés, la valeur brute distincte et l'UUID de
|
||||||
|
l'entité associée ;
|
||||||
|
3. persister explicitement les dérivés confirmés du staging ;
|
||||||
|
4. ajouter un délai maximal autonome aux outils documentaires ;
|
||||||
|
5. automatiser davantage le parcours GTK.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,61 @@
|
||||||
# Test manuel du parcours EML
|
# Test manuel du parcours EML
|
||||||
|
|
||||||
Ce test emploie uniquement la fixture synthétique
|
Ce parcours complète les tests automatiques. Il utilise exclusivement la
|
||||||
`tests/fixtures/eml/manual_smoke_test.eml`. Il ne nécessite aucune donnée
|
fixture synthétique `tests/fixtures/eml/manual_smoke_test.eml`, dans une
|
||||||
réelle.
|
enquête synthétique neuve. Toute donnée ou base réelle est interdite.
|
||||||
|
|
||||||
## Parcours synthétique
|
## Préparation et tests automatiques
|
||||||
|
|
||||||
1. Compiler avec `make -j8`.
|
```sh
|
||||||
2. Créer une enquête synthétique neuve dans un répertoire de test.
|
make -j8
|
||||||
3. Importer `tests/fixtures/eml/manual_smoke_test.eml` comme preuve.
|
make -j8 test
|
||||||
4. Sélectionner cette preuve dans la barre latérale.
|
```
|
||||||
5. Vérifier que l’action compacte « Analyser l’e-mail » devient active.
|
|
||||||
6. Lancer l’action et observer « Analyse complète de l’e-mail » dans le
|
|
||||||
panneau d’activité.
|
|
||||||
7. Pendant un premier essai, annuler la tâche depuis ce panneau et vérifier
|
|
||||||
qu’aucun dialogue de succès ni aucune nouvelle entité n’apparaît.
|
|
||||||
8. Relancer l’analyse et vérifier dans le dialogue les en-têtes, les deux
|
|
||||||
pièces jointes, les textes, les métadonnées et les avertissements relatifs
|
|
||||||
aux outils optionnels éventuellement absents.
|
|
||||||
9. Vérifier que l’IBAN commençant par `FR00` est indiqué comme donnée de
|
|
||||||
démonstration invalide et n’est pas proposé à l’intégration.
|
|
||||||
10. Vérifier que les valeurs sont sélectionnables et copiables.
|
|
||||||
11. Fermer avec « Rejeter et fermer » et confirmer l’absence de nouvelle
|
|
||||||
entité, relation ou rattachement.
|
|
||||||
12. Relancer et vérifier que chaque proposition affiche deux actions
|
|
||||||
distinctes : « Conserver dans la fiche » et « Promouvoir en entité ».
|
|
||||||
La promotion doit être décochée et désactivée tant que la conservation
|
|
||||||
n’est pas cochée.
|
|
||||||
13. Vérifier que chaque proposition affiche son rôle et son
|
|
||||||
origine (`From`, `To`, `Message-ID` ou `Received #n`). Vérifier
|
|
||||||
explicitement que `192.0.2.10`, `198.51.100.20` sont des IP et que
|
|
||||||
`1.0` n’est jamais proposé comme domaine.
|
|
||||||
14. Cocher uniquement « Conserver dans la fiche » pour quelques adresses,
|
|
||||||
domaines et relais synthétiques, puis
|
|
||||||
choisir « Intégrer les éléments sélectionnés ».
|
|
||||||
15. Vérifier que le bilan annonce zéro promotion, qu’aucun nœud ni lien
|
|
||||||
`preuve_entites` n’est créé et que la
|
|
||||||
fiche de la preuve reste sélectionnée. La section « Entités observées
|
|
||||||
dans cette preuve » doit afficher valeur canonique, type, rôle,
|
|
||||||
en-tête/occurrence, provenance et « graphe : non ajoutée ».
|
|
||||||
16. Fermer puis rouvrir l’enquête synthétique et vérifier que les
|
|
||||||
observations, rôles et origines sont toujours présents sans nœud.
|
|
||||||
17. Relancer l’analyse, cocher la conservation et « Promouvoir en entité »
|
|
||||||
pour une seule observation, confirmer et vérifier qu’un seul nœud
|
|
||||||
apparaît et que la fiche indique la promotion.
|
|
||||||
18. Répéter la promotion et vérifier qu’aucune observation ni entité n’est
|
|
||||||
dupliquée.
|
|
||||||
19. Dans « Observations extraites », utiliser « Retirer du graphe » sur
|
|
||||||
l’observation promue et confirmer. Vérifier que l’observation, son rôle
|
|
||||||
et sa provenance restent affichés avec « Graphe : Non ajoutée ».
|
|
||||||
20. Vérifier que le nœud disparaît lorsqu’il n’a aucune autre référence.
|
|
||||||
21. Promouvoir deux observations de rôles différents vers la même valeur
|
|
||||||
canonique, puis n’en retirer qu’une. Vérifier que l’entité partagée et
|
|
||||||
l’autre observation restent présentes.
|
|
||||||
22. Fermer puis rouvrir l’enquête synthétique et vérifier que tous les états
|
|
||||||
de promotion et de retrait persistent.
|
|
||||||
23. Recalculer l’intégrité de la preuve et vérifier que son SHA-256 est
|
|
||||||
inchangé.
|
|
||||||
|
|
||||||
Noter séparément le comportement lorsque ExifTool, Tesseract ou les outils
|
Un échec de compilation ou de test est bloquant. Créer ensuite une enquête
|
||||||
Poppler ne sont pas installés : les en-têtes et l’extraction MIME doivent
|
neuve dans un répertoire temporaire dédié, importer la fixture et vérifier son
|
||||||
rester consultables.
|
intégrité depuis la fiche. L'empreinte doit correspondre à celle enregistrée.
|
||||||
|
|
||||||
Ce parcours doit rester exclusivement synthétique : aucune enquête, preuve,
|
## Parcours manuel
|
||||||
base SQLite, pièce jointe ou donnée réelle ne doit être utilisée.
|
|
||||||
|
| Étape | Action | Résultat attendu | Échec bloquant |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Sélectionner la fixture importée. | « Analyser l'e-mail » est disponible. | Action absente ou active sur un fichier non EML. |
|
||||||
|
| 2 | Lancer l'analyse puis l'annuler depuis le panneau d'activité. | État annulé, aucun dialogue d'intégration, aucune observation, entité ou relation créée. | Écriture persistante après annulation. |
|
||||||
|
| 3 | Relancer l'analyse. | Le dialogue affiche en-têtes, deux pièces jointes, textes, métadonnées et avertissements disponibles. | Plantage ou modification de la preuve. |
|
||||||
|
| 4 | Contrôler `From`, `Sender`, `Reply-To`, `Return-Path`, `To`, `Cc`, `Bcc`, `Message-ID` et chaque `Received`. | Chaque proposition d'en-tête indique son rôle et son origine avec occurrence. | Rôle ou origine perdus. |
|
||||||
|
| 5 | Contrôler les indicateurs. | `192.0.2.10` et `198.51.100.20` sont des IP ; `1.0` n'est jamais un domaine ; `Message-ID` n'est pas une adresse e-mail. | Mauvaise qualification. |
|
||||||
|
| 6 | Contrôler les propositions bancaires. | L'IBAN synthétique commençant par `FR00` est invalide et non intégrable ; aucune attribution pénale n'est déduite du titulaire déclaré. | Promotion automatique ou donnée invalide intégrable. |
|
||||||
|
| 7 | Fermer avec « Rejeter et fermer ». | Aucun objet persistant nouveau. | Observation, entité ou lien créé. |
|
||||||
|
| 8 | Relancer, cocher seulement « Conserver dans la fiche » sur quelques propositions. | « Promouvoir en entité » reste décoché par défaut et n'est activable que pour une proposition conservée. | Promotion implicite. |
|
||||||
|
| 9 | Intégrer la sélection. | Le bilan annonce zéro promotion ; la fiche reste sélectionnée et affiche toutes les observations choisies, leur valeur canonique, type, rôle, source/occurrence, provenance, validation et « Graphe : Non ajoutée ». Aucune ligne `entites` ou `preuve_entites` n'est créée. | Nœud ou rattachement créé par la conservation. |
|
||||||
|
| 10 | Changer de preuve puis revenir. | Toutes les observations réapparaissent immédiatement. | Observation perdue au rafraîchissement. |
|
||||||
|
| 11 | Fermer puis rouvrir l'enquête synthétique. | Observations, rôles, origines et état non promu persistent. | Perte de données. |
|
||||||
|
| 12 | Relancer et promouvoir explicitement une seule observation conservée. | Une seule entité est créée ou réutilisée, un seul nœud apparaît et la fiche indique la promotion. | Plusieurs nœuds ou promotion non demandée. |
|
||||||
|
| 13 | Répéter la même intégration. | Aucune observation ni entité en double. | Doublon. |
|
||||||
|
| 14 | Dans « Observations extraites », choisir « Retirer du graphe » et confirmer. | L'observation reste présente avec rôle et provenance ; elle repasse à « Graphe : Non ajoutée ». | Observation supprimée. |
|
||||||
|
| 15 | Vérifier le graphe. | Le nœud disparaît seulement s'il n'a aucune autre référence. | Suppression d'une entité encore utilisée. |
|
||||||
|
| 16 | Promouvoir deux rôles vers la même valeur canonique, puis en retirer un. | L'entité et l'autre observation promue restent présentes. | Entité partagée supprimée. |
|
||||||
|
| 17 | Relier une entité promue à une relation, puis retirer la promotion. | La relation et l'entité restent présentes. | Relation ou entité utilisée supprimée. |
|
||||||
|
| 18 | Réouvrir l'enquête. | États de conservation, promotion et retrait persistants. | État uniquement visuel. |
|
||||||
|
| 19 | Créer manuellement le même rattachement preuve-entité qu'une promotion, puis retirer cette promotion. | L'observation est détachée, mais le rattachement manuel, le nœud et l'entité restent présents. | Le rattachement manuel ou l'entité disparaît. |
|
||||||
|
| 19 | Recalculer l'intégrité. | Le SHA-256 final de la fixture est inchangé. | Empreinte modifiée. |
|
||||||
|
|
||||||
|
## Dépendances optionnelles
|
||||||
|
|
||||||
|
Répéter l'analyse avec ExifTool, Tesseract ou les outils Poppler absents, selon
|
||||||
|
les possibilités du poste. L'outil concerné doit être signalé indisponible ou
|
||||||
|
le résultat partiel ; les en-têtes et l'extraction MIME restent consultables.
|
||||||
|
Un plantage ou l'impossibilité d'accéder aux résultats EML de base est
|
||||||
|
bloquant.
|
||||||
|
|
||||||
|
## Limites connues
|
||||||
|
|
||||||
|
- La promotion est proposée dans le dialogue d'analyse, pas depuis la fiche.
|
||||||
|
- La fiche affiche encore les codes persistés et ne montre pas séparément la
|
||||||
|
valeur brute différente ni l'UUID de l'entité.
|
||||||
|
- Les sorties des outils sont bornées et annulables, mais sans délai maximal
|
||||||
|
autonome.
|
||||||
|
- Les fichiers du staging sont nettoyés ; le parcours ne conserve pas encore
|
||||||
|
les pièces jointes comme dérivés confirmés.
|
||||||
|
- Tester le retrait avec un rattachement manuel préexistant : V13 conserve sa
|
||||||
|
source `manual` indépendamment de la source `eml_observation`.
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,16 @@ gboolean evidence_entity_dao_unlink(
|
||||||
GError **error
|
GError **error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_add_source(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
|
const char *entity_identifier, const char *source_kind,
|
||||||
|
const char *source_uuid, const char *created_at, GError **error);
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_remove_source(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
|
const char *entity_identifier, const char *source_kind,
|
||||||
|
const char *source_uuid, gboolean *out_link_removed, GError **error);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Vérifie si une association existe.
|
* @brief Vérifie si une association existe.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ bool schema_install_v9(Database *database);
|
||||||
bool schema_install_v10(Database *database);
|
bool schema_install_v10(Database *database);
|
||||||
bool schema_install_v11(Database *database);
|
bool schema_install_v11(Database *database);
|
||||||
bool schema_install_v12(Database *database);
|
bool schema_install_v12(Database *database);
|
||||||
|
bool schema_install_v13(Database *database);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
|
|
|
||||||
|
|
@ -4069,14 +4069,8 @@ static void application_on_rib_ocr_confirmed(const char *iban,
|
||||||
}
|
}
|
||||||
link_dao = evidence_entity_dao_new(database, &error);
|
link_dao = evidence_entity_dao_new(database, &error);
|
||||||
if (link_dao == NULL) goto failure;
|
if (link_dao == NULL) goto failure;
|
||||||
{
|
if (!evidence_entity_dao_link(link_dao, context->evidence_identifier,
|
||||||
gboolean exists = FALSE;
|
entity_identifier, &error)) goto failure;
|
||||||
if (!evidence_entity_dao_exists(link_dao, context->evidence_identifier,
|
|
||||||
entity_identifier, &exists, &error) ||
|
|
||||||
(!exists && !evidence_entity_dao_link(link_dao,
|
|
||||||
context->evidence_identifier, entity_identifier, &error)))
|
|
||||||
goto failure;
|
|
||||||
}
|
|
||||||
if (!database_transaction_commit(database)) goto failure;
|
if (!database_transaction_commit(database)) goto failure;
|
||||||
active = FALSE; project = investigation_session_get_project(application->session);
|
active = FALSE; project = investigation_session_get_project(application->session);
|
||||||
g_free(application->pending_entity_selection_identifier);
|
g_free(application->pending_entity_selection_identifier);
|
||||||
|
|
@ -6416,11 +6410,8 @@ static void application_on_person_evidences_selected(GPtrArray *selected,
|
||||||
for (guint index = 0; index < selected->len; index++)
|
for (guint index = 0; index < selected->len; index++)
|
||||||
{
|
{
|
||||||
const char *identifier = g_ptr_array_index(selected, index);
|
const char *identifier = g_ptr_array_index(selected, index);
|
||||||
gboolean exists = FALSE;
|
if (!evidence_entity_dao_link(dao, identifier,
|
||||||
if (!evidence_entity_dao_exists(dao, identifier,
|
context->entity_identifier, &error)) goto failure;
|
||||||
context->entity_identifier, &exists, &error) ||
|
|
||||||
(!exists && !evidence_entity_dao_link(dao, identifier,
|
|
||||||
context->entity_identifier, &error))) goto failure;
|
|
||||||
}
|
}
|
||||||
if (!database_transaction_commit(database)) goto failure;
|
if (!database_transaction_commit(database)) goto failure;
|
||||||
active = FALSE;
|
active = FALSE;
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,6 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
const char *identifier = existing != NULL
|
const char *identifier = existing != NULL
|
||||||
? entity_record_get_identifier(existing) : NULL;
|
? entity_record_get_identifier(existing) : NULL;
|
||||||
char *new_identifier = NULL; EntityRecord *new_record = NULL;
|
char *new_identifier = NULL; EntityRecord *new_record = NULL;
|
||||||
gboolean linked = FALSE;
|
|
||||||
if (existing == NULL)
|
if (existing == NULL)
|
||||||
{
|
{
|
||||||
new_identifier = g_uuid_string_random(); identifier = new_identifier;
|
new_identifier = g_uuid_string_random(); identifier = new_identifier;
|
||||||
|
|
@ -178,10 +177,9 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
||||||
}
|
}
|
||||||
else reused++;
|
else reused++;
|
||||||
if (!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
if (!evidence_entity_dao_add_source(link_dao, evidence_identifier,
|
||||||
identifier, &linked, error) ||
|
identifier, "eml_observation", observation_identifier,
|
||||||
(!linked && !evidence_entity_dao_link(link_dao,
|
timestamp, error))
|
||||||
evidence_identifier, identifier, error)))
|
|
||||||
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
||||||
if (!evidence_entity_dao_promote_observation(link_dao,
|
if (!evidence_entity_dao_promote_observation(link_dao,
|
||||||
observation_identifier, identifier, timestamp,
|
observation_identifier, identifier, timestamp,
|
||||||
|
|
@ -225,15 +223,11 @@ gboolean eml_integration_remove_promotion(Database *database,
|
||||||
static const char *detach_sql =
|
static const char *detach_sql =
|
||||||
"UPDATE evidence_entity_observations SET entity_id=NULL,"
|
"UPDATE evidence_entity_observations SET entity_id=NULL,"
|
||||||
"promoted_at=NULL,promotion_kind=NULL WHERE id=? AND entity_id=?;";
|
"promoted_at=NULL,promotion_kind=NULL WHERE id=? AND entity_id=?;";
|
||||||
static const char *same_evidence_sql =
|
|
||||||
"SELECT COUNT(*) FROM evidence_entity_observations "
|
|
||||||
"WHERE evidence_id=?1 AND entity_id=?2;";
|
|
||||||
static const char *unlink_sql =
|
|
||||||
"DELETE FROM preuve_entites WHERE preuve_id=?1 AND entite_id=?2;";
|
|
||||||
static const char *dependency_sql =
|
static const char *dependency_sql =
|
||||||
"SELECT "
|
"SELECT "
|
||||||
"(SELECT COUNT(*) FROM evidence_entity_observations WHERE entity_id=?1)+"
|
"(SELECT COUNT(*) FROM evidence_entity_observations WHERE entity_id=?1)+"
|
||||||
"(SELECT COUNT(*) FROM preuve_entites WHERE entite_id=?1)+"
|
"(SELECT COUNT(*) FROM preuve_entites WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM preuve_entite_sources WHERE entite_id=?1)+"
|
||||||
"(SELECT COUNT(*) FROM relations WHERE entite_source_id=?1 OR entite_cible_id=?1)+"
|
"(SELECT COUNT(*) FROM relations WHERE entite_source_id=?1 OR entite_cible_id=?1)+"
|
||||||
"(SELECT COUNT(*) FROM tag_entites WHERE entite_id=?1)+"
|
"(SELECT COUNT(*) FROM tag_entites WHERE entite_id=?1)+"
|
||||||
"(SELECT COUNT(*) FROM recherche_entites WHERE entite_id=?1)+"
|
"(SELECT COUNT(*) FROM recherche_entites WHERE entite_id=?1)+"
|
||||||
|
|
@ -244,6 +238,7 @@ gboolean eml_integration_remove_promotion(Database *database,
|
||||||
"(SELECT COUNT(*) FROM person_roles WHERE entity_id=?1);";
|
"(SELECT COUNT(*) FROM person_roles WHERE entity_id=?1);";
|
||||||
static const char *delete_sql = "DELETE FROM entites WHERE id=?;";
|
static const char *delete_sql = "DELETE FROM entites WHERE id=?;";
|
||||||
DatabaseStatement *statement = NULL;
|
DatabaseStatement *statement = NULL;
|
||||||
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
char *evidence_identifier = NULL, *entity_identifier = NULL;
|
char *evidence_identifier = NULL, *entity_identifier = NULL;
|
||||||
gint64 count = 0; gboolean active = FALSE, success = FALSE;
|
gint64 count = 0; gboolean active = FALSE, success = FALSE;
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
|
@ -253,6 +248,8 @@ gboolean eml_integration_remove_promotion(Database *database,
|
||||||
return FALSE;
|
return FALSE;
|
||||||
if (!database_transaction_begin(database)) return FALSE;
|
if (!database_transaction_begin(database)) return FALSE;
|
||||||
active = TRUE;
|
active = TRUE;
|
||||||
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
|
if (link_dao == NULL) goto cleanup;
|
||||||
statement = database_statement_prepare(database, read_sql);
|
statement = database_statement_prepare(database, read_sql);
|
||||||
if (statement == NULL ||
|
if (statement == NULL ||
|
||||||
!database_statement_bind_text(statement, 1, observation_identifier) ||
|
!database_statement_bind_text(statement, 1, observation_identifier) ||
|
||||||
|
|
@ -275,24 +272,9 @@ gboolean eml_integration_remove_promotion(Database *database,
|
||||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
database_statement_finalize(statement); statement = NULL;
|
database_statement_finalize(statement); statement = NULL;
|
||||||
statement = database_statement_prepare(database, same_evidence_sql);
|
if (!evidence_entity_dao_remove_source(link_dao, evidence_identifier,
|
||||||
if (statement == NULL ||
|
entity_identifier, "eml_observation", observation_identifier,
|
||||||
!database_statement_bind_text(statement, 1, evidence_identifier) ||
|
NULL, error)) goto cleanup;
|
||||||
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
|
||||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW ||
|
|
||||||
!database_statement_column_int64(statement, 0, &count))
|
|
||||||
goto cleanup;
|
|
||||||
database_statement_finalize(statement); statement = NULL;
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
statement = database_statement_prepare(database, unlink_sql);
|
|
||||||
if (statement == NULL ||
|
|
||||||
!database_statement_bind_text(statement, 1, evidence_identifier) ||
|
|
||||||
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
|
||||||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
|
||||||
goto cleanup;
|
|
||||||
database_statement_finalize(statement); statement = NULL;
|
|
||||||
}
|
|
||||||
if (!eml_integration_read_count(database, dependency_sql,
|
if (!eml_integration_read_count(database, dependency_sql,
|
||||||
entity_identifier, &count)) goto cleanup;
|
entity_identifier, &count)) goto cleanup;
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
|
|
@ -311,6 +293,7 @@ gboolean eml_integration_remove_promotion(Database *database,
|
||||||
cleanup:
|
cleanup:
|
||||||
database_statement_finalize(statement);
|
database_statement_finalize(statement);
|
||||||
if (!success && active) database_transaction_rollback(database);
|
if (!success && active) database_transaction_rollback(database);
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
g_free(evidence_identifier); g_free(entity_identifier);
|
g_free(evidence_identifier); g_free(entity_identifier);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,6 @@ static gboolean extraction_drop_service_attach_in_transaction(
|
||||||
{
|
{
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
char *evidence_identifier = NULL;
|
char *evidence_identifier = NULL;
|
||||||
gboolean exists = FALSE;
|
|
||||||
gboolean success = FALSE;
|
gboolean success = FALSE;
|
||||||
|
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
|
@ -254,10 +253,8 @@ static gboolean extraction_drop_service_attach_in_transaction(
|
||||||
if (evidence_identifier == NULL) goto cleanup;
|
if (evidence_identifier == NULL) goto cleanup;
|
||||||
link_dao = evidence_entity_dao_new(database, error);
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
if (link_dao == NULL ||
|
if (link_dao == NULL ||
|
||||||
!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
!evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||||
entity_identifier, &exists, error) ||
|
entity_identifier, error)) goto cleanup;
|
||||||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
|
|
||||||
entity_identifier, error))) goto cleanup;
|
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
cleanup:
|
cleanup:
|
||||||
evidence_entity_dao_free(link_dao);
|
evidence_entity_dao_free(link_dao);
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ static const char *const evidence_entity_dao_association_exists_sql =
|
||||||
* @brief Requête créant une association.
|
* @brief Requête créant une association.
|
||||||
*/
|
*/
|
||||||
static const char *const evidence_entity_dao_link_sql =
|
static const char *const evidence_entity_dao_link_sql =
|
||||||
"INSERT INTO preuve_entites"
|
"INSERT OR IGNORE INTO preuve_entites"
|
||||||
"("
|
"("
|
||||||
" preuve_id,"
|
" preuve_id,"
|
||||||
" entite_id"
|
" entite_id"
|
||||||
|
|
@ -71,6 +71,36 @@ static const char *const evidence_entity_dao_unlink_sql =
|
||||||
"WHERE preuve_id = ? "
|
"WHERE preuve_id = ? "
|
||||||
"AND entite_id = ?;";
|
"AND entite_id = ?;";
|
||||||
|
|
||||||
|
static void evidence_entity_dao_set_database_error(
|
||||||
|
EvidenceEntityDao *evidence_entity_dao, GError **error,
|
||||||
|
EvidenceEntityDaoError error_code, const char *context);
|
||||||
|
|
||||||
|
static gboolean evidence_entity_dao_execute_source_statement(
|
||||||
|
EvidenceEntityDao *dao, const char *sql, const char *evidence_identifier,
|
||||||
|
const char *entity_identifier, const char *source_kind,
|
||||||
|
const char *source_uuid, const char *created_at, GError **error)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(dao->database, sql);
|
||||||
|
gboolean success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, evidence_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, entity_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 3, source_kind);
|
||||||
|
if (success && source_uuid != NULL)
|
||||||
|
success = database_statement_bind_text(statement, 4, source_uuid);
|
||||||
|
if (success && created_at != NULL)
|
||||||
|
success = database_statement_bind_text(statement, 5, created_at);
|
||||||
|
if (success)
|
||||||
|
success = database_statement_step(statement) ==
|
||||||
|
DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
if (!success)
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
statement == NULL ? EVIDENCE_ENTITY_DAO_ERROR_PREPARE :
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de modifier la provenance de l'association");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Requête listant les entités liées à une preuve.
|
* @brief Requête listant les entités liées à une preuve.
|
||||||
*/
|
*/
|
||||||
|
|
@ -801,8 +831,9 @@ gboolean evidence_entity_dao_link(
|
||||||
gboolean entity_exists =
|
gboolean entity_exists =
|
||||||
FALSE;
|
FALSE;
|
||||||
|
|
||||||
gboolean association_exists =
|
GDateTime *now = NULL;
|
||||||
FALSE;
|
char *created_at = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
g_return_val_if_fail(
|
g_return_val_if_fail(
|
||||||
error == NULL || *error == NULL,
|
error == NULL || *error == NULL,
|
||||||
|
|
@ -865,39 +896,20 @@ gboolean evidence_entity_dao_link(
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!evidence_entity_dao_query_exists(
|
now = g_date_time_new_now_utc();
|
||||||
evidence_entity_dao,
|
created_at = now != NULL
|
||||||
evidence_entity_dao_association_exists_sql,
|
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||||
evidence_identifier,
|
if (created_at == NULL)
|
||||||
entity_identifier,
|
evidence_entity_dao_set_error_literal(error,
|
||||||
&association_exists,
|
EVIDENCE_ENTITY_DAO_ERROR_MEMORY,
|
||||||
error
|
"Impossible de dater l'association preuve-entité.");
|
||||||
))
|
else
|
||||||
{
|
success = evidence_entity_dao_add_source(evidence_entity_dao,
|
||||||
return FALSE;
|
evidence_identifier, entity_identifier, "manual", NULL,
|
||||||
}
|
created_at, error);
|
||||||
|
g_free(created_at);
|
||||||
if (association_exists)
|
g_clear_pointer(&now, g_date_time_unref);
|
||||||
{
|
return success;
|
||||||
evidence_entity_dao_set_error_literal(
|
|
||||||
error,
|
|
||||||
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT,
|
|
||||||
"Cette preuve est déjà associée à cette entité."
|
|
||||||
);
|
|
||||||
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return evidence_entity_dao_execute_pair_statement(
|
|
||||||
evidence_entity_dao,
|
|
||||||
evidence_entity_dao_link_sql,
|
|
||||||
evidence_identifier,
|
|
||||||
entity_identifier,
|
|
||||||
"Impossible de préparer la création de l'association",
|
|
||||||
"Impossible de lier les identifiants de l'association",
|
|
||||||
"Impossible de créer l'association",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean evidence_entity_dao_unlink(
|
gboolean evidence_entity_dao_unlink(
|
||||||
|
|
@ -948,16 +960,103 @@ gboolean evidence_entity_dao_unlink(
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return evidence_entity_dao_execute_pair_statement(
|
return evidence_entity_dao_remove_source(evidence_entity_dao,
|
||||||
evidence_entity_dao,
|
evidence_identifier, entity_identifier, "manual", NULL, NULL, error);
|
||||||
evidence_entity_dao_unlink_sql,
|
}
|
||||||
evidence_identifier,
|
|
||||||
entity_identifier,
|
gboolean evidence_entity_dao_add_source(
|
||||||
"Impossible de préparer la suppression de l'association",
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
"Impossible de lier les identifiants de l'association",
|
const char *entity_identifier, const char *source_kind,
|
||||||
"Impossible de supprimer l'association",
|
const char *source_uuid, const char *created_at, GError **error)
|
||||||
error
|
{
|
||||||
);
|
static const char *insert_sql =
|
||||||
|
"INSERT OR IGNORE INTO preuve_entite_sources"
|
||||||
|
"(id,preuve_id,entite_id,source_kind,source_uuid,created_at)"
|
||||||
|
"VALUES(?,?,?,?,?,?);";
|
||||||
|
char *source_identifier = NULL;
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
if (!evidence_entity_dao_validate_association(dao, evidence_identifier,
|
||||||
|
entity_identifier, error) ||
|
||||||
|
source_kind == NULL || created_at == NULL ||
|
||||||
|
(g_strcmp0(source_kind, "eml_observation") == 0 &&
|
||||||
|
(source_uuid == NULL || !g_uuid_string_is_valid(source_uuid))))
|
||||||
|
return FALSE;
|
||||||
|
if (!evidence_entity_dao_execute_pair_statement(dao,
|
||||||
|
evidence_entity_dao_link_sql, evidence_identifier,
|
||||||
|
entity_identifier, "Impossible de préparer l'association",
|
||||||
|
"Impossible de lier l'association",
|
||||||
|
"Impossible de matérialiser l'association", error))
|
||||||
|
return FALSE;
|
||||||
|
source_identifier = g_uuid_string_random();
|
||||||
|
statement = database_statement_prepare(dao->database, insert_sql);
|
||||||
|
success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, source_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, evidence_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 3, entity_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 4, source_kind);
|
||||||
|
if (success && source_uuid != NULL)
|
||||||
|
success = database_statement_bind_text(statement, 5, source_uuid);
|
||||||
|
if (success)
|
||||||
|
success = database_statement_bind_text(statement, 6, created_at) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
if (!success)
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
statement == NULL ? EVIDENCE_ENTITY_DAO_ERROR_PREPARE :
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible d'enregistrer la provenance de l'association");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
g_free(source_identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_remove_source(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
|
const char *entity_identifier, const char *source_kind,
|
||||||
|
const char *source_uuid, gboolean *out_link_removed, GError **error)
|
||||||
|
{
|
||||||
|
static const char *delete_source_sql =
|
||||||
|
"DELETE FROM preuve_entite_sources WHERE preuve_id=?1 AND entite_id=?2 "
|
||||||
|
"AND source_kind=?3 AND COALESCE(source_uuid,'')=COALESCE(?4,'');";
|
||||||
|
static const char *count_sql =
|
||||||
|
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||||
|
"WHERE preuve_id=?1 AND entite_id=?2;";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
gint64 count = 0;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
if (out_link_removed != NULL) *out_link_removed = FALSE;
|
||||||
|
if (!evidence_entity_dao_validate_association(dao, evidence_identifier,
|
||||||
|
entity_identifier, error) || source_kind == NULL)
|
||||||
|
return FALSE;
|
||||||
|
if (!evidence_entity_dao_execute_source_statement(dao, delete_source_sql,
|
||||||
|
evidence_identifier, entity_identifier, source_kind, source_uuid,
|
||||||
|
NULL, error))
|
||||||
|
return FALSE;
|
||||||
|
statement = database_statement_prepare(dao->database, count_sql);
|
||||||
|
success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, evidence_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, entity_identifier) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
|
||||||
|
database_statement_column_int64(statement, 0, &count);
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de vérifier les provenances de l'association");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
success = evidence_entity_dao_execute_pair_statement(dao,
|
||||||
|
evidence_entity_dao_unlink_sql, evidence_identifier,
|
||||||
|
entity_identifier, "Impossible de préparer le détachement",
|
||||||
|
"Impossible de lier le détachement",
|
||||||
|
"Impossible de supprimer l'association sans provenance", error);
|
||||||
|
if (success && out_link_removed != NULL) *out_link_removed = TRUE;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean evidence_entity_dao_exists(
|
gboolean evidence_entity_dao_exists(
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 12
|
#define DATABASE_SCHEMA_VERSION_CURRENT 13
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "12"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "13"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -842,6 +842,21 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v12_to_v13(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v13(database) ||
|
||||||
|
!database_update_schema_version(database, "13") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V12 vers V13.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1079,6 +1094,10 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v11_to_v12(database)) return false;
|
if (!database_migrate_v11_to_v12(database)) return false;
|
||||||
schema_version = 12;
|
schema_version = 12;
|
||||||
break;
|
break;
|
||||||
|
case 12:
|
||||||
|
if (!database_migrate_v12_to_v13(database)) return false;
|
||||||
|
schema_version = 13;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,12 @@ bool schema_install_v12(Database *database)
|
||||||
"la migration SQLite V12");
|
"la migration SQLite V12");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v13(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v13.sql",
|
||||||
|
"la migration SQLite V13");
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "12") == 0);
|
assert(strcmp(schema_version, "13") == 0);
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
test_database_assert_table_exists(database, "graph_viewport");
|
test_database_assert_table_exists(database, "graph_viewport");
|
||||||
|
|
@ -992,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"12"
|
"13"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1399,6 +1399,54 @@ static void test_database_migration_rollback(void)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
char *directory = g_dir_make_tmp("labfy-v13-test-XXXXXX", &error);
|
||||||
|
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||||
|
sqlite3 *sqlite_database = NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
char *version = NULL;
|
||||||
|
char *legacy_sources = NULL;
|
||||||
|
assert(directory != NULL && error == NULL);
|
||||||
|
assert(database_initialize(path, "Migration V13 synthétique", directory));
|
||||||
|
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
|
||||||
|
test_database_execute_sql(sqlite_database,
|
||||||
|
"DROP TABLE preuve_entite_sources;"
|
||||||
|
"UPDATE metadata SET value='12' WHERE key='schema_version';"
|
||||||
|
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
|
||||||
|
"imported_at,updated_at,status,locked,original_name) VALUES("
|
||||||
|
"'10000000-0000-4000-8000-000000000013','legacy.eml','legacy.eml',"
|
||||||
|
"5,1,'0000000000000000000000000000000000000000000000000000000000000000',"
|
||||||
|
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',0,'legacy.eml');"
|
||||||
|
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,"
|
||||||
|
"updated_at,status) VALUES("
|
||||||
|
"'20000000-0000-4000-8000-000000000013',1,'legacy@example.test',"
|
||||||
|
"'legacy@example.test',50,'2026-07-28T08:00:00Z',"
|
||||||
|
"'2026-07-28T08:00:00Z','active');"
|
||||||
|
"INSERT INTO preuve_entites(preuve_id,entite_id) VALUES("
|
||||||
|
"'10000000-0000-4000-8000-000000000013',"
|
||||||
|
"'20000000-0000-4000-8000-000000000013');");
|
||||||
|
assert(sqlite3_close(sqlite_database) == SQLITE_OK);
|
||||||
|
database = database_open(path);
|
||||||
|
assert(database != NULL && database_migrate_to_latest(database));
|
||||||
|
database_close(database);
|
||||||
|
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
|
||||||
|
version = test_database_read_single_text(sqlite_database,
|
||||||
|
"SELECT value FROM metadata WHERE key='schema_version';");
|
||||||
|
legacy_sources = test_database_read_single_text(sqlite_database,
|
||||||
|
"SELECT COUNT(*) FROM preuve_entite_sources "
|
||||||
|
"WHERE source_kind='legacy_manual';");
|
||||||
|
assert(strcmp(version, "13") == 0);
|
||||||
|
assert(strcmp(legacy_sources, "1") == 0);
|
||||||
|
assert(sqlite3_close(sqlite_database) == SQLITE_OK);
|
||||||
|
assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
|
||||||
|
g_free(legacy_sources);
|
||||||
|
g_free(version);
|
||||||
|
g_free(path);
|
||||||
|
g_free(directory);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
test_database_initialize_valid_database();
|
test_database_initialize_valid_database();
|
||||||
|
|
@ -1406,6 +1454,7 @@ int main(void)
|
||||||
|
|
||||||
test_database_migrate_v1_to_v2();
|
test_database_migrate_v1_to_v2();
|
||||||
test_database_migration_rollback();
|
test_database_migration_rollback();
|
||||||
|
test_database_migrate_v12_to_v13_preserves_legacy_link();
|
||||||
|
|
||||||
test_database_initialize_invalid_parameters();
|
test_database_initialize_invalid_parameters();
|
||||||
test_database_initialize_missing_parent();
|
test_database_initialize_missing_parent();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "core/eml_integration.h"
|
#include "core/eml_integration.h"
|
||||||
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
#include "database/statement.h"
|
#include "database/statement.h"
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
@ -38,6 +39,20 @@ static char *read_observation_identifier(Database *database, const char *role)
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *read_observation_entity_identifier(
|
||||||
|
Database *database, const char *observation_identifier)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database,
|
||||||
|
"SELECT entity_id FROM evidence_entity_observations WHERE id=?;");
|
||||||
|
char *identifier = NULL;
|
||||||
|
assert(statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, observation_identifier));
|
||||||
|
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||||
|
assert(database_statement_column_text(statement, 0, &identifier));
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
|
|
@ -82,13 +97,25 @@ int main(void)
|
||||||
&observations, &created, &reused, &error));
|
&observations, &created, &reused, &error));
|
||||||
assert(created == 0 && reused == 1 && count_rows(database, "entites") == 1);
|
assert(created == 0 && reused == 1 && count_rows(database, "entites") == 1);
|
||||||
char *from_observation = read_observation_identifier(database, "from");
|
char *from_observation = read_observation_identifier(database, "from");
|
||||||
|
char *from_entity = read_observation_entity_identifier(
|
||||||
|
database, from_observation);
|
||||||
|
EvidenceEntityDao *link_dao = evidence_entity_dao_new(database, &error);
|
||||||
|
assert(link_dao != NULL && error == NULL);
|
||||||
|
/* Vérifie aussi la coexistence des propriétaires manuel et EML. */
|
||||||
|
assert(evidence_entity_dao_link(link_dao, evidence_id, from_entity, &error));
|
||||||
gboolean deleted = FALSE, shared = FALSE;
|
gboolean deleted = FALSE, shared = FALSE;
|
||||||
assert(eml_integration_remove_promotion(database, from_observation,
|
assert(eml_integration_remove_promotion(database, from_observation,
|
||||||
&deleted, &shared, &error));
|
&deleted, &shared, &error));
|
||||||
assert(deleted && !shared);
|
assert(!deleted && shared);
|
||||||
assert(count_rows(database, "evidence_entity_observations") == 1);
|
assert(count_rows(database, "evidence_entity_observations") == 1);
|
||||||
assert(count_rows(database, "entites") == 0);
|
assert(count_rows(database, "entites") == 1);
|
||||||
|
assert(count_rows(database, "preuve_entites") == 1);
|
||||||
|
assert(evidence_entity_dao_unlink(link_dao, evidence_id, from_entity,
|
||||||
|
&error));
|
||||||
assert(count_rows(database, "preuve_entites") == 0);
|
assert(count_rows(database, "preuve_entites") == 0);
|
||||||
|
execute_done(database, "DELETE FROM entites;");
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
g_free(from_entity);
|
||||||
assert(eml_integration_apply(database, evidence_id, proposals,
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
&observations, &created, &reused, &error));
|
&observations, &created, &reused, &error));
|
||||||
assert(created == 1);
|
assert(created == 1);
|
||||||
|
|
@ -128,6 +155,42 @@ int main(void)
|
||||||
assert(!deleted && shared);
|
assert(!deleted && shared);
|
||||||
assert(count_rows(database, "relations") == 1);
|
assert(count_rows(database, "relations") == 1);
|
||||||
assert(count_rows(database, "evidence_entity_observations") == 2);
|
assert(count_rows(database, "evidence_entity_observations") == 2);
|
||||||
|
|
||||||
|
/* Reproduction exacte : rattachement manuel avant la promotion EML. */
|
||||||
|
execute_done(database,
|
||||||
|
"INSERT INTO entites(id,type_id,valeur,label,description,confiance,"
|
||||||
|
"created_at,updated_at,status) VALUES("
|
||||||
|
"'20000000-0000-4000-8000-000000000107',1,'manual@example.test',"
|
||||||
|
"'manual@example.test',NULL,50,'2026-07-28T08:00:00Z',"
|
||||||
|
"'2026-07-28T08:00:00Z','active');");
|
||||||
|
link_dao = evidence_entity_dao_new(database, &error);
|
||||||
|
assert(link_dao != NULL &&
|
||||||
|
evidence_entity_dao_link(link_dao, evidence_id,
|
||||||
|
"20000000-0000-4000-8000-000000000107", &error));
|
||||||
|
GPtrArray *manual_first = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) eml_entity_proposal_free);
|
||||||
|
EmlEntityProposal *manual_first_proposal =
|
||||||
|
eml_entity_proposal_new_observation("email_address",
|
||||||
|
"manual@example.test", "manual@example.test", "to", "to", 1,
|
||||||
|
"confirmed", "header");
|
||||||
|
manual_first_proposal->promote_to_entity = TRUE;
|
||||||
|
g_ptr_array_add(manual_first, manual_first_proposal);
|
||||||
|
assert(eml_integration_apply(database, evidence_id, manual_first,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(created == 0 && reused == 1);
|
||||||
|
char *manual_first_observation = read_observation_identifier(database, "to");
|
||||||
|
assert(eml_integration_remove_promotion(database, manual_first_observation,
|
||||||
|
&deleted, &shared, &error));
|
||||||
|
gboolean manual_link_exists = FALSE;
|
||||||
|
assert(!deleted && shared &&
|
||||||
|
evidence_entity_dao_exists(link_dao, evidence_id,
|
||||||
|
"20000000-0000-4000-8000-000000000107", &manual_link_exists,
|
||||||
|
&error));
|
||||||
|
assert(manual_link_exists);
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
g_free(manual_first_observation);
|
||||||
|
g_ptr_array_unref(manual_first);
|
||||||
|
|
||||||
g_free(reply_observation);
|
g_free(reply_observation);
|
||||||
g_free(from_observation);
|
g_free(from_observation);
|
||||||
g_ptr_array_unref(proposals);
|
g_ptr_array_unref(proposals);
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,7 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Vérifie le refus d'une association dupliquée.
|
* @brief Vérifie l'idempotence d'une association manuelle.
|
||||||
*/
|
*/
|
||||||
static void test_evidence_entity_dao_link_duplicate(void)
|
static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
{
|
{
|
||||||
|
|
@ -637,7 +637,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
assert(error == NULL);
|
assert(error == NULL);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
!evidence_entity_dao_link(
|
evidence_entity_dao_link(
|
||||||
fixture.evidence_entity_dao,
|
fixture.evidence_entity_dao,
|
||||||
evidence_identifier,
|
evidence_identifier,
|
||||||
entity_identifier,
|
entity_identifier,
|
||||||
|
|
@ -645,10 +645,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
test_evidence_entity_dao_assert_error(
|
assert(error == NULL);
|
||||||
error,
|
|
||||||
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
test_evidence_entity_dao_count_rows(
|
test_evidence_entity_dao_count_rows(
|
||||||
|
|
@ -656,10 +653,6 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
) == 1
|
) == 1
|
||||||
);
|
);
|
||||||
|
|
||||||
g_clear_error(
|
|
||||||
&error
|
|
||||||
);
|
|
||||||
|
|
||||||
test_evidence_entity_dao_fixture_clear(
|
test_evidence_entity_dao_fixture_clear(
|
||||||
&fixture
|
&fixture
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue