feat(relations): centraliser les types et améliorer les aperçus
This commit is contained in:
parent
8bc3b43d63
commit
613d2096bc
28 changed files with 1826 additions and 18 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,3 +16,4 @@ labfy-investigation
|
||||||
# Neovim / LSP
|
# Neovim / LSP
|
||||||
.cache/
|
.cache/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
/AGENTS.md
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
42
Makefile
42
Makefile
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 d’exécution ;
|
- conserver les sorties brutes, les versions et les paramètres d’exé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 d’outils, les corrélations et les hypothèses ;
|
- distinguer les faits observés, les résultats d’outils, les corrélations et les hypothèses ;
|
||||||
- produire des rapports compréhensibles et traçables.
|
- produire des rapports compréhensibles et traçables.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
45
database/schema_v10.sql
Normal 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);
|
||||||
|
|
@ -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 d’association existantes.
|
||||||
|
|
||||||
|
Les codes de statuts, de provenance, de rôles d’adresse 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 :
|
||||||
|
|
|
||||||
73
include/core/bank_proposal.h
Normal file
73
include/core/bank_proposal.h
Normal 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 */
|
||||||
67
include/core/controlled_vocab.h
Normal file
67
include/core/controlled_vocab.h
Normal 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 */
|
||||||
|
|
@ -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);
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
68
include/core/eml_mime_extractor.h
Normal file
68
include/core/eml_mime_extractor.h
Normal 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 */
|
||||||
40
include/core/eml_pipeline_task.h
Normal file
40
include/core/eml_pipeline_task.h
Normal 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 */
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
40
include/widgets/controlled_vocab_dropdown.h
Normal file
40
include/widgets/controlled_vocab_dropdown.h
Normal 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 été 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
202
src/core/bank_proposal.c
Normal 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
182
src/core/controlled_vocab.c
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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 d’une 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;
|
||||||
|
|
|
||||||
388
src/core/eml_mime_extractor.c
Normal file
388
src/core/eml_mime_extractor.c
Normal 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;
|
||||||
|
}
|
||||||
177
src/core/eml_pipeline_task.c
Normal file
177
src/core/eml_pipeline_task.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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 d’annuler 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -28,12 +29,21 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
(void) button;
|
(void) button;
|
||||||
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))
|
||||||
|
{
|
||||||
|
GtkWidget *check = GTK_IS_BOX(child) ? gtk_widget_get_first_child(child) : NULL;
|
||||||
|
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
||||||
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
||||||
{
|
{
|
||||||
const char *type = g_object_get_data(G_OBJECT(child), "eml-type");
|
const char *type = g_object_get_data(G_OBJECT(child), "eml-type");
|
||||||
const char *value = g_object_get_data(G_OBJECT(child), "eml-value");
|
const char *value = g_object_get_data(G_OBJECT(child), "eml-value");
|
||||||
if (GTK_IS_CHECK_BUTTON(child) && gtk_check_button_get_active(
|
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
||||||
GTK_CHECK_BUTTON(child)))
|
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
||||||
g_ptr_array_add(selected, eml_entity_proposal_new(type, value));
|
"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. */
|
||||||
|
|
|
||||||
83
src/widgets/controlled_vocab_dropdown.c
Normal file
83
src/widgets/controlled_vocab_dropdown.c
Normal 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;
|
||||||
|
}
|
||||||
58
tests/test_bank_proposal.c
Normal file
58
tests/test_bank_proposal.c
Normal 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();
|
||||||
|
}
|
||||||
91
tests/test_controlled_vocab.c
Normal file
91
tests/test_controlled_vocab.c
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
94
tests/test_eml_pipeline_task.c
Normal file
94
tests/test_eml_pipeline_task.c
Normal 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
5
watch_20260723-211101
Normal 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
|
||||||
Loading…
Reference in a new issue