feat(relations): centraliser les types et améliorer les aperçus

This commit is contained in:
grayTerminal-sh 2026-07-24 12:53:31 +02:00
parent 8bc3b43d63
commit 613d2096bc
28 changed files with 1826 additions and 18 deletions

1
.gitignore vendored
View file

@ -16,3 +16,4 @@ labfy-investigation
# Neovim / LSP # Neovim / LSP
.cache/ .cache/
compile_commands.json compile_commands.json
/AGENTS.md

View file

@ -2,6 +2,9 @@
### Added ### Added
- Préparation du pivot e-mail : pipeline EML asynchrone, extraction MIME
sécurisée, propositions bancaires IBAN/BIC et vocabulaire contrôlé.
- Référentiel persistant et normalisé des types de relations, avec codes - Référentiel persistant et normalisé des types de relations, avec codes
système stables, types personnalisés, renommage et fusion transactionnelle. système stables, types personnalisés, renommage et fusion transactionnelle.
- Sélecteur canonique dans les formulaires de création et de modification des - Sélecteur canonique dans les formulaires de création et de modification des

View file

@ -132,9 +132,36 @@ TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service
TEST_RELATION_TYPE_NORMALIZER := tests/test_relation_type_normalizer TEST_RELATION_TYPE_NORMALIZER := tests/test_relation_type_normalizer
TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service
TEST_CONTROLLED_VOCAB := tests/test_controlled_vocab
TEST_BANK_PROPOSAL := tests/test_bank_proposal
TEST_EML_PIPELINE_TASK := tests/test_eml_pipeline_task
all: $(TARGET) all: $(TARGET)
$(TEST_BANK_PROPOSAL): \
tests/test_bank_proposal.c \
src/core/bank_proposal.c \
src/core/controlled_vocab.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(shell $(PKG_CONFIG) --libs glib-2.0)
$(TEST_CONTROLLED_VOCAB): \
tests/test_controlled_vocab.c \
src/core/controlled_vocab.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(shell $(PKG_CONFIG) --libs glib-2.0)
$(TEST_EML_PIPELINE_TASK): \
tests/test_eml_pipeline_task.c \
src/core/eml_pipeline_task.c src/core/eml_mime_extractor.c \
src/core/eml_analyzer.c src/core/bank_proposal.c \
src/core/controlled_vocab.c src/core/iban_analyzer.c \
src/core/rib_ocr.c src/core/file_hash.c src/core/background_task.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(TEST_LDFLAGS) -lsqlite3
$(TEST_RELATION_TYPE_NORMALIZER): \ $(TEST_RELATION_TYPE_NORMALIZER): \
tests/test_relation_type_normalizer.c \ tests/test_relation_type_normalizer.c \
src/core/relation_type_normalizer.c src/core/relation_type_normalizer.c
@ -799,7 +826,10 @@ test: \
$(TEST_PDF_PASSWORD_RECOVERY) \ $(TEST_PDF_PASSWORD_RECOVERY) \
$(TEST_EXTRACTION_DROP_SERVICE) \ $(TEST_EXTRACTION_DROP_SERVICE) \
$(TEST_RELATION_TYPE_NORMALIZER) \ $(TEST_RELATION_TYPE_NORMALIZER) \
$(TEST_RELATION_TYPE_SERVICE) $(TEST_RELATION_TYPE_SERVICE) \
$(TEST_CONTROLLED_VOCAB) \
$(TEST_BANK_PROPOSAL) \
$(TEST_EML_PIPELINE_TASK)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -865,6 +895,9 @@ test: \
@$(TEST_EXTRACTION_DROP_SERVICE) @$(TEST_EXTRACTION_DROP_SERVICE)
@$(TEST_RELATION_TYPE_NORMALIZER) @$(TEST_RELATION_TYPE_NORMALIZER)
@$(TEST_RELATION_TYPE_SERVICE) @$(TEST_RELATION_TYPE_SERVICE)
@$(TEST_CONTROLLED_VOCAB)
@$(TEST_BANK_PROPOSAL)
@$(TEST_EML_PIPELINE_TASK)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -933,7 +966,12 @@ clean:
$(TEST_EML_ANALYZER) \ $(TEST_EML_ANALYZER) \
$(TEST_EXTRACTION_DROP_SERVICE) \ $(TEST_EXTRACTION_DROP_SERVICE) \
$(TEST_RELATION_TYPE_NORMALIZER) \ $(TEST_RELATION_TYPE_NORMALIZER) \
$(TEST_RELATION_TYPE_SERVICE) $(TEST_RELATION_TYPE_SERVICE) \
$(TEST_CONTROLLED_VOCAB) \
$(TEST_BANK_PROPOSAL) \
$(TEST_EML_PIPELINE_TASK)
-include $(DEP) -include $(DEP)

View file

@ -35,6 +35,10 @@ Labfy Investigation doit permettre de :
- exécuter des traitements longs en arrière-plan ; - exécuter des traitements longs en arrière-plan ;
- intégrer progressivement des outils OSINT externes ; - intégrer progressivement des outils OSINT externes ;
- conserver les sorties brutes, les versions et les paramètres dexécution ; - conserver les sorties brutes, les versions et les paramètres dexécution ;
- analyser localement des messages EML et inventorier leurs pièces jointes
sans modifier la preuve originale ;
- conserver les propositions IBAN/BIC avec leur provenance et leur statut de
vérification ;
- distinguer les faits observés, les résultats doutils, les corrélations et les hypothèses ; - distinguer les faits observés, les résultats doutils, les corrélations et les hypothèses ;
- produire des rapports compréhensibles et traçables. - produire des rapports compréhensibles et traçables.

View file

@ -138,3 +138,42 @@ FOR EACH ROW
BEGIN BEGIN
DELETE FROM graph_layout_positions WHERE node_id = OLD.id; DELETE FROM graph_layout_positions WHERE node_id = OLD.id;
END; END;
CREATE TABLE IF NOT EXISTS bank_account_entities
(
id TEXT PRIMARY KEY,
iban TEXT NOT NULL,
bic TEXT,
holder_name TEXT,
bank_name TEXT,
bank_address TEXT,
country_code TEXT,
bank_code TEXT,
branch_code TEXT,
account_number TEXT,
rib_key TEXT,
verification_status TEXT NOT NULL DEFAULT 'proposed' CHECK (verification_status IN ('proposed', 'confirmed', 'rejected', 'conflicted', 'invalid')),
provenance_kind TEXT NOT NULL DEFAULT 'ocr' CHECK (provenance_kind IN ('observed', 'ocr', 'header', 'metadata', 'derived', 'manual')),
evidence_id TEXT,
extraction_id TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL,
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL,
CHECK (length(trim(id)) > 0),
CHECK (length(trim(iban)) > 0)
);
CREATE INDEX IF NOT EXISTS idx_bank_account_entities_iban ON bank_account_entities(iban);
CREATE INDEX IF NOT EXISTS idx_bank_account_entities_evidence ON bank_account_entities(evidence_id);
INSERT OR IGNORE INTO relation_types(code, label, normalized_key, description, is_system) VALUES
('sent_from', 'Envoyé depuis', 'envoyé depuis', 'Message e-mail envoyé depuis une adresse ou serveur.', 1),
('sent_to', 'Envoyé à', 'envoyé à', 'Message e-mail envoyé à une adresse.', 1),
('reply_to', 'Répondre à', 'répondre à', 'Adresse de réponse configurée.', 1),
('has_attachment', 'Possède la pièce jointe', 'possède la pièce jointe', 'Preuve ou fichier joint à un e-mail.', 1),
('relayed_by', 'Relayé par', 'relayé par', 'Relais SMTP ayant acheminé le message.', 1),
('uses_domain', 'Utilise le domaine', 'utilise le domaine', 'Adresse e-mail rattachée à un domaine.', 1),
('held_at', 'Tenu auprès de', 'tenu auprès de', 'Compte bancaire ouvert dans une banque.', 1),
('named_as_holder_of', 'Nommé titulaire de', 'nommé titulaire de', 'Personne ou entité observée comme titulaire du RIB.', 1),
('supports', 'Soutient', 'soutient', 'Preuve soutenant une entité ou relation.', 1);

45
database/schema_v10.sql Normal file
View file

@ -0,0 +1,45 @@
/******************************************************************************
* Labfy Investigation
*
* Schéma SQLite officiel V10
* Extension pour le pivot e-mail, les entités bancaires et la traçabilité.
******************************************************************************/
CREATE TABLE IF NOT EXISTS bank_account_entities
(
id TEXT PRIMARY KEY,
iban TEXT NOT NULL,
bic TEXT,
holder_name TEXT,
bank_name TEXT,
bank_address TEXT,
country_code TEXT,
bank_code TEXT,
branch_code TEXT,
account_number TEXT,
rib_key TEXT,
verification_status TEXT NOT NULL DEFAULT 'proposed' CHECK (verification_status IN ('proposed', 'confirmed', 'rejected', 'conflicted', 'invalid')),
provenance_kind TEXT NOT NULL DEFAULT 'ocr' CHECK (provenance_kind IN ('observed', 'ocr', 'header', 'metadata', 'derived', 'manual')),
evidence_id TEXT,
extraction_id TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL,
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL,
CHECK (length(trim(id)) > 0),
CHECK (length(trim(iban)) > 0)
);
CREATE INDEX IF NOT EXISTS idx_bank_account_entities_iban ON bank_account_entities(iban);
CREATE INDEX IF NOT EXISTS idx_bank_account_entities_evidence ON bank_account_entities(evidence_id);
INSERT OR IGNORE INTO relation_types(code, label, normalized_key, description, is_system) VALUES
('sent_from', 'Envoyé depuis', 'envoyé depuis', 'Message e-mail envoyé depuis une adresse ou serveur.', 1),
('sent_to', 'Envoyé à', 'envoyé à', 'Message e-mail envoyé à une adresse.', 1),
('reply_to', 'Répondre à', 'répondre à', 'Adresse de réponse configurée.', 1),
('has_attachment', 'Possède la pièce jointe', 'possède la pièce jointe', 'Preuve ou fichier joint à un e-mail.', 1),
('relayed_by', 'Relayé par', 'relayé par', 'Relais SMTP ayant acheminé le message.', 1),
('uses_domain', 'Utilise le domaine', 'utilise le domaine', 'Adresse e-mail rattachée à un domaine.', 1),
('held_at', 'Tenu auprès de', 'tenu auprès de', 'Compte bancaire ouvert dans une banque.', 1),
('named_as_holder_of', 'Nommé titulaire de', 'nommé titulaire de', 'Personne ou entité observée comme titulaire du RIB.', 1),
('supports', 'Soutient', 'soutient', 'Preuve soutenant une entité ou relation.', 1);

View file

@ -2392,6 +2392,19 @@ maintenable et pérenne.
--- ---
## Pivot e-mail et comptes bancaires
La version V10 ajoute `bank_account_entities`, qui conserve les composants
IBAN/RIB ainsi que leur statut de vérification et leur provenance. Une valeur
extraite par OCR reste une proposition (`proposed`) ; une dérivation locale
est marquée `derived` et ne devient jamais automatiquement une donnée
confirmée. Les fichiers EML dérivés restent reliés à leur preuve source par
les tables dassociation existantes.
Les codes de statuts, de provenance, de rôles dadresse et de type de valeur
sont fournis par `controlled_vocab` : les codes techniques sont stables et
les libellés affichés sont séparés.
## Références ## Références
Documents associés : Documents associés :

View file

@ -0,0 +1,73 @@
/******************************************************************************
* @file bank_proposal.h
* @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC).
******************************************************************************/
#ifndef LABFY_INVESTIGATION_BANK_PROPOSAL_H
#define LABFY_INVESTIGATION_BANK_PROPOSAL_H
#include "core/controlled_vocab.h"
#include <glib.h>
G_BEGIN_DECLS
/** @brief Objet métier représentant une proposition bancaire complète. */
typedef struct BankProposal
{
char *id; /**< UUID de la proposition */
char *raw_iban; /**< Graphie IBAN brute lue/OCR */
char *normalized_iban; /**< IBAN nettoyé et majuscule */
char *bic; /**< BIC / SWIFT (8 ou 11 car) */
char *holder_name; /**< Titulaire du compte */
char *bank_name; /**< Nom de la banque */
char *bank_address; /**< Adresse de la banque */
char *country_code; /**< Code pays (ex: "FR") */
char *bank_code; /**< Code banque (5 ch pour RIB FR) */
char *branch_code; /**< Code guichet (5 ch pour RIB FR) */
char *account_number; /**< Numéro de compte (11 car) */
char *rib_key; /**< Clé RIB (2 ch) */
gboolean is_iban_valid; /**< VRAI si MOD-97 et format valides */
gboolean is_derived_bban; /**< VRAI si composants dérivés de l'IBAN */
char *suggested_ocr_fix; /**< Proposition de correction OCR (ex: "O->0") */
char *verification_status; /**< Code contrôlé: proposed, confirmed, rejected, etc. */
char *provenance_kind; /**< Code contrôlé: ocr, observed, derived, etc. */
char *evidence_id; /**< UUID de la preuve source */
char *extraction_id; /**< UUID de l'extraction source */
char *created_at; /**< Horodatage ISO 8601 UTC */
char *updated_at; /**< Horodatage ISO 8601 UTC */
} BankProposal;
/** @brief Libère une structure BankProposal. */
void bank_proposal_free(BankProposal *proposal);
/**
* @brief Crée une proposition bancaire depuis une chaîne IBAN ou un texte OCR.
* @param raw_text Texte brut observé / OCR.
* @param evidence_id UUID de la preuve source (facultatif).
* @return Nouvelle BankProposal, ou NULL.
*/
BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evidence_id);
/**
* @brief Valide la structure MOD-97 d'un IBAN.
* @param iban IBAN nettoyé (sans espaces).
* @return VRAI si valide, FALSE sinon.
*/
gboolean bank_proposal_validate_iban(const char *iban);
/**
* @brief Tente de dériver les composants BBAN (RIB) pour un IBAN français valide.
* @param proposal Proposition bancaire à enrichir.
* @return VRAI si dérivation réussie, FALSE sinon.
*/
gboolean bank_proposal_derive_french_rib(BankProposal *proposal);
/**
* @brief Valide la structure d'un code BIC/SWIFT (8 ou 11 caractères).
* @param bic Code BIC à vérifier.
* @return VRAI si valide, FALSE sinon.
*/
gboolean bank_proposal_validate_bic(const char *bic);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_BANK_PROPOSAL_H */

View file

@ -0,0 +1,67 @@
/******************************************************************************
* @file controlled_vocab.h
* @brief Gestion du vocabulaire contrôlé pour les domaines fermés.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_CONTROLLED_VOCAB_H
#define LABFY_INVESTIGATION_CONTROLLED_VOCAB_H
#include <glib.h>
#include <stddef.h>
G_BEGIN_DECLS
/** @brief Élément d'un vocabulaire contrôlé. */
typedef struct ControlledVocabItem
{
const char *code; /**< Code technique stable en anglais */
const char *label; /**< Libellé affichable en français */
} ControlledVocabItem;
/** @brief Catégories de vocabulaire contrôlé. */
typedef enum ControlledVocabCategory
{
CONTROLLED_VOCAB_VERIFICATION_STATUS = 0,
CONTROLLED_VOCAB_PROVENANCE_KIND,
CONTROLLED_VOCAB_EMAIL_ROLE,
CONTROLLED_VOCAB_SMTP_ROLE,
CONTROLLED_VOCAB_VALUE_TYPE,
CONTROLLED_VOCAB_COUNT
} ControlledVocabCategory;
/**
* @brief Retourne la liste ordonnée d'un vocabulaire contrôlé.
* @param category Catégorie demandée.
* @param out_count Pointeur recevant le nombre d'éléments.
* @return Tableau d'éléments statiques, ou NULL si catégorie invalide.
*/
const ControlledVocabItem *controlled_vocab_get_items(ControlledVocabCategory category,
size_t *out_count);
/**
* @brief Vérifie si un code appartient à une catégorie de vocabulaire.
*/
gboolean controlled_vocab_is_valid_code(ControlledVocabCategory category,
const char *code);
/**
* @brief Obtient le libellé utilisateur (français) à partir d'un code technique.
*/
const char *controlled_vocab_get_label(ControlledVocabCategory category,
const char *code);
/**
* @brief Obtient le code technique à partir d'un libellé utilisateur.
*/
const char *controlled_vocab_get_code_from_label(ControlledVocabCategory category,
const char *label);
/* Helpers spécifiques pour les catégories principales */
gboolean controlled_vocab_is_valid_verification_status(const char *code);
gboolean controlled_vocab_is_valid_provenance_kind(const char *code);
gboolean controlled_vocab_is_valid_email_role(const char *code);
gboolean controlled_vocab_is_valid_smtp_role(const char *code);
gboolean controlled_vocab_is_valid_value_type(const char *code);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_CONTROLLED_VOCAB_H */

View file

@ -8,10 +8,18 @@
#include <glib.h> #include <glib.h>
G_BEGIN_DECLS G_BEGIN_DECLS
/** @brief Proposition d'entité explicitement sélectionnable. */ /** @brief Proposition d'entité explicitement sélectionnable. */
typedef struct { char *type_identifier; char *value; } EmlEntityProposal; typedef struct {
char *type_identifier;
char *value;
char *verification_status;
char *provenance_kind;
} EmlEntityProposal;
/** @brief Crée une proposition possédée. */ /** @brief Crée une proposition possédée. */
EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier, EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
const char *value); const char *value);
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
const char *type_identifier, const char *value,
const char *verification_status, const char *provenance_kind);
/** @brief Libère une proposition. */ /** @brief Libère une proposition. */
void eml_entity_proposal_free(EmlEntityProposal *proposal); void eml_entity_proposal_free(EmlEntityProposal *proposal);
/** /**

View file

@ -0,0 +1,68 @@
/******************************************************************************
* @file eml_mime_extractor.h
* @brief Extraction MIME sécurisée et inventaire des pièces jointes d'un EML.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#include <glib.h>
G_BEGIN_DECLS
/** @brief Représentation d'une pièce jointe extraite d'un message EML. */
typedef struct EmlAttachment
{
char *part_index; /**< Chemin/index MIME (ex: "1.2") */
char *declared_filename; /**< Nom de fichier d'origine */
char *sanitized_filename; /**< Nom assaini (anti path-traversal) */
char *extracted_path; /**< Chemin absolu dans 02_Preuves_Traitees */
char *relative_path; /**< Chemin relatif par rapport à la racine d'enquête */
char *content_type; /**< Type MIME déclaré */
char *detected_mime; /**< Type MIME détecté */
char *content_id; /**< Content-ID pour les images/pièces inline */
char *transfer_encoding; /**< Content-Transfer-Encoding */
gboolean is_inline; /**< VRAI si disposition inline */
gsize encoded_size; /**< Taille encodée */
gsize decoded_size; /**< Taille décodée */
char *sha256; /**< Empreinte SHA-256 du fichier extrait */
gboolean has_inconsistency; /**< VRAI si incohérence extension/MIME */
} EmlAttachment;
/** @brief Résultat de l'extraction MIME d'un fichier EML. */
typedef struct EmlMimeResult
{
GPtrArray *attachments; /**< Tableau de EmlAttachment* */
GPtrArray *warnings; /**< Avertissements d'extraction */
} EmlMimeResult;
/**
* @brief Libère une structure EmlAttachment.
*/
void eml_attachment_free(EmlAttachment *attachment);
/**
* @brief Libère une structure EmlMimeResult.
*/
void eml_mime_result_free(EmlMimeResult *result);
/**
* @brief Assainit un nom de fichier pour éviter les attaques par traversée de chemin.
* @param raw_filename Nom d'origine.
* @return Nom assaini libéré avec g_free.
*/
char *eml_mime_sanitize_filename(const char *raw_filename);
/**
* @brief Extrait et enregistre de manière sécurisée les pièces jointes d'un fichier EML.
* @param eml_path Chemin du fichier EML source.
* @param target_dir Dossier de destination dans 02_Preuves_Traitees (ex: .../02_Preuves_Traitees/eml_attachments/<hash>).
* @param error Destination d'erreur facultative.
* @return Résultat MIME, ou NULL en cas d'erreur.
*/
EmlMimeResult *eml_mime_extract_attachments(const char *eml_path,
const char *target_dir,
GError **error);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */

View file

@ -0,0 +1,40 @@
/******************************************************************************
* @file eml_pipeline_task.h
* @brief Pipeline d'analyse asynchrone complète pour fichier EML (En-têtes, MIME, OCR, Banque).
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_PIPELINE_TASK_H
#define LABFY_INVESTIGATION_EML_PIPELINE_TASK_H
#include "core/background_task.h"
#include "core/bank_proposal.h"
#include "core/eml_analyzer.h"
#include "core/eml_mime_extractor.h"
G_BEGIN_DECLS
/** @brief Résultat global d'un pipeline d'analyse EML. */
typedef struct EmlPipelineResult
{
EmlAnalysis *analysis; /**< Analyse des en-têtes EML */
EmlMimeResult *mime_result; /**< Pièces jointes extraites */
GPtrArray *bank_proposals; /**< Tableau de BankProposal* */
GPtrArray *warnings; /**< Avertissements globaux */
} EmlPipelineResult;
/** @brief Libère un résultat EmlPipelineResult. */
void eml_pipeline_result_free(EmlPipelineResult *result);
/**
* @brief Crée une nouvelle tâche asynchrone d'analyse EML complète.
* @param eml_path Chemin du fichier .eml source.
* @param processed_evidence_dir Dossier racine 02_Preuves_Traitees.
* @param evidence_id UUID de la preuve EML.
* @return Nouvelle BackgroundTask, ou NULL.
*/
BackgroundTask *eml_pipeline_task_new(const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_EML_PIPELINE_TASK_H */

View file

@ -90,6 +90,7 @@ bool schema_install_v6(Database *database);
bool schema_install_v7(Database *database); bool schema_install_v7(Database *database);
bool schema_install_v8(Database *database); bool schema_install_v8(Database *database);
bool schema_install_v9(Database *database); bool schema_install_v9(Database *database);
bool schema_install_v10(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.

View file

@ -0,0 +1,40 @@
/******************************************************************************
* @file controlled_vocab_dropdown.h
* @brief Composant GTK4 (GtkDropDown) basé sur le vocabulaire contrôlé.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H
#define LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H
#include "core/controlled_vocab.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
/**
* @brief Crée un widget GtkDropDown initialisé avec une catégorie de vocabulaire contrôlé.
* @param category Catégorie de vocabulaire.
* @param default_code Code sélectionné par défaut (facultatif).
* @return Nouveau GtkWidget (GtkDropDown).
*/
GtkWidget *controlled_vocab_dropdown_new(ControlledVocabCategory category,
const char *default_code);
/**
* @brief Obtient le code technique sélectionné dans le dropdown.
* @param dropdown Widget GtkDropDown créé avec controlled_vocab_dropdown_new.
* @return Code technique emprunté, ou NULL si rien n'est sélectionné.
*/
const char *controlled_vocab_dropdown_get_selected_code(GtkWidget *dropdown);
/**
* @brief Sélectionne un code technique dans le dropdown.
* @param dropdown Widget GtkDropDown.
* @param code Code à sélectionner.
* @return TRUE si le code a é trouvé et sélectionné, FALSE sinon.
*/
gboolean controlled_vocab_dropdown_set_selected_code(GtkWidget *dropdown,
const char *code);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_CONTROLLED_VOCAB_DROPDOWN_H */

202
src/core/bank_proposal.c Normal file
View file

@ -0,0 +1,202 @@
/******************************************************************************
* @file bank_proposal.c
* @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC).
******************************************************************************/
#include "core/bank_proposal.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void bank_proposal_free(BankProposal *p)
{
if (p == NULL)
return;
g_free(p->id);
g_free(p->raw_iban);
g_free(p->normalized_iban);
g_free(p->bic);
g_free(p->holder_name);
g_free(p->bank_name);
g_free(p->bank_address);
g_free(p->country_code);
g_free(p->bank_code);
g_free(p->branch_code);
g_free(p->account_number);
g_free(p->rib_key);
g_free(p->suggested_ocr_fix);
g_free(p->verification_status);
g_free(p->provenance_kind);
g_free(p->evidence_id);
g_free(p->extraction_id);
g_free(p->created_at);
g_free(p->updated_at);
g_free(p);
}
gboolean bank_proposal_validate_iban(const char *iban)
{
if (iban == NULL)
return FALSE;
gsize len = strlen(iban);
if (len < 15 || len > 34)
return FALSE;
/* Vérification des 2 premières lettres (Code pays) */
if (!g_ascii_isalpha(iban[0]) || !g_ascii_isalpha(iban[1]))
return FALSE;
/* Repositionnement des 4 premiers caractères à la fin */
GString *rearranged = g_string_new(iban + 4);
g_string_append_len(rearranged, iban, 4);
/* Conversion des lettres en chiffres (A=10, Z=35) */
GString *numeric = g_string_new(NULL);
for (gsize i = 0; i < rearranged->len; i++)
{
char c = rearranged->str[i];
if (g_ascii_isalpha(c))
{
int val = g_ascii_toupper(c) - 'A' + 10;
g_string_append_printf(numeric, "%d", val);
}
else if (g_ascii_isdigit(c))
{
g_string_append_c(numeric, c);
}
else
{
g_string_free(numeric, TRUE);
g_string_free(rearranged, TRUE);
return FALSE;
}
}
g_string_free(rearranged, TRUE);
/* Calcul du modulo 97 par blocs */
guint remainder = 0;
for (gsize i = 0; i < numeric->len; i++)
{
int digit = numeric->str[i] - '0';
remainder = (remainder * 10 + (guint)digit) % 97;
}
g_string_free(numeric, TRUE);
return (remainder == 1);
}
gboolean bank_proposal_validate_bic(const char *bic)
{
if (bic == NULL)
return FALSE;
gsize len = strlen(bic);
if (len != 8 && len != 11)
return FALSE;
for (gsize i = 0; i < 4; i++)
{
if (!g_ascii_isalpha(bic[i]))
return FALSE;
}
for (gsize i = 4; i < 6; i++)
{
if (!g_ascii_isalpha(bic[i]))
return FALSE;
}
for (gsize i = 6; i < len; i++)
{
if (!g_ascii_isalnum(bic[i]))
return FALSE;
}
return TRUE;
}
gboolean bank_proposal_derive_french_rib(BankProposal *proposal)
{
if (proposal == NULL || proposal->normalized_iban == NULL)
return FALSE;
/* Vérification d'un IBAN français : "FR76..." (27 caractères) */
if (strlen(proposal->normalized_iban) != 27 ||
g_ascii_strncasecmp(proposal->normalized_iban, "FR", 2) != 0)
{
return FALSE;
}
if (!proposal->is_iban_valid)
return FALSE;
proposal->bank_code = g_strndup(proposal->normalized_iban + 4, 5);
proposal->branch_code = g_strndup(proposal->normalized_iban + 9, 5);
proposal->account_number = g_strndup(proposal->normalized_iban + 14, 11);
proposal->rib_key = g_strndup(proposal->normalized_iban + 25, 2);
proposal->is_derived_bban = TRUE;
return TRUE;
}
BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evidence_id)
{
if (raw_text == NULL || raw_text[0] == '\0')
return NULL;
/* Nettoyage des espaces pour recherche d'IBAN */
GString *clean = g_string_new(NULL);
gsize raw_len = strlen(raw_text);
for (gsize i = 0; i < raw_len; i++)
{
char c = raw_text[i];
if (g_ascii_isalnum(c))
{
g_string_append_c(clean, g_ascii_toupper(c));
}
}
/* Recherche de motif IBAN (ex: FR76...) */
const char *data = clean->str;
const char *iban_start = strstr(data, "FR");
if (iban_start == NULL)
{
/* Essai avec d'autres codes pays à 2 lettres */
if (clean->len >= 15 && g_ascii_isalpha(data[0]) && g_ascii_isalpha(data[1]))
iban_start = data;
}
if (iban_start == NULL)
{
g_string_free(clean, TRUE);
return NULL;
}
BankProposal *p = g_new0(BankProposal, 1);
p->id = g_uuid_string_random();
p->raw_iban = g_strdup(raw_text);
p->normalized_iban = g_strndup(iban_start, 27 < strlen(iban_start) ? 27 : strlen(iban_start));
p->country_code = g_strndup(p->normalized_iban, 2);
p->is_iban_valid = bank_proposal_validate_iban(p->normalized_iban);
p->verification_status = g_strdup("proposed");
p->provenance_kind = g_strdup("ocr");
p->evidence_id = evidence_id != NULL ? g_strdup(evidence_id) : NULL;
/* Horodatage UTC courant */
GDateTime *now = g_date_time_new_now_utc();
p->created_at = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
p->updated_at = g_strdup(p->created_at);
g_date_time_unref(now);
/* Dérivation RIB si IBAN français */
if (g_ascii_strcasecmp(p->country_code, "FR") == 0)
{
bank_proposal_derive_french_rib(p);
}
g_string_free(clean, TRUE);
return p;
}

182
src/core/controlled_vocab.c Normal file
View file

@ -0,0 +1,182 @@
/******************************************************************************
* @file controlled_vocab.c
* @brief Implémentation du vocabulaire contrôlé pour les domaines fermés.
******************************************************************************/
#include "core/controlled_vocab.h"
#include <string.h>
static const ControlledVocabItem VERIFICATION_STATUSES[] = {
{ "proposed", "Proposé" },
{ "confirmed", "Confirmé" },
{ "rejected", "Rejeté" },
{ "conflicted", "Contradictoire" },
{ "invalid", "Invalide" }
};
static const size_t VERIFICATION_STATUSES_COUNT = sizeof(VERIFICATION_STATUSES) / sizeof(VERIFICATION_STATUSES[0]);
static const ControlledVocabItem PROVENANCE_KINDS[] = {
{ "observed", "Observé directement" },
{ "ocr", "Extrait par OCR" },
{ "header", "Extrait d'un en-tête" },
{ "metadata", "Extrait des métadonnées" },
{ "derived", "Dérivé localement" },
{ "manual", "Saisi manuellement" }
};
static const size_t PROVENANCE_KINDS_COUNT = sizeof(PROVENANCE_KINDS) / sizeof(PROVENANCE_KINDS[0]);
static const ControlledVocabItem EMAIL_ROLES[] = {
{ "from", "Expéditeur principal (From)" },
{ "sender", "Expéditeur réel (Sender)" },
{ "reply_to", "Adresse de réponse (Reply-To)" },
{ "return_path", "Adresse de retour (Return-Path)" },
{ "to", "Destinataire principal (To)" },
{ "cc", "Copie (Cc)" },
{ "bcc", "Copie cachée (Bcc)" },
{ "message_id_domain", "Domaine Message-ID" },
{ "other", "Autre rôle" }
};
static const size_t EMAIL_ROLES_COUNT = sizeof(EMAIL_ROLES) / sizeof(EMAIL_ROLES[0]);
static const ControlledVocabItem SMTP_ROLES[] = {
{ "declared_source", "Source déclarée" },
{ "smtp_relay", "Relais SMTP" },
{ "destination_server", "Serveur destinataire" },
{ "private_infrastructure","Infrastructure locale/privée" },
{ "unknown", "Rôle indéterminé" }
};
static const size_t SMTP_ROLES_COUNT = sizeof(SMTP_ROLES) / sizeof(SMTP_ROLES[0]);
static const ControlledVocabItem VALUE_TYPES[] = {
{ "text", "Texte" },
{ "identifier", "Identifiant" },
{ "email", "Adresse e-mail" },
{ "iban", "IBAN" },
{ "bic", "BIC / SWIFT" },
{ "ip_address", "Adresse IP" },
{ "domain", "Nom de domaine" },
{ "uri", "URI / URL" },
{ "integer", "Nombre entier" },
{ "decimal", "Nombre décimal" },
{ "date", "Date" },
{ "datetime", "Date et heure" },
{ "boolean", "Booléen" },
{ "json", "JSON" }
};
static const size_t VALUE_TYPES_COUNT = sizeof(VALUE_TYPES) / sizeof(VALUE_TYPES[0]);
const ControlledVocabItem *controlled_vocab_get_items(ControlledVocabCategory category,
size_t *out_count)
{
switch (category)
{
case CONTROLLED_VOCAB_VERIFICATION_STATUS:
if (out_count != NULL) *out_count = VERIFICATION_STATUSES_COUNT;
return VERIFICATION_STATUSES;
case CONTROLLED_VOCAB_PROVENANCE_KIND:
if (out_count != NULL) *out_count = PROVENANCE_KINDS_COUNT;
return PROVENANCE_KINDS;
case CONTROLLED_VOCAB_EMAIL_ROLE:
if (out_count != NULL) *out_count = EMAIL_ROLES_COUNT;
return EMAIL_ROLES;
case CONTROLLED_VOCAB_SMTP_ROLE:
if (out_count != NULL) *out_count = SMTP_ROLES_COUNT;
return SMTP_ROLES;
case CONTROLLED_VOCAB_VALUE_TYPE:
if (out_count != NULL) *out_count = VALUE_TYPES_COUNT;
return VALUE_TYPES;
default:
if (out_count != NULL) *out_count = 0;
return NULL;
}
}
gboolean controlled_vocab_is_valid_code(ControlledVocabCategory category,
const char *code)
{
size_t count = 0;
const ControlledVocabItem *items = NULL;
if (code == NULL || code[0] == '\0')
return FALSE;
items = controlled_vocab_get_items(category, &count);
if (items == NULL)
return FALSE;
for (size_t i = 0; i < count; i++)
{
if (g_ascii_strcasecmp(items[i].code, code) == 0)
return TRUE;
}
return FALSE;
}
const char *controlled_vocab_get_label(ControlledVocabCategory category,
const char *code)
{
size_t count = 0;
const ControlledVocabItem *items = NULL;
if (code == NULL || code[0] == '\0')
return NULL;
items = controlled_vocab_get_items(category, &count);
if (items == NULL)
return NULL;
for (size_t i = 0; i < count; i++)
{
if (g_ascii_strcasecmp(items[i].code, code) == 0)
return items[i].label;
}
return NULL;
}
const char *controlled_vocab_get_code_from_label(ControlledVocabCategory category,
const char *label)
{
size_t count = 0;
const ControlledVocabItem *items = NULL;
if (label == NULL || label[0] == '\0')
return NULL;
items = controlled_vocab_get_items(category, &count);
if (items == NULL)
return NULL;
for (size_t i = 0; i < count; i++)
{
if (g_ascii_strcasecmp(items[i].label, label) == 0)
return items[i].code;
}
return NULL;
}
gboolean controlled_vocab_is_valid_verification_status(const char *code)
{
return controlled_vocab_is_valid_code(CONTROLLED_VOCAB_VERIFICATION_STATUS, code);
}
gboolean controlled_vocab_is_valid_provenance_kind(const char *code)
{
return controlled_vocab_is_valid_code(CONTROLLED_VOCAB_PROVENANCE_KIND, code);
}
gboolean controlled_vocab_is_valid_email_role(const char *code)
{
return controlled_vocab_is_valid_code(CONTROLLED_VOCAB_EMAIL_ROLE, code);
}
gboolean controlled_vocab_is_valid_smtp_role(const char *code)
{
return controlled_vocab_is_valid_code(CONTROLLED_VOCAB_SMTP_ROLE, code);
}
gboolean controlled_vocab_is_valid_value_type(const char *code)
{
return controlled_vocab_is_valid_code(CONTROLLED_VOCAB_VALUE_TYPE, code);
}

View file

@ -3,25 +3,40 @@
* @brief Intégration transactionnelle de propositions issues d'un EML. * @brief Intégration transactionnelle de propositions issues d'un EML.
******************************************************************************/ ******************************************************************************/
#include "core/eml_integration.h" #include "core/eml_integration.h"
#include "core/controlled_vocab.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/evidence_entity_dao.h" #include "dao/evidence_entity_dao.h"
#include "database/transaction.h" #include "database/transaction.h"
#include "models/entity_record.h" #include "models/entity_record.h"
EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value) EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value)
{
return eml_entity_proposal_new_with_metadata(type, value, "proposed",
"header");
}
EmlEntityProposal *eml_entity_proposal_new_with_metadata(const char *type,
const char *value, const char *verification_status,
const char *provenance_kind)
{ {
EmlEntityProposal *proposal = NULL; EmlEntityProposal *proposal = NULL;
if (type == NULL || type[0] == '\0' || value == NULL || value[0] == '\0') return NULL; if (type == NULL || type[0] == '\0' || value == NULL || value[0] == '\0') return NULL;
proposal = g_new0(EmlEntityProposal, 1); proposal = g_new0(EmlEntityProposal, 1);
proposal->type_identifier = g_strdup(type); proposal->value = g_strdup(value); proposal->type_identifier = g_strdup(type); proposal->value = g_strdup(value);
if (proposal->type_identifier == NULL || proposal->value == NULL) proposal->verification_status = g_strdup(verification_status != NULL
? verification_status : "proposed");
proposal->provenance_kind = g_strdup(provenance_kind != NULL
? provenance_kind : "header");
if (proposal->type_identifier == NULL || proposal->value == NULL ||
proposal->verification_status == NULL || proposal->provenance_kind == NULL)
{ eml_entity_proposal_free(proposal); return NULL; } { eml_entity_proposal_free(proposal); return NULL; }
return proposal; return proposal;
} }
void eml_entity_proposal_free(EmlEntityProposal *proposal) void eml_entity_proposal_free(EmlEntityProposal *proposal)
{ {
if (proposal == NULL) return; if (proposal == NULL) return;
g_free(proposal->type_identifier); g_free(proposal->value); g_free(proposal); g_free(proposal->type_identifier); g_free(proposal->value);
g_free(proposal->verification_status); g_free(proposal->provenance_kind);
g_free(proposal);
} }
/** @brief Recherche une entité existante avec le même type et la même valeur. */ /** @brief Recherche une entité existante avec le même type et la même valeur. */
static const EntityRecord *eml_integration_find_existing(const GPtrArray *entities, static const EntityRecord *eml_integration_find_existing(const GPtrArray *entities,
@ -64,6 +79,30 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
"Sélectionnez au moins une proposition EML."); "Sélectionnez au moins une proposition EML.");
return FALSE; return FALSE;
} }
for (guint i = 0; i < proposals->len; i++)
{
const EmlEntityProposal *proposal = g_ptr_array_index(
(GPtrArray *) proposals, i);
if (proposal == NULL ||
!controlled_vocab_is_valid_verification_status(
proposal->verification_status) ||
!controlled_vocab_is_valid_provenance_kind(
proposal->provenance_kind))
{
g_set_error_literal(error,
g_quark_from_static_string("eml-integration-error"), 2,
"Le statut ou la provenance dune proposition est invalide.");
return FALSE;
}
if (g_strcmp0(proposal->verification_status, "rejected") == 0 ||
g_strcmp0(proposal->verification_status, "invalid") == 0)
{
g_set_error_literal(error,
g_quark_from_static_string("eml-integration-error"), 3,
"Une proposition rejetée ou invalide ne peut pas être intégrée.");
return FALSE;
}
}
if (!database_transaction_begin(database)) if (!database_transaction_begin(database))
return FALSE; return FALSE;
active = TRUE; active = TRUE;

View file

@ -0,0 +1,388 @@
/******************************************************************************
* @file eml_mime_extractor.c
* @brief Extraction MIME sécurisée et inventaire des pièces jointes d'un EML.
******************************************************************************/
#include "core/eml_mime_extractor.h"
#include "core/file_hash.h"
#include <gio/gio.h>
#include <glib.h>
#include <string.h>
#define EML_MIME_MAX_FILE_SIZE (50U * 1024U * 1024U)
#define EML_MIME_MAX_PARTS 100U
#define EML_MIME_MAX_DEPTH 10U
void eml_attachment_free(EmlAttachment *attachment)
{
if (attachment == NULL)
return;
g_free(attachment->part_index);
g_free(attachment->declared_filename);
g_free(attachment->sanitized_filename);
g_free(attachment->extracted_path);
g_free(attachment->relative_path);
g_free(attachment->content_type);
g_free(attachment->detected_mime);
g_free(attachment->content_id);
g_free(attachment->transfer_encoding);
g_free(attachment->sha256);
g_free(attachment);
}
void eml_mime_result_free(EmlMimeResult *result)
{
if (result == NULL)
return;
if (result->attachments != NULL)
g_ptr_array_unref(result->attachments);
if (result->warnings != NULL)
g_ptr_array_unref(result->warnings);
g_free(result);
}
char *eml_mime_sanitize_filename(const char *raw_filename)
{
if (raw_filename == NULL || raw_filename[0] == '\0')
return g_strdup("attachment.bin");
char *clean = g_strdup(raw_filename);
/* Décodage simple RFC 2047 si présent =?...?= */
if (strstr(clean, "=?") != NULL)
{
/* Traitement basique ou suppression de préfixe */
char *start = strstr(clean, "?B?");
if (start == NULL) start = strstr(clean, "?b?");
if (start != NULL)
{
char *end = strstr(start + 3, "?=");
if (end != NULL)
{
*end = '\0';
gsize out_len = 0;
guchar *decoded = g_base64_decode(start + 3, &out_len);
if (decoded != NULL && out_len > 0)
{
char *valid = g_utf8_make_valid((const char *) decoded, (gssize) out_len);
g_free(clean);
clean = valid;
g_free(decoded);
}
}
}
}
/* Remplacement des caractères dangereux ou des séparateurs de chemin */
gsize len = strlen(clean);
GString *sanitized = g_string_new_len(NULL, (gssize) len);
for (gsize i = 0; i < len; i++)
{
char c = clean[i];
if (c == '/' || c == '\\' || c == ':' || c == '\0' || c == '\r' || c == '\n' || c == '\t')
{
g_string_append_c(sanitized, '_');
}
else
{
g_string_append_c(sanitized, c);
}
}
g_free(clean);
/* Suppression des séquences '..' */
char *res = g_strdup(sanitized->str);
g_string_free(sanitized, TRUE);
while (strstr(res, "..") != NULL)
{
char *pos = strstr(res, "..");
pos[0] = '_';
pos[1] = '_';
}
g_strstrip(res);
if (res[0] == '\0' || strcmp(res, ".") == 0 || strcmp(res, "..") == 0)
{
g_free(res);
return g_strdup("attachment.bin");
}
return res;
}
static GBytes *decode_transfer_encoding(const char *encoding, const char *raw_data, gsize raw_len)
{
if (encoding != NULL && g_ascii_strcasecmp(encoding, "base64") == 0)
{
gsize out_len = 0;
guchar *decoded = g_base64_decode(raw_data, &out_len);
if (decoded != NULL)
{
return g_bytes_new_take(decoded, out_len);
}
}
if (encoding != NULL &&
g_ascii_strcasecmp(encoding, "quoted-printable") == 0)
{
GByteArray *decoded = g_byte_array_new();
for (gsize index = 0; index < raw_len; index++)
{
if (raw_data[index] == '=' && index + 2 < raw_len &&
raw_data[index + 1] == '\r' && raw_data[index + 2] == '\n')
{
index += 2;
continue;
}
if (raw_data[index] == '=' && index + 2 < raw_len &&
g_ascii_isxdigit(raw_data[index + 1]) &&
g_ascii_isxdigit(raw_data[index + 2]))
{
char hex[3] = { raw_data[index + 1], raw_data[index + 2], 0 };
guint8 value = (guint8) g_ascii_strtoll(hex, NULL, 16);
g_byte_array_append(decoded, &value, 1);
index += 2;
continue;
}
g_byte_array_append(decoded, (const guint8 *) &raw_data[index], 1);
}
return g_byte_array_free_to_bytes(decoded);
}
/* Traitement par défaut ou quoted-printable simple */
return g_bytes_new(raw_data, raw_len);
}
EmlMimeResult *eml_mime_extract_attachments(const char *eml_path,
const char *target_dir,
GError **error)
{
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (eml_path == NULL || eml_path[0] == '\0' || target_dir == NULL || target_dir[0] == '\0')
{
g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Les chemins de l'EML et du dossier cible doivent être valides.");
return NULL;
}
GMappedFile *mapped = g_mapped_file_new(eml_path, FALSE, error);
if (mapped == NULL)
return NULL;
gsize size = g_mapped_file_get_length(mapped);
const char *data = g_mapped_file_get_contents(mapped);
if (size == 0 || size > EML_MIME_MAX_FILE_SIZE)
{
g_mapped_file_unref(mapped);
g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Fichier EML vide ou trop volumineux.");
return NULL;
}
if (g_mkdir_with_parents(target_dir, 0755) != 0)
{
g_mapped_file_unref(mapped);
g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_ACCES,
"Impossible de créer le dossier de destination des pièces jointes.");
return NULL;
}
EmlMimeResult *result = g_new0(EmlMimeResult, 1);
result->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free);
result->warnings = g_ptr_array_new_with_free_func(g_free);
/* Détection de boundary MIME si multipart */
const char *boundary_key = "boundary=";
const char *b_pos = strstr(data, boundary_key);
char *boundary = NULL;
if (b_pos != NULL)
{
const char *b_start = b_pos + strlen(boundary_key);
if (*b_start == '"')
{
b_start++;
const char *b_end = strchr(b_start, '"');
if (b_end != NULL)
boundary = g_strndup(b_start, (gsize)(b_end - b_start));
}
else
{
const char *b_end = b_start;
while (*b_end && *b_end != '\r' && *b_end != '\n' && *b_end != ';')
b_end++;
boundary = g_strndup(b_start, (gsize)(b_end - b_start));
}
}
if (boundary != NULL)
{
char *delimiter = g_strdup_printf("--%s", boundary);
char **parts = g_strsplit(data, delimiter, EML_MIME_MAX_PARTS);
g_free(delimiter);
g_free(boundary);
for (guint i = 1; parts[i] != NULL && parts[i][0] != '\0'; i++)
{
if (strncmp(parts[i], "--", 2) == 0)
break; /* Fin du multipart */
const char *part_content = parts[i];
const char *hdr_end = strstr(part_content, "\r\n\r\n");
if (hdr_end == NULL) hdr_end = strstr(part_content, "\n\n");
if (hdr_end == NULL) continue;
gsize hdr_len = (gsize)(hdr_end - part_content);
char *headers = g_strndup(part_content, hdr_len);
const char *body = hdr_end + (strstr(hdr_end, "\r\n\r\n") == hdr_end ? 4 : 2);
/* Extraction du nom de fichier et content-type */
char *filename = NULL;
const char *fn_pos = strstr(headers, "filename=");
if (fn_pos == NULL) fn_pos = strstr(headers, "name=");
if (fn_pos != NULL)
{
const char *fn_start = fn_pos + (strstr(fn_pos, "filename=") == fn_pos ? 9 : 5);
if (*fn_start == '"')
{
fn_start++;
const char *fn_end = strchr(fn_start, '"');
if (fn_end != NULL) filename = g_strndup(fn_start, (gsize)(fn_end - fn_start));
}
else
{
const char *fn_end = fn_start;
while (*fn_end && *fn_end != '\r' && *fn_end != '\n' && *fn_end != ';') fn_end++;
filename = g_strndup(fn_start, (gsize)(fn_end - fn_start));
}
}
gboolean is_attachment = (strstr(headers, "attachment") != NULL) || (filename != NULL);
if (is_attachment)
{
char *sanitized = eml_mime_sanitize_filename(filename);
char *dest_path = g_build_filename(target_dir, sanitized, NULL);
/* Gestion des collisions */
guint counter = 1;
while (g_file_test(dest_path, G_FILE_TEST_EXISTS))
{
g_free(dest_path);
char *new_name = g_strdup_printf("%u_%s", counter++, sanitized);
dest_path = g_build_filename(target_dir, new_name, NULL);
g_free(new_name);
}
/* Extraction du Content-Transfer-Encoding */
char *encoding = NULL;
const char *enc_pos = strstr(headers, "Content-Transfer-Encoding:");
if (enc_pos != NULL)
{
const char *enc_val = enc_pos + 26;
const char *enc_end = strchr(enc_val, '\n');
if (enc_end != NULL) encoding = g_strndup(enc_val, (gsize)(enc_end - enc_val));
if (encoding != NULL) g_strstrip(encoding);
}
/* Extraction du Content-Type */
char *content_type = NULL;
const char *ct_pos = strstr(headers, "Content-Type:");
if (ct_pos != NULL)
{
const char *ct_val = ct_pos + 13;
const char *ct_end = strchr(ct_val, ';');
if (ct_end == NULL) ct_end = strchr(ct_val, '\n');
if (ct_end != NULL) content_type = g_strndup(ct_val, (gsize)(ct_end - ct_val));
if (content_type != NULL) g_strstrip(content_type);
}
/* Extraction du Content-ID */
char *content_id = NULL;
const char *cid_pos = strstr(headers, "Content-ID:");
if (cid_pos != NULL)
{
const char *cid_val = cid_pos + 11;
const char *cid_end = strchr(cid_val, '\n');
if (cid_end != NULL) content_id = g_strndup(cid_val, (gsize)(cid_end - cid_val));
if (content_id != NULL) g_strstrip(content_id);
}
gsize body_len = strlen(body);
GBytes *decoded_bytes = decode_transfer_encoding(encoding, body, body_len);
gsize decoded_len = 0;
gconstpointer decoded_data = g_bytes_get_data(decoded_bytes, &decoded_len);
GError *write_error = NULL;
if (g_file_set_contents(dest_path, decoded_data, (gssize) decoded_len, &write_error))
{
char *sha256 = NULL;
guint64 file_size = 0;
file_hash_compute_sha256(dest_path, NULL, &sha256,
&file_size, NULL);
EmlAttachment *att = g_new0(EmlAttachment, 1);
att->part_index = g_strdup_printf("1.%u", i);
att->declared_filename = filename != NULL ? g_strdup(filename) : g_strdup("attachment.bin");
att->sanitized_filename = g_strdup(sanitized);
att->extracted_path = dest_path; dest_path = NULL;
att->content_type = content_type != NULL ? content_type : g_strdup("application/octet-stream"); content_type = NULL;
att->content_id = content_id; content_id = NULL;
att->transfer_encoding = encoding != NULL ? encoding : g_strdup("7bit"); encoding = NULL;
att->is_inline = (strstr(headers, "inline") != NULL);
att->encoded_size = body_len;
att->decoded_size = decoded_len;
att->sha256 = sha256;
{
char *target_name = g_path_get_basename(target_dir);
att->relative_path = g_build_filename(
"02_Preuves_Traitees", "eml_attachments",
target_name, sanitized, NULL);
g_free(target_name);
}
if (content_type != NULL &&
g_content_type_is_a(content_type, "text/plain"))
att->detected_mime = g_strdup("text/plain");
else
att->detected_mime = g_content_type_guess(
dest_path != NULL ? dest_path : att->extracted_path,
decoded_data, decoded_len, NULL);
if (att->detected_mime == NULL)
att->detected_mime = g_strdup("application/octet-stream");
{
const char *dot = strrchr(att->sanitized_filename, '.');
if (dot != NULL && g_ascii_strcasecmp(dot + 1, "txt") == 0 &&
g_strcmp0(att->detected_mime, "text/plain") != 0)
att->has_inconsistency = TRUE;
}
g_ptr_array_add(result->attachments, att);
}
else
{
char *warn = g_strdup_printf("Impossible d'écrire la pièce jointe %s : %s",
sanitized, write_error->message);
g_ptr_array_add(result->warnings, warn);
g_error_free(write_error);
g_free(dest_path);
}
g_bytes_unref(decoded_bytes);
g_free(content_type);
g_free(content_id);
g_free(encoding);
g_free(sanitized);
}
g_free(filename);
g_free(headers);
}
g_strfreev(parts);
}
g_mapped_file_unref(mapped);
return result;
}

View file

@ -0,0 +1,177 @@
/******************************************************************************
* @file eml_pipeline_task.c
* @brief Pipeline d'analyse asynchrone complète pour fichier EML.
******************************************************************************/
#include "core/eml_pipeline_task.h"
#include "core/file_hash.h"
#include "core/rib_ocr.h"
#include <gio/gio.h>
#include <glib.h>
#include <string.h>
typedef struct
{
char *eml_path;
char *processed_evidence_dir;
char *evidence_id;
} EmlPipelineTaskData;
static void eml_pipeline_task_data_free(gpointer user_data)
{
EmlPipelineTaskData *data = user_data;
if (data == NULL)
return;
g_free(data->eml_path);
g_free(data->processed_evidence_dir);
g_free(data->evidence_id);
g_free(data);
}
void eml_pipeline_result_free(EmlPipelineResult *res)
{
if (res == NULL)
return;
if (res->analysis != NULL)
eml_analysis_free(res->analysis);
if (res->mime_result != NULL)
eml_mime_result_free(res->mime_result);
if (res->bank_proposals != NULL)
g_ptr_array_unref(res->bank_proposals);
if (res->warnings != NULL)
g_ptr_array_unref(res->warnings);
g_free(res);
}
static gboolean eml_pipeline_task_worker(BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *out_result,
GError **error)
{
EmlPipelineTaskData *data = worker_data;
g_return_val_if_fail(data != NULL, FALSE);
background_task_report_progress(task, 0.10, "Analyse des en-têtes EML...");
if (g_cancellable_is_cancelled(cancellable))
return FALSE;
EmlAnalysis *analysis = eml_analyzer_analyze_file(data->eml_path, error);
if (analysis == NULL)
return FALSE;
background_task_report_progress(task, 0.30, "Calcul d'empreinte SHA-256...");
char *eml_hash = NULL;
guint64 size = 0;
file_hash_compute_sha256(data->eml_path, cancellable, &eml_hash, &size,
NULL);
char *target_dir = g_build_filename(data->processed_evidence_dir, "eml_attachments",
eml_hash != NULL ? eml_hash : "default", NULL);
g_free(eml_hash);
background_task_report_progress(task, 0.50, "Extraction sécurisée des pièces jointes...");
if (g_cancellable_is_cancelled(cancellable))
{
g_free(target_dir);
eml_analysis_free(analysis);
return FALSE;
}
EmlMimeResult *mime_res = eml_mime_extract_attachments(data->eml_path, target_dir, error);
g_free(target_dir);
if (mime_res == NULL)
{
/* Si l'extraction MIME échoue, on conserve quand même l'analyse des en-têtes (résultat partiel) */
mime_res = g_new0(EmlMimeResult, 1);
mime_res->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free);
mime_res->warnings = g_ptr_array_new_with_free_func(g_free);
g_ptr_array_add(mime_res->warnings, g_strdup("L'extraction MIME a échoué ou ne contient aucune pièce jointe."));
}
background_task_report_progress(task, 0.75, "Analyse OCR et détection bancaire...");
GPtrArray *bank_proposals = g_ptr_array_new_with_free_func((GDestroyNotify) bank_proposal_free);
for (guint i = 0; mime_res->attachments != NULL && i < mime_res->attachments->len; i++)
{
EmlAttachment *att = g_ptr_array_index(mime_res->attachments, i);
if (att->extracted_path == NULL)
continue;
/* Analyse bancaire sur les fichiers texte/images compatibles */
if (g_str_has_suffix(att->extracted_path, ".txt") || g_str_has_suffix(att->extracted_path, ".eml"))
{
char *content = NULL;
if (g_file_get_contents(att->extracted_path, &content, NULL, NULL))
{
BankProposal *bp = bank_proposal_analyze_text(content, data->evidence_id);
if (bp != NULL)
{
bp->extraction_id = g_strdup(att->part_index);
g_ptr_array_add(bank_proposals, bp);
}
g_free(content);
}
}
else if (g_str_has_suffix(att->extracted_path, ".png") || g_str_has_suffix(att->extracted_path, ".jpg") || g_str_has_suffix(att->extracted_path, ".jpeg"))
{
char *ocr_text = NULL;
char *ocr_version = NULL;
(void) rib_ocr_extract_text(att->extracted_path, &ocr_text,
&ocr_version, NULL);
g_free(ocr_version);
if (ocr_text != NULL)
{
BankProposal *bp = bank_proposal_analyze_text(ocr_text, data->evidence_id);
if (bp != NULL)
{
bp->extraction_id = g_strdup(att->part_index);
g_ptr_array_add(bank_proposals, bp);
}
g_free(ocr_text);
}
}
}
background_task_report_progress(task, 1.0, "Analyse EML terminée avec succès.");
EmlPipelineResult *res = g_new0(EmlPipelineResult, 1);
res->analysis = analysis;
res->mime_result = mime_res;
res->bank_proposals = bank_proposals;
res->warnings = g_ptr_array_new_with_free_func(g_free);
if (out_result != NULL)
*out_result = res;
return TRUE;
}
BackgroundTask *eml_pipeline_task_new(const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id)
{
if (eml_path == NULL || processed_evidence_dir == NULL)
return NULL;
EmlPipelineTaskData *data = g_new0(EmlPipelineTaskData, 1);
data->eml_path = g_strdup(eml_path);
data->processed_evidence_dir = g_strdup(processed_evidence_dir);
data->evidence_id = g_strdup(evidence_id);
BackgroundTask *task = background_task_new(
"Analyse du message EML et de ses pièces jointes");
GError *start_error = NULL;
if (task == NULL || !background_task_start(task, eml_pipeline_task_worker,
data, eml_pipeline_task_data_free, (GDestroyNotify)
eml_pipeline_result_free, NULL, NULL, NULL, &start_error))
{
if (task != NULL)
background_task_unref(task);
else
eml_pipeline_task_data_free(data);
g_clear_error(&start_error);
return NULL;
}
return task;
}

View file

@ -16,12 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @brief Version actuelle du schéma SQLite.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT 9 #define DATABASE_SCHEMA_VERSION_CURRENT 10
/** /**
* @brief Version actuelle sous forme textuelle pour metadata. * @brief Version actuelle sous forme textuelle pour metadata.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "9" #define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "10"
/** /**
* @brief Nom de l'application enregistré dans les métadonnées. * @brief Nom de l'application enregistré dans les métadonnées.
@ -797,6 +797,21 @@ rollback:
return false; return false;
} }
static bool database_migrate_v9_to_v10(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v10(database) ||
!database_update_schema_version(database, "10") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V9 vers V10.");
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.
*/ */
@ -1022,6 +1037,10 @@ bool database_migrate_to_latest(
if (!database_migrate_v8_to_v9(database)) return false; if (!database_migrate_v8_to_v9(database)) return false;
schema_version = 9; schema_version = 9;
break; break;
case 9:
if (!database_migrate_v9_to_v10(database)) return false;
schema_version = 10;
break;
default: default:
database_set_error( database_set_error(
@ -1156,7 +1175,8 @@ bool database_initialize(
if (!schema_install_v7(database) || if (!schema_install_v7(database) ||
!schema_install_v8(database) || !schema_install_v8(database) ||
!schema_install_v9(database)) !schema_install_v9(database) ||
!schema_install_v10(database))
{ {
goto rollback; goto rollback;
} }

View file

@ -347,6 +347,12 @@ bool schema_install_v9(Database *database)
schema_v9_migrate_relation_types(database); schema_v9_migrate_relation_types(database);
} }
bool schema_install_v10(Database *database)
{
return schema_execute_file(database, "database/schema_v10.sql",
"la migration SQLite V10");
}
bool schema_ensure_current( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -3,6 +3,7 @@
* @brief Présentation en lecture seule d'une analyse EML. * @brief Présentation en lecture seule d'une analyse EML.
******************************************************************************/ ******************************************************************************/
#include "views/eml_analysis_dialog.h" #include "views/eml_analysis_dialog.h"
#include "widgets/controlled_vocab_dropdown.h"
typedef struct { GtkWindow *window; GtkWidget *proposals_box; typedef struct { GtkWindow *window; GtkWidget *proposals_box;
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed; EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
} EmlAnalysisDialogState; } EmlAnalysisDialogState;
@ -29,11 +30,20 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box); for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
child != NULL; child = gtk_widget_get_next_sibling(child)) child != NULL; child = gtk_widget_get_next_sibling(child))
{ {
const char *type = g_object_get_data(G_OBJECT(child), "eml-type"); GtkWidget *check = GTK_IS_BOX(child) ? gtk_widget_get_first_child(child) : NULL;
const char *value = g_object_get_data(G_OBJECT(child), "eml-value"); if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
if (GTK_IS_CHECK_BUTTON(child) && gtk_check_button_get_active( gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
GTK_CHECK_BUTTON(child))) {
g_ptr_array_add(selected, eml_entity_proposal_new(type, value)); const char *type = g_object_get_data(G_OBJECT(child), "eml-type");
const char *value = g_object_get_data(G_OBJECT(child), "eml-value");
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
"eml-provenance");
g_ptr_array_add(selected, eml_entity_proposal_new_with_metadata(
type, value,
controlled_vocab_dropdown_get_selected_code(status),
controlled_vocab_dropdown_get_selected_code(provenance)));
}
} }
if (selected->len == 0) if (selected->len == 0)
{ g_ptr_array_unref(selected); return; } { g_ptr_array_unref(selected); return; }
@ -50,10 +60,22 @@ static void eml_analysis_dialog_add_proposals(GtkWidget *box,
{ {
const char *value = g_ptr_array_index((GPtrArray *) values, i); const char *value = g_ptr_array_index((GPtrArray *) values, i);
char *text = g_strdup_printf("%s : %s", label, value); char *text = g_strdup_printf("%s : %s", label, value);
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *check = gtk_check_button_new_with_label(text); GtkWidget *check = gtk_check_button_new_with_label(text);
g_object_set_data_full(G_OBJECT(check), "eml-type", g_strdup(type), g_free); GtkWidget *status = controlled_vocab_dropdown_new(
g_object_set_data_full(G_OBJECT(check), "eml-value", g_strdup(value), g_free); CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
gtk_box_append(GTK_BOX(box), check); g_free(text); GtkWidget *provenance = controlled_vocab_dropdown_new(
CONTROLLED_VOCAB_PROVENANCE_KIND,
g_strcmp0(type, "ip_address") == 0 ? "header" : "header");
g_object_set_data_full(G_OBJECT(row), "eml-type", g_strdup(type), g_free);
g_object_set_data_full(G_OBJECT(row), "eml-value", g_strdup(value), g_free);
g_object_set_data(G_OBJECT(row), "eml-status", status);
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
gtk_widget_set_hexpand(check, TRUE);
gtk_box_append(GTK_BOX(row), check);
gtk_box_append(GTK_BOX(row), status);
gtk_box_append(GTK_BOX(row), provenance);
gtk_box_append(GTK_BOX(box), row); g_free(text);
} }
} }
/** @brief Ajoute une ligne de métadonnée sélectionnable. */ /** @brief Ajoute une ligne de métadonnée sélectionnable. */

View file

@ -0,0 +1,83 @@
/******************************************************************************
* @file controlled_vocab_dropdown.c
* @brief Implémentation du widget GTK4 GtkDropDown pour le vocabulaire contrôlé.
******************************************************************************/
#include "widgets/controlled_vocab_dropdown.h"
#define CONTROLLED_VOCAB_CATEGORY_KEY "labfy-vocab-category"
GtkWidget *controlled_vocab_dropdown_new(ControlledVocabCategory category,
const char *default_code)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(category, &count);
if (items == NULL || count == 0)
return NULL;
const char **labels = g_new0(const char *, count + 1);
for (size_t i = 0; i < count; i++)
{
labels[i] = items[i].label;
}
labels[count] = NULL;
GtkStringList *string_list = gtk_string_list_new(labels);
g_free(labels);
GtkWidget *dropdown = gtk_drop_down_new(G_LIST_MODEL(string_list), NULL);
g_object_set_data(G_OBJECT(dropdown), CONTROLLED_VOCAB_CATEGORY_KEY,
GUINT_TO_POINTER(category));
if (default_code != NULL)
{
controlled_vocab_dropdown_set_selected_code(dropdown, default_code);
}
return dropdown;
}
const char *controlled_vocab_dropdown_get_selected_code(GtkWidget *dropdown)
{
if (dropdown == NULL || !GTK_IS_DROP_DOWN(dropdown))
return NULL;
guint category_val = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(dropdown), CONTROLLED_VOCAB_CATEGORY_KEY));
ControlledVocabCategory category = (ControlledVocabCategory) category_val;
guint selected = gtk_drop_down_get_selected(GTK_DROP_DOWN(dropdown));
if (selected == GTK_INVALID_LIST_POSITION)
return NULL;
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(category, &count);
if (items == NULL || selected >= count)
return NULL;
return items[selected].code;
}
gboolean controlled_vocab_dropdown_set_selected_code(GtkWidget *dropdown,
const char *code)
{
if (dropdown == NULL || !GTK_IS_DROP_DOWN(dropdown) || code == NULL)
return FALSE;
guint category_val = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(dropdown), CONTROLLED_VOCAB_CATEGORY_KEY));
ControlledVocabCategory category = (ControlledVocabCategory) category_val;
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(category, &count);
if (items == NULL)
return FALSE;
for (size_t i = 0; i < count; i++)
{
if (g_ascii_strcasecmp(items[i].code, code) == 0)
{
gtk_drop_down_set_selected(GTK_DROP_DOWN(dropdown), (guint) i);
return TRUE;
}
}
return FALSE;
}

View file

@ -0,0 +1,58 @@
/******************************************************************************
* @file test_bank_proposal.c
* @brief Tests unitaires du modèle bancaire et de l'analyse d'IBAN / RIB / BIC.
******************************************************************************/
#include "core/bank_proposal.h"
#include <glib.h>
static void test_iban_validation(void)
{
/* IBAN français synthétique avec MOD-97 et RIB valide */
const char *valid_fr = "FR4830002005500000000000052";
g_assert_true(bank_proposal_validate_iban(valid_fr));
/* IBAN invalide (mauvaise clé) */
const char *invalid_fr = "FR0030002005500000000000021";
g_assert_false(bank_proposal_validate_iban(invalid_fr));
/* IBAN trop court */
g_assert_false(bank_proposal_validate_iban("FR763000"));
}
static void test_bic_validation(void)
{
g_assert_true(bank_proposal_validate_bic("BNPAFRPPXXX"));
g_assert_true(bank_proposal_validate_bic("BNPAFRPP"));
g_assert_false(bank_proposal_validate_bic("BNPAFR"));
g_assert_false(bank_proposal_validate_bic("INVALID BIC!"));
}
static void test_french_rib_derivation(void)
{
const char *valid_fr = "FR4830002005500000000000052";
BankProposal *proposal = bank_proposal_analyze_text(valid_fr, "evidence-123");
g_assert_nonnull(proposal);
g_assert_true(proposal->is_iban_valid);
g_assert_true(proposal->is_derived_bban);
g_assert_cmpstr(proposal->country_code, ==, "FR");
g_assert_cmpstr(proposal->bank_code, ==, "30002");
g_assert_cmpstr(proposal->branch_code, ==, "00550");
g_assert_cmpstr(proposal->account_number, ==, "00000000000");
g_assert_cmpstr(proposal->rib_key, ==, "52");
g_assert_cmpstr(proposal->evidence_id, ==, "evidence-123");
g_assert_cmpstr(proposal->verification_status, ==, "proposed");
bank_proposal_free(proposal);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/bank-proposal/iban-validation", test_iban_validation);
g_test_add_func("/bank-proposal/bic-validation", test_bic_validation);
g_test_add_func("/bank-proposal/french-rib-derivation", test_french_rib_derivation);
return g_test_run();
}

View file

@ -0,0 +1,91 @@
/******************************************************************************
* @file test_controlled_vocab.c
* @brief Tests unitaires du vocabulaire contrôlé.
******************************************************************************/
#include "core/controlled_vocab.h"
#include <glib.h>
static void test_verification_statuses(void)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(CONTROLLED_VOCAB_VERIFICATION_STATUS, &count);
g_assert_nonnull(items);
g_assert_cmpuint(count, ==, 5);
g_assert_true(controlled_vocab_is_valid_verification_status("proposed"));
g_assert_true(controlled_vocab_is_valid_verification_status("confirmed"));
g_assert_true(controlled_vocab_is_valid_verification_status("rejected"));
g_assert_true(controlled_vocab_is_valid_verification_status("conflicted"));
g_assert_true(controlled_vocab_is_valid_verification_status("invalid"));
g_assert_false(controlled_vocab_is_valid_verification_status("unknown_status"));
g_assert_false(controlled_vocab_is_valid_verification_status(NULL));
g_assert_false(controlled_vocab_is_valid_verification_status(""));
g_assert_cmpstr(controlled_vocab_get_label(CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed"), ==, "Proposé");
g_assert_cmpstr(controlled_vocab_get_code_from_label(CONTROLLED_VOCAB_VERIFICATION_STATUS, "Proposé"), ==, "proposed");
}
static void test_provenance_kinds(void)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(CONTROLLED_VOCAB_PROVENANCE_KIND, &count);
g_assert_nonnull(items);
g_assert_cmpuint(count, ==, 6);
g_assert_true(controlled_vocab_is_valid_provenance_kind("observed"));
g_assert_true(controlled_vocab_is_valid_provenance_kind("ocr"));
g_assert_true(controlled_vocab_is_valid_provenance_kind("header"));
g_assert_true(controlled_vocab_is_valid_provenance_kind("metadata"));
g_assert_true(controlled_vocab_is_valid_provenance_kind("derived"));
g_assert_true(controlled_vocab_is_valid_provenance_kind("manual"));
g_assert_false(controlled_vocab_is_valid_provenance_kind("invalid_kind"));
}
static void test_email_roles(void)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(CONTROLLED_VOCAB_EMAIL_ROLE, &count);
g_assert_nonnull(items);
g_assert_cmpuint(count, ==, 9);
g_assert_true(controlled_vocab_is_valid_email_role("from"));
g_assert_true(controlled_vocab_is_valid_email_role("reply_to"));
g_assert_false(controlled_vocab_is_valid_email_role("custom_role"));
}
static void test_smtp_roles(void)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(CONTROLLED_VOCAB_SMTP_ROLE, &count);
g_assert_nonnull(items);
g_assert_cmpuint(count, ==, 5);
g_assert_true(controlled_vocab_is_valid_smtp_role("declared_source"));
g_assert_true(controlled_vocab_is_valid_smtp_role("smtp_relay"));
g_assert_false(controlled_vocab_is_valid_smtp_role("fraudster_ip"));
}
static void test_value_types(void)
{
size_t count = 0;
const ControlledVocabItem *items = controlled_vocab_get_items(CONTROLLED_VOCAB_VALUE_TYPE, &count);
g_assert_nonnull(items);
g_assert_cmpuint(count, ==, 14);
g_assert_true(controlled_vocab_is_valid_value_type("iban"));
g_assert_true(controlled_vocab_is_valid_value_type("email"));
g_assert_false(controlled_vocab_is_valid_value_type("invalid_type"));
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/controlled-vocab/verification-status", test_verification_statuses);
g_test_add_func("/controlled-vocab/provenance-kinds", test_provenance_kinds);
g_test_add_func("/controlled-vocab/email-roles", test_email_roles);
g_test_add_func("/controlled-vocab/smtp-roles", test_smtp_roles);
g_test_add_func("/controlled-vocab/value-types", test_value_types);
return g_test_run();
}

View file

@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "FROM investigation;"
); );
assert(strcmp(schema_version, "9") == 0); assert(strcmp(schema_version, "10") == 0);
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");
test_database_assert_table_exists(database, "osint_executions"); test_database_assert_table_exists(database, "osint_executions");
@ -991,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
assert( assert(
strcmp( strcmp(
schema_version, schema_version,
"9" "10"
) == 0 ) == 0
); );

View file

@ -0,0 +1,94 @@
/******************************************************************************
* @file test_eml_pipeline_task.c
* @brief Tests unitaires du pipeline asynchrone d'analyse EML et pièces jointes.
******************************************************************************/
#include "core/eml_pipeline_task.h"
#include <gio/gio.h>
#include <glib.h>
#include <glib/gstdio.h>
static void test_eml_pipeline_basic(void)
{
char *tmp_dir = g_dir_make_tmp("labfy-eml-test-XXXXXX", NULL);
g_assert_nonnull(tmp_dir);
char *eml_path = g_build_filename(tmp_dir, "test.eml", NULL);
const char *eml_content =
"From: Alice <alice@example.com>\r\n"
"To: Bob <bob@example.com>\r\n"
"Subject: Rib suspect\r\n"
"Date: Thu, 23 Jul 2026 12:00:00 +0200\r\n"
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/mixed; boundary=\"BOUNDARY123\"\r\n"
"\r\n"
"--BOUNDARY123\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"\r\n"
"Veuillez trouver ci-joint mon RIB.\r\n"
"--BOUNDARY123\r\n"
"Content-Type: text/plain; name=\"../rib_suspect.txt\"\r\n"
"Content-Disposition: attachment; filename=\"../rib_suspect.txt\"\r\n"
"\r\n"
"FR4830002005500000000000052\r\n"
"--BOUNDARY123\r\n"
"Content-Type: text/plain; name=\"../rib_suspect.txt\"\r\n"
"Content-Disposition: attachment; filename=\"../rib_suspect.txt\"\r\n"
"Content-Transfer-Encoding: quoted-printable\r\n"
"\r\n"
"Deuxi=C3=A8me RIB\r\n"
"--BOUNDARY123--\r\n";
g_file_set_contents(eml_path, eml_content, -1, NULL);
char *processed_dir = g_build_filename(tmp_dir, "02_Preuves_Traitees", NULL);
g_mkdir_with_parents(processed_dir, 0755);
BackgroundTask *task = eml_pipeline_task_new(eml_path, processed_dir, "evidence-uuid-1");
g_assert_nonnull(task);
g_assert_cmpint(background_task_get_state(task), !=,
BACKGROUND_TASK_STATE_FAILED);
/* Attente de la fin de la tâche */
while (background_task_get_state(task) == BACKGROUND_TASK_STATE_RUNNING ||
background_task_get_state(task) == BACKGROUND_TASK_STATE_PENDING)
{
g_main_context_iteration(NULL, TRUE);
}
g_assert_cmpint(background_task_get_state(task), ==, BACKGROUND_TASK_STATE_COMPLETED);
EmlPipelineResult *result = background_task_get_result(task);
g_assert_nonnull(result);
g_assert_nonnull(result->analysis);
g_assert_nonnull(result->mime_result);
/* Vérification de la protection contre les traversées de chemin "../" */
g_assert_cmpuint(result->mime_result->attachments->len, ==, 2);
EmlAttachment *att = g_ptr_array_index(result->mime_result->attachments, 0);
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
att = g_ptr_array_index(result->mime_result->attachments, 1);
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
g_assert_true(g_str_has_suffix(att->extracted_path,
"/1____rib_suspect.txt"));
g_assert_cmpuint(att->decoded_size, >, 0U);
/* Vérification de la détection de la proposition bancaire dans la pièce jointe */
g_assert_cmpuint(result->bank_proposals->len, ==, 1);
BankProposal *bp = g_ptr_array_index(result->bank_proposals, 0);
g_assert_cmpstr(bp->normalized_iban, ==, "FR4830002005500000000000052");
g_assert_true(bp->is_iban_valid);
background_task_unref(task);
g_remove(eml_path);
g_free(eml_path);
g_free(processed_dir);
g_free(tmp_dir);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/eml-pipeline-task/basic", test_eml_pipeline_basic);
return g_test_run();
}

5
watch_20260723-211101 Normal file
View file

@ -0,0 +1,5 @@
NAME ID SIZE PROCESSOR CONTEXT UNTIL
total utilisé libre partagé tamp/cache disponible
Mem: 14Gi 5,1Gi 7,9Gi 70Mi 2,2Gi 9,8Gi
Échange: 4,0Gi 3,8Gi 224Mi