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,
|
||||
avec confirmation explicite, création d'entité ou rattachement à une entité
|
||||
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 :
|
||||
|
||||
|
|
@ -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 :
|
||||
|
||||
```bash
|
||||
make
|
||||
make -j8
|
||||
```
|
||||
|
||||
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 :
|
||||
|
||||
```bash
|
||||
make test
|
||||
make -j8 test
|
||||
```
|
||||
|
||||
Vérifications recommandées avant chaque commit :
|
||||
|
||||
```bash
|
||||
make clean
|
||||
make
|
||||
make test
|
||||
make -j8
|
||||
make -j8 test
|
||||
git diff --check
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -212,3 +212,29 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_evidence_entity_observations_semantic
|
|||
ON evidence_entity_observations(
|
||||
evidence_id,entity_type,value_normalized,role,source_header,
|
||||
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
|
||||
|
||||
> **Version :** 3.0
|
||||
> **Dernière mise à jour :** 2026-07-24
|
||||
> **Schéma SQLite courant :** V10
|
||||
> **Version :** 3.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Schéma SQLite courant :** V13
|
||||
> **Statut :** architecture courante
|
||||
|
||||
---
|
||||
|
|
@ -301,7 +301,7 @@ validation du chemin
|
|||
↓
|
||||
création de l'arborescence
|
||||
↓
|
||||
initialisation transactionnelle de SQLite V10
|
||||
initialisation transactionnelle de SQLite V13
|
||||
↓
|
||||
création de l'identité de l'enquête
|
||||
↓
|
||||
|
|
@ -492,43 +492,168 @@ sont proches visuellement.
|
|||
|
||||
## 13. Pivot EML
|
||||
|
||||
Statut :
|
||||
### 13.1 Parcours
|
||||
|
||||
```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,
|
||||
ExifTool, vocabulaire contrôlé, propositions bancaires et pipeline de tâche.
|
||||
La preuve originale n'est jamais modifiée. Le staging et ses pièces jointes
|
||||
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
|
||||
EML original
|
||||
↓
|
||||
en-têtes et MIME
|
||||
↓
|
||||
pièces jointes dérivées
|
||||
↓
|
||||
empreintes et métadonnées
|
||||
↓
|
||||
OCR et indicateurs
|
||||
↓
|
||||
valeurs brutes + normalisées + dérivées
|
||||
↓
|
||||
révision humaine
|
||||
↓
|
||||
intégration transactionnelle
|
||||
```
|
||||
- `EmlAnalyzer` lit les en-têtes, conserve leurs occurrences et qualifie les
|
||||
adresses, domaines et IP.
|
||||
- `EmlMimeExtractor` parcourt les parties imbriquées, y compris
|
||||
`message/rfc822`, décode Base64, quoted-printable, RFC 2047 et RFC 2231,
|
||||
assainit les noms et applique les limites de profondeur, nombre et taille.
|
||||
- `EmlPipelineTask` orchestre en arrière-plan l'analyse, le staging, les outils
|
||||
documentaires et les propositions bancaires.
|
||||
- `DocumentToolRunner` lance les programmes avec `GSubprocess`, sans shell,
|
||||
draine simultanément `stdout` et `stderr`, borne les sorties et propage
|
||||
l'annulation.
|
||||
- les modules ExifTool, OCR et PDF structurent les résultats sans modifier la
|
||||
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,
|
||||
leur validation et une éventuelle correction OCR distincte.
|
||||
- `EmlAnalysisDialog` présente les résultats et collecte séparément la
|
||||
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
|
||||
d'un suspect.
|
||||
Une **proposition** est un résultat temporaire. Elle peut être rejetée,
|
||||
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
|
||||
|
||||
> **Version :** 2.0
|
||||
> **Dernière mise à jour :** 2026-07-24
|
||||
> **Version :** 2.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **Projet :** Labfy Investigation
|
||||
|
||||
---
|
||||
|
|
@ -265,7 +265,7 @@ présentation graphique.
|
|||
La version courante du schéma est :
|
||||
|
||||
```text
|
||||
V10
|
||||
V13
|
||||
```
|
||||
|
||||
Les scripts versionnés sont conservés dans :
|
||||
|
|
@ -273,7 +273,7 @@ Les scripts versionnés sont conservés dans :
|
|||
```text
|
||||
database/schema_v1.sql
|
||||
...
|
||||
database/schema_v10.sql
|
||||
database/schema_v12.sql
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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) |
|
||||
| --- | --- | --- | --- |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
|
||||
| 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 :
|
||||
|
||||
```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
|
||||
|
|
@ -83,8 +87,9 @@ vérifiée avec `command -v`, puis avec son option de version (`--version` ou
|
|||
## Notes
|
||||
|
||||
- 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
|
||||
fichier.
|
||||
- Les versions peuvent varier selon la distribution ; noter ici toute
|
||||
commande particulière nécessaire à Ubuntu des forces de l’ordre.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Guide de développement
|
||||
|
||||
> **Version :** 2.0
|
||||
> **Dernière mise à jour :** 2026-07-24
|
||||
> **Version :** 2.1
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **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
|
||||
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
|
||||
|
|
@ -195,6 +211,42 @@ make 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.
|
||||
|
||||
### 6.3 Validation avant intégration
|
||||
|
|
@ -345,7 +397,7 @@ Lire :
|
|||
docs/database/DATABASE_ARCHITECTURE.md
|
||||
docs/database/SCHEMA_AUDIT_CURRENT.md
|
||||
database/schema_current.sql
|
||||
database/schema_v10.sql
|
||||
database/schema_v12.sql
|
||||
src/database/database.c
|
||||
src/database/schema.c
|
||||
tests/test_database.c
|
||||
|
|
@ -353,21 +405,24 @@ tests/test_database.c
|
|||
|
||||
### 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` ;
|
||||
2. déclarer et implémenter `schema_install_v11()` ;
|
||||
3. ajouter `database_migrate_v10_to_v11()` ;
|
||||
1. ajouter `database/schema_v13.sql` ;
|
||||
2. déclarer et implémenter `schema_install_v13()` ;
|
||||
3. ajouter `database_migrate_v12_to_v13()` ;
|
||||
4. raccorder la migration dans la boucle vers la version courante ;
|
||||
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 ;
|
||||
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 ;
|
||||
10. tester le rollback ;
|
||||
11. vérifier l'intégrité et les clés étrangères ;
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Roadmap
|
||||
|
||||
> **Dernière mise à jour :** 2026-07-24
|
||||
> **Dernière mise à jour :** 2026-07-28
|
||||
> **É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
|
||||
|
||||
---
|
||||
|
|
@ -116,7 +116,7 @@ La branche `main` contient notamment :
|
|||
|
||||
- création, validation et ouverture d'enquêtes ;
|
||||
- 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 ;
|
||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||
- vérification d'intégrité et reclassement des preuves ;
|
||||
|
|
@ -135,7 +135,8 @@ La branche `main` contient notamment :
|
|||
- types canoniques de relations ;
|
||||
- vocabulaire contrôlé ;
|
||||
- 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é.
|
||||
|
||||
|
|
@ -145,15 +146,13 @@ Cette liste est une synthèse et non un contrat de stabilité.
|
|||
|
||||
### Ticket #107 — Pivot e-mail forensique
|
||||
|
||||
Statut :
|
||||
Statut technique :
|
||||
|
||||
```text
|
||||
PARTIEL
|
||||
IMPLÉMENTÉ — VALIDATION DOCUMENTAIRE ET CORRECTION BLOQUANTE RESTANTES
|
||||
```
|
||||
|
||||
Les briques préparatoires existent, mais le flux complet reste à terminer.
|
||||
|
||||
Objectif principal :
|
||||
Le parcours livré est :
|
||||
|
||||
```text
|
||||
preuve EML
|
||||
|
|
@ -171,30 +170,29 @@ OCR et détection d'indicateurs
|
|||
normalisation sans perte de la valeur brute
|
||||
↓
|
||||
interface de révision
|
||||
↓
|
||||
confirmation explicite
|
||||
↓
|
||||
intégration transactionnelle
|
||||
↓
|
||||
rafraîchissement des vues et du graphe
|
||||
↓ conservation explicite
|
||||
observation persistante dans la fiche
|
||||
↓ promotion facultative
|
||||
entité créée ou réutilisée dans le 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 ;
|
||||
2. conserver les pièces jointes comme fichiers dérivés ;
|
||||
3. réunir EML, OCR, IBAN et ExifTool dans un pipeline unique ;
|
||||
4. terminer l'interface de révision ;
|
||||
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é.
|
||||
V13 distingue désormais chaque propriétaire du rattachement
|
||||
`preuve_entites`. Un retrait EML enlève sa seule source
|
||||
`eml_observation`; les sources manuelles, historiques et celles des autres
|
||||
observations restent protégées.
|
||||
|
||||
Le ticket reste ouvert tant que son flux complet et ses critères d'acceptation
|
||||
ne sont pas validés.
|
||||
Améliorations suivantes :
|
||||
|
||||
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
|
||||
|
||||
Ce test emploie uniquement la fixture synthétique
|
||||
`tests/fixtures/eml/manual_smoke_test.eml`. Il ne nécessite aucune donnée
|
||||
réelle.
|
||||
Ce parcours complète les tests automatiques. Il utilise exclusivement la
|
||||
fixture synthétique `tests/fixtures/eml/manual_smoke_test.eml`, dans une
|
||||
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`.
|
||||
2. Créer une enquête synthétique neuve dans un répertoire de test.
|
||||
3. Importer `tests/fixtures/eml/manual_smoke_test.eml` comme preuve.
|
||||
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é.
|
||||
```sh
|
||||
make -j8
|
||||
make -j8 test
|
||||
```
|
||||
|
||||
Noter séparément le comportement lorsque ExifTool, Tesseract ou les outils
|
||||
Poppler ne sont pas installés : les en-têtes et l’extraction MIME doivent
|
||||
rester consultables.
|
||||
Un échec de compilation ou de test est bloquant. Créer ensuite une enquête
|
||||
neuve dans un répertoire temporaire dédié, importer la fixture et vérifier son
|
||||
intégrité depuis la fiche. L'empreinte doit correspondre à celle enregistrée.
|
||||
|
||||
Ce parcours doit rester exclusivement synthétique : aucune enquête, preuve,
|
||||
base SQLite, pièce jointe ou donnée réelle ne doit être utilisée.
|
||||
## Parcours manuel
|
||||
|
||||
| É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
|
||||
);
|
||||
|
||||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ bool schema_install_v9(Database *database);
|
|||
bool schema_install_v10(Database *database);
|
||||
bool schema_install_v11(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.
|
||||
|
|
|
|||
|
|
@ -4069,14 +4069,8 @@ static void application_on_rib_ocr_confirmed(const char *iban,
|
|||
}
|
||||
link_dao = evidence_entity_dao_new(database, &error);
|
||||
if (link_dao == NULL) goto failure;
|
||||
{
|
||||
gboolean exists = FALSE;
|
||||
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 (!evidence_entity_dao_link(link_dao, context->evidence_identifier,
|
||||
entity_identifier, &error)) goto failure;
|
||||
if (!database_transaction_commit(database)) goto failure;
|
||||
active = FALSE; project = investigation_session_get_project(application->session);
|
||||
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++)
|
||||
{
|
||||
const char *identifier = g_ptr_array_index(selected, index);
|
||||
gboolean exists = FALSE;
|
||||
if (!evidence_entity_dao_exists(dao, identifier,
|
||||
context->entity_identifier, &exists, &error) ||
|
||||
(!exists && !evidence_entity_dao_link(dao, identifier,
|
||||
context->entity_identifier, &error))) goto failure;
|
||||
if (!evidence_entity_dao_link(dao, identifier,
|
||||
context->entity_identifier, &error)) goto failure;
|
||||
}
|
||||
if (!database_transaction_commit(database)) goto failure;
|
||||
active = FALSE;
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
|||
const char *identifier = existing != NULL
|
||||
? entity_record_get_identifier(existing) : NULL;
|
||||
char *new_identifier = NULL; EntityRecord *new_record = NULL;
|
||||
gboolean linked = FALSE;
|
||||
if (existing == NULL)
|
||||
{
|
||||
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++;
|
||||
}
|
||||
else reused++;
|
||||
if (!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
||||
identifier, &linked, error) ||
|
||||
(!linked && !evidence_entity_dao_link(link_dao,
|
||||
evidence_identifier, identifier, error)))
|
||||
if (!evidence_entity_dao_add_source(link_dao, evidence_identifier,
|
||||
identifier, "eml_observation", observation_identifier,
|
||||
timestamp, error))
|
||||
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
||||
if (!evidence_entity_dao_promote_observation(link_dao,
|
||||
observation_identifier, identifier, timestamp,
|
||||
|
|
@ -225,15 +223,11 @@ gboolean eml_integration_remove_promotion(Database *database,
|
|||
static const char *detach_sql =
|
||||
"UPDATE evidence_entity_observations SET entity_id=NULL,"
|
||||
"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 =
|
||||
"SELECT "
|
||||
"(SELECT COUNT(*) FROM evidence_entity_observations WHERE entity_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 tag_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);";
|
||||
static const char *delete_sql = "DELETE FROM entites WHERE id=?;";
|
||||
DatabaseStatement *statement = NULL;
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
char *evidence_identifier = NULL, *entity_identifier = NULL;
|
||||
gint64 count = 0; gboolean active = FALSE, success = FALSE;
|
||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||
|
|
@ -253,6 +248,8 @@ gboolean eml_integration_remove_promotion(Database *database,
|
|||
return FALSE;
|
||||
if (!database_transaction_begin(database)) return FALSE;
|
||||
active = TRUE;
|
||||
link_dao = evidence_entity_dao_new(database, error);
|
||||
if (link_dao == NULL) goto cleanup;
|
||||
statement = database_statement_prepare(database, read_sql);
|
||||
if (statement == NULL ||
|
||||
!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)
|
||||
goto cleanup;
|
||||
database_statement_finalize(statement); statement = NULL;
|
||||
statement = database_statement_prepare(database, same_evidence_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_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 (!evidence_entity_dao_remove_source(link_dao, evidence_identifier,
|
||||
entity_identifier, "eml_observation", observation_identifier,
|
||||
NULL, error)) goto cleanup;
|
||||
if (!eml_integration_read_count(database, dependency_sql,
|
||||
entity_identifier, &count)) goto cleanup;
|
||||
if (count == 0)
|
||||
|
|
@ -311,6 +293,7 @@ gboolean eml_integration_remove_promotion(Database *database,
|
|||
cleanup:
|
||||
database_statement_finalize(statement);
|
||||
if (!success && active) database_transaction_rollback(database);
|
||||
evidence_entity_dao_free(link_dao);
|
||||
g_free(evidence_identifier); g_free(entity_identifier);
|
||||
return success;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,7 +237,6 @@ static gboolean extraction_drop_service_attach_in_transaction(
|
|||
{
|
||||
EvidenceEntityDao *link_dao = NULL;
|
||||
char *evidence_identifier = NULL;
|
||||
gboolean exists = FALSE;
|
||||
gboolean success = 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;
|
||||
link_dao = evidence_entity_dao_new(database, error);
|
||||
if (link_dao == NULL ||
|
||||
!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
||||
entity_identifier, &exists, error) ||
|
||||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||
entity_identifier, error))) goto cleanup;
|
||||
!evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||
entity_identifier, error)) goto cleanup;
|
||||
success = TRUE;
|
||||
cleanup:
|
||||
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.
|
||||
*/
|
||||
static const char *const evidence_entity_dao_link_sql =
|
||||
"INSERT INTO preuve_entites"
|
||||
"INSERT OR IGNORE INTO preuve_entites"
|
||||
"("
|
||||
" preuve_id,"
|
||||
" entite_id"
|
||||
|
|
@ -71,6 +71,36 @@ static const char *const evidence_entity_dao_unlink_sql =
|
|||
"WHERE preuve_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.
|
||||
*/
|
||||
|
|
@ -801,8 +831,9 @@ gboolean evidence_entity_dao_link(
|
|||
gboolean entity_exists =
|
||||
FALSE;
|
||||
|
||||
gboolean association_exists =
|
||||
FALSE;
|
||||
GDateTime *now = NULL;
|
||||
char *created_at = NULL;
|
||||
gboolean success = FALSE;
|
||||
|
||||
g_return_val_if_fail(
|
||||
error == NULL || *error == NULL,
|
||||
|
|
@ -865,39 +896,20 @@ gboolean evidence_entity_dao_link(
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
if (!evidence_entity_dao_query_exists(
|
||||
evidence_entity_dao,
|
||||
evidence_entity_dao_association_exists_sql,
|
||||
evidence_identifier,
|
||||
entity_identifier,
|
||||
&association_exists,
|
||||
error
|
||||
))
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (association_exists)
|
||||
{
|
||||
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
|
||||
);
|
||||
now = g_date_time_new_now_utc();
|
||||
created_at = now != NULL
|
||||
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||
if (created_at == NULL)
|
||||
evidence_entity_dao_set_error_literal(error,
|
||||
EVIDENCE_ENTITY_DAO_ERROR_MEMORY,
|
||||
"Impossible de dater l'association preuve-entité.");
|
||||
else
|
||||
success = evidence_entity_dao_add_source(evidence_entity_dao,
|
||||
evidence_identifier, entity_identifier, "manual", NULL,
|
||||
created_at, error);
|
||||
g_free(created_at);
|
||||
g_clear_pointer(&now, g_date_time_unref);
|
||||
return success;
|
||||
}
|
||||
|
||||
gboolean evidence_entity_dao_unlink(
|
||||
|
|
@ -948,16 +960,103 @@ gboolean evidence_entity_dao_unlink(
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
return evidence_entity_dao_execute_pair_statement(
|
||||
evidence_entity_dao,
|
||||
evidence_entity_dao_unlink_sql,
|
||||
evidence_identifier,
|
||||
entity_identifier,
|
||||
"Impossible de préparer la suppression de l'association",
|
||||
"Impossible de lier les identifiants de l'association",
|
||||
"Impossible de supprimer l'association",
|
||||
error
|
||||
);
|
||||
return evidence_entity_dao_remove_source(evidence_entity_dao,
|
||||
evidence_identifier, entity_identifier, "manual", NULL, NULL, 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)
|
||||
{
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
/**
|
||||
* @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.
|
||||
*/
|
||||
#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.
|
||||
|
|
@ -842,6 +842,21 @@ rollback:
|
|||
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.
|
||||
*/
|
||||
|
|
@ -1079,6 +1094,10 @@ bool database_migrate_to_latest(
|
|||
if (!database_migrate_v11_to_v12(database)) return false;
|
||||
schema_version = 12;
|
||||
break;
|
||||
case 12:
|
||||
if (!database_migrate_v12_to_v13(database)) return false;
|
||||
schema_version = 13;
|
||||
break;
|
||||
|
||||
default:
|
||||
database_set_error(
|
||||
|
|
|
|||
|
|
@ -365,6 +365,12 @@ bool schema_install_v12(Database *database)
|
|||
"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(
|
||||
Database *database
|
||||
)
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
|||
"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, "relation_types");
|
||||
test_database_assert_table_exists(database, "graph_viewport");
|
||||
|
|
@ -992,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
|
|||
assert(
|
||||
strcmp(
|
||||
schema_version,
|
||||
"12"
|
||||
"13"
|
||||
) == 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)
|
||||
{
|
||||
test_database_initialize_valid_database();
|
||||
|
|
@ -1406,6 +1454,7 @@ int main(void)
|
|||
|
||||
test_database_migrate_v1_to_v2();
|
||||
test_database_migration_rollback();
|
||||
test_database_migrate_v12_to_v13_preserves_legacy_link();
|
||||
|
||||
test_database_initialize_invalid_parameters();
|
||||
test_database_initialize_missing_parent();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "core/eml_integration.h"
|
||||
#include "dao/evidence_entity_dao.h"
|
||||
#include "database/database.h"
|
||||
#include "database/statement.h"
|
||||
#include <assert.h>
|
||||
|
|
@ -38,6 +39,20 @@ static char *read_observation_identifier(Database *database, const char *role)
|
|||
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)
|
||||
{
|
||||
GError *error = NULL;
|
||||
|
|
@ -82,13 +97,25 @@ int main(void)
|
|||
&observations, &created, &reused, &error));
|
||||
assert(created == 0 && reused == 1 && count_rows(database, "entites") == 1);
|
||||
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;
|
||||
assert(eml_integration_remove_promotion(database, from_observation,
|
||||
&deleted, &shared, &error));
|
||||
assert(deleted && !shared);
|
||||
assert(!deleted && shared);
|
||||
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);
|
||||
execute_done(database, "DELETE FROM entites;");
|
||||
evidence_entity_dao_free(link_dao);
|
||||
g_free(from_entity);
|
||||
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||
&observations, &created, &reused, &error));
|
||||
assert(created == 1);
|
||||
|
|
@ -128,6 +155,42 @@ int main(void)
|
|||
assert(!deleted && shared);
|
||||
assert(count_rows(database, "relations") == 1);
|
||||
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(from_observation);
|
||||
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)
|
||||
{
|
||||
|
|
@ -637,7 +637,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
|||
assert(error == NULL);
|
||||
|
||||
assert(
|
||||
!evidence_entity_dao_link(
|
||||
evidence_entity_dao_link(
|
||||
fixture.evidence_entity_dao,
|
||||
evidence_identifier,
|
||||
entity_identifier,
|
||||
|
|
@ -645,10 +645,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
|||
)
|
||||
);
|
||||
|
||||
test_evidence_entity_dao_assert_error(
|
||||
error,
|
||||
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT
|
||||
);
|
||||
assert(error == NULL);
|
||||
|
||||
assert(
|
||||
test_evidence_entity_dao_count_rows(
|
||||
|
|
@ -656,10 +653,6 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
|||
) == 1
|
||||
);
|
||||
|
||||
g_clear_error(
|
||||
&error
|
||||
);
|
||||
|
||||
test_evidence_entity_dao_fixture_clear(
|
||||
&fixture
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue