feat: finaliser les observations EML réversibles
This commit is contained in:
parent
a3c4b4c51a
commit
d00d149bef
28 changed files with 1585 additions and 55 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -17,3 +17,8 @@ labfy-investigation
|
||||||
.cache/
|
.cache/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
/AGENTS.md
|
/AGENTS.md
|
||||||
|
/prompt.md
|
||||||
|
|
||||||
|
# OpenCode local
|
||||||
|
/opencode.json
|
||||||
|
/.opencode/
|
||||||
|
|
|
||||||
16
Makefile
16
Makefile
|
|
@ -126,6 +126,7 @@ TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
|
||||||
TEST_SOCIAL_PLATFORM := tests/test_social_platform
|
TEST_SOCIAL_PLATFORM := tests/test_social_platform
|
||||||
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
|
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
|
||||||
TEST_EML_ANALYZER := tests/test_eml_analyzer
|
TEST_EML_ANALYZER := tests/test_eml_analyzer
|
||||||
|
TEST_EML_INTEGRATION := tests/test_eml_integration
|
||||||
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
||||||
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
||||||
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
||||||
|
|
@ -316,6 +317,7 @@ $(TEST_EXTRACTION_DROP_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/evidence_record.c \
|
src/models/evidence_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
|
|
@ -575,6 +577,7 @@ $(TEST_EVIDENCE_ENTITY_DAO): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/models/evidence_record.c \
|
src/models/evidence_record.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -759,6 +762,7 @@ $(TEST_SOCIAL_ACCOUNT_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -777,6 +781,7 @@ $(TEST_PERSON_ENTITY_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -787,6 +792,14 @@ $(TEST_PERSON_ENTITY_SERVICE): \
|
||||||
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
|
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
|
||||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||||
|
|
||||||
|
$(TEST_EML_INTEGRATION): tests/test_eml_integration.c \
|
||||||
|
src/core/eml_integration.c src/core/controlled_vocab.c \
|
||||||
|
src/dao/entity_dao.c src/dao/evidence_entity_dao.c \
|
||||||
|
src/models/entity_record.c src/models/evidence_observation.c \
|
||||||
|
src/database/database.c src/database/schema.c \
|
||||||
|
src/database/statement.c src/database/transaction.c src/database/error.c
|
||||||
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||||
|
|
||||||
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
|
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
|
||||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||||
|
|
||||||
|
|
@ -883,6 +896,7 @@ test: \
|
||||||
$(TEST_SOCIAL_PLATFORM) \
|
$(TEST_SOCIAL_PLATFORM) \
|
||||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||||
$(TEST_EML_ANALYZER) \
|
$(TEST_EML_ANALYZER) \
|
||||||
|
$(TEST_EML_INTEGRATION) \
|
||||||
$(TEST_IBAN_ANALYZER) \
|
$(TEST_IBAN_ANALYZER) \
|
||||||
$(TEST_EXIFTOOL_METADATA) \
|
$(TEST_EXIFTOOL_METADATA) \
|
||||||
$(TEST_PDF_PASSWORD_RECOVERY) \
|
$(TEST_PDF_PASSWORD_RECOVERY) \
|
||||||
|
|
@ -956,6 +970,7 @@ test: \
|
||||||
@$(TEST_SOCIAL_PLATFORM)
|
@$(TEST_SOCIAL_PLATFORM)
|
||||||
@$(TEST_PERSON_ENTITY_SERVICE)
|
@$(TEST_PERSON_ENTITY_SERVICE)
|
||||||
@$(TEST_EML_ANALYZER)
|
@$(TEST_EML_ANALYZER)
|
||||||
|
@$(TEST_EML_INTEGRATION)
|
||||||
@$(TEST_IBAN_ANALYZER)
|
@$(TEST_IBAN_ANALYZER)
|
||||||
@$(TEST_EXIFTOOL_METADATA)
|
@$(TEST_EXIFTOOL_METADATA)
|
||||||
@$(TEST_PDF_PASSWORD_RECOVERY)
|
@$(TEST_PDF_PASSWORD_RECOVERY)
|
||||||
|
|
@ -1036,6 +1051,7 @@ clean:
|
||||||
$(TEST_SOCIAL_PLATFORM) \
|
$(TEST_SOCIAL_PLATFORM) \
|
||||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||||
$(TEST_EML_ANALYZER) \
|
$(TEST_EML_ANALYZER) \
|
||||||
|
$(TEST_EML_INTEGRATION) \
|
||||||
$(TEST_EXTRACTION_DROP_SERVICE) \
|
$(TEST_EXTRACTION_DROP_SERVICE) \
|
||||||
$(TEST_RELATION_TYPE_NORMALIZER) \
|
$(TEST_RELATION_TYPE_NORMALIZER) \
|
||||||
$(TEST_RELATION_TYPE_SERVICE) \
|
$(TEST_RELATION_TYPE_SERVICE) \
|
||||||
|
|
|
||||||
|
|
@ -177,3 +177,38 @@ INSERT OR IGNORE INTO relation_types(code, label, normalized_key, description, i
|
||||||
('held_at', 'Tenu auprès de', 'tenu auprès de', 'Compte bancaire ouvert dans une banque.', 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),
|
('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);
|
('supports', 'Soutient', 'soutient', 'Preuve soutenant une entité ou relation.', 1);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS evidence_entity_observations
|
||||||
|
(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
entity_id TEXT,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
value_raw TEXT NOT NULL,
|
||||||
|
value_normalized TEXT,
|
||||||
|
value_corrected TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
provenance_kind TEXT NOT NULL,
|
||||||
|
source_header TEXT NOT NULL,
|
||||||
|
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
|
||||||
|
verification_status TEXT NOT NULL DEFAULT 'proposed',
|
||||||
|
extraction_id TEXT,
|
||||||
|
warning TEXT,
|
||||||
|
observed_at TEXT NOT NULL CHECK (length(observed_at) = 20),
|
||||||
|
integrated_at TEXT NOT NULL CHECK (length(integrated_at) = 20),
|
||||||
|
promoted_at TEXT,
|
||||||
|
promotion_kind TEXT,
|
||||||
|
UNIQUE (evidence_id, entity_type, value_normalized, role, source_header,
|
||||||
|
occurrence, provenance_kind, extraction_id),
|
||||||
|
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_evidence
|
||||||
|
ON evidence_entity_observations(evidence_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_entity
|
||||||
|
ON evidence_entity_observations(entity_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_evidence_entity_observations_semantic
|
||||||
|
ON evidence_entity_observations(
|
||||||
|
evidence_id,entity_type,value_normalized,role,source_header,
|
||||||
|
occurrence,provenance_kind,COALESCE(extraction_id,''));
|
||||||
|
|
|
||||||
24
database/schema_v11.sql
Normal file
24
database/schema_v11.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* Schéma SQLite V11 — observations sémantiques preuve-entité.
|
||||||
|
******************************************************************************/
|
||||||
|
CREATE TABLE IF NOT EXISTS evidence_entity_observations
|
||||||
|
(
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
entity_id TEXT NOT NULL,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
value_raw TEXT NOT NULL,
|
||||||
|
value_normalized TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
provenance_kind TEXT NOT NULL,
|
||||||
|
source_header TEXT NOT NULL,
|
||||||
|
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
|
||||||
|
verification_status TEXT NOT NULL DEFAULT 'proposed',
|
||||||
|
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
|
||||||
|
PRIMARY KEY (evidence_id, entity_id, role, source_header, occurrence),
|
||||||
|
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
|
||||||
|
CHECK (length(trim(role)) > 0),
|
||||||
|
CHECK (length(trim(source_header)) > 0)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_evidence
|
||||||
|
ON evidence_entity_observations(evidence_id);
|
||||||
55
database/schema_v12.sql
Normal file
55
database/schema_v12.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* Schéma SQLite V12 — observations indépendantes et promotion facultative.
|
||||||
|
******************************************************************************/
|
||||||
|
ALTER TABLE evidence_entity_observations RENAME TO evidence_entity_observations_v11;
|
||||||
|
|
||||||
|
CREATE TABLE evidence_entity_observations
|
||||||
|
(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
evidence_id TEXT NOT NULL,
|
||||||
|
entity_id TEXT,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
value_raw TEXT NOT NULL,
|
||||||
|
value_normalized TEXT,
|
||||||
|
value_corrected TEXT,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
provenance_kind TEXT NOT NULL,
|
||||||
|
source_header TEXT NOT NULL,
|
||||||
|
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
|
||||||
|
extraction_id TEXT,
|
||||||
|
verification_status TEXT NOT NULL DEFAULT 'proposed',
|
||||||
|
warning TEXT,
|
||||||
|
observed_at TEXT NOT NULL CHECK (length(observed_at) = 20),
|
||||||
|
integrated_at TEXT NOT NULL CHECK (length(integrated_at) = 20),
|
||||||
|
promoted_at TEXT CHECK (promoted_at IS NULL OR length(promoted_at) = 20),
|
||||||
|
promotion_kind TEXT CHECK (promotion_kind IS NULL OR promotion_kind IN ('created', 'reused', 'legacy')),
|
||||||
|
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL,
|
||||||
|
UNIQUE (evidence_id, entity_type, value_normalized, role, source_header,
|
||||||
|
occurrence, provenance_kind, extraction_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO evidence_entity_observations(
|
||||||
|
id,evidence_id,entity_id,entity_type,value_raw,value_normalized,role,
|
||||||
|
provenance_kind,source_header,occurrence,verification_status,
|
||||||
|
observed_at,integrated_at,promoted_at,promotion_kind)
|
||||||
|
SELECT
|
||||||
|
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-a' ||
|
||||||
|
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
|
||||||
|
evidence_id,entity_id,entity_type,value_raw,value_normalized,role,
|
||||||
|
provenance_kind,source_header,occurrence,verification_status,
|
||||||
|
created_at,created_at,created_at,'legacy'
|
||||||
|
FROM evidence_entity_observations_v11;
|
||||||
|
|
||||||
|
DROP TABLE evidence_entity_observations_v11;
|
||||||
|
|
||||||
|
CREATE INDEX idx_evidence_entity_observations_evidence
|
||||||
|
ON evidence_entity_observations(evidence_id);
|
||||||
|
CREATE INDEX idx_evidence_entity_observations_entity
|
||||||
|
ON evidence_entity_observations(entity_id);
|
||||||
|
CREATE UNIQUE INDEX idx_evidence_entity_observations_semantic
|
||||||
|
ON evidence_entity_observations(
|
||||||
|
evidence_id,entity_type,value_normalized,role,source_header,
|
||||||
|
occurrence,provenance_kind,COALESCE(extraction_id,''));
|
||||||
|
|
@ -23,25 +23,44 @@ réelle.
|
||||||
10. Vérifier que les valeurs sont sélectionnables et copiables.
|
10. Vérifier que les valeurs sont sélectionnables et copiables.
|
||||||
11. Fermer avec « Rejeter et fermer » et confirmer l’absence de nouvelle
|
11. Fermer avec « Rejeter et fermer » et confirmer l’absence de nouvelle
|
||||||
entité, relation ou rattachement.
|
entité, relation ou rattachement.
|
||||||
12. Relancer, sélectionner quelques adresses ou domaines synthétiques, puis
|
12. Relancer et vérifier que chaque proposition affiche deux actions
|
||||||
|
distinctes : « Conserver dans la fiche » et « Promouvoir en entité ».
|
||||||
|
La promotion doit être décochée et désactivée tant que la conservation
|
||||||
|
n’est pas cochée.
|
||||||
|
13. Vérifier que chaque proposition affiche son rôle et son
|
||||||
|
origine (`From`, `To`, `Message-ID` ou `Received #n`). Vérifier
|
||||||
|
explicitement que `192.0.2.10`, `198.51.100.20` sont des IP et que
|
||||||
|
`1.0` n’est jamais proposé comme domaine.
|
||||||
|
14. Cocher uniquement « Conserver dans la fiche » pour quelques adresses,
|
||||||
|
domaines et relais synthétiques, puis
|
||||||
choisir « Intégrer les éléments sélectionnés ».
|
choisir « Intégrer les éléments sélectionnés ».
|
||||||
13. Vérifier le message de bilan, le rafraîchissement du graphe, de la barre
|
15. Vérifier que le bilan annonce zéro promotion, qu’aucun nœud ni lien
|
||||||
latérale et des détails de preuve.
|
`preuve_entites` n’est créé et que la
|
||||||
14. Fermer puis rouvrir l’enquête synthétique et vérifier que les objets
|
fiche de la preuve reste sélectionnée. La section « Entités observées
|
||||||
confirmés sont toujours présents.
|
dans cette preuve » doit afficher valeur canonique, type, rôle,
|
||||||
15. Recalculer l’intégrité de la preuve et vérifier que son SHA-256 est
|
en-tête/occurrence, provenance et « graphe : non ajoutée ».
|
||||||
|
16. Fermer puis rouvrir l’enquête synthétique et vérifier que les
|
||||||
|
observations, rôles et origines sont toujours présents sans nœud.
|
||||||
|
17. Relancer l’analyse, cocher la conservation et « Promouvoir en entité »
|
||||||
|
pour une seule observation, confirmer et vérifier qu’un seul nœud
|
||||||
|
apparaît et que la fiche indique la promotion.
|
||||||
|
18. Répéter la promotion et vérifier qu’aucune observation ni entité n’est
|
||||||
|
dupliquée.
|
||||||
|
19. Dans « Observations extraites », utiliser « Retirer du graphe » sur
|
||||||
|
l’observation promue et confirmer. Vérifier que l’observation, son rôle
|
||||||
|
et sa provenance restent affichés avec « Graphe : Non ajoutée ».
|
||||||
|
20. Vérifier que le nœud disparaît lorsqu’il n’a aucune autre référence.
|
||||||
|
21. Promouvoir deux observations de rôles différents vers la même valeur
|
||||||
|
canonique, puis n’en retirer qu’une. Vérifier que l’entité partagée et
|
||||||
|
l’autre observation restent présentes.
|
||||||
|
22. Fermer puis rouvrir l’enquête synthétique et vérifier que tous les états
|
||||||
|
de promotion et de retrait persistent.
|
||||||
|
23. Recalculer l’intégrité de la preuve et vérifier que son SHA-256 est
|
||||||
inchangé.
|
inchangé.
|
||||||
|
|
||||||
Noter séparément le comportement lorsque ExifTool, Tesseract ou les outils
|
Noter séparément le comportement lorsque ExifTool, Tesseract ou les outils
|
||||||
Poppler ne sont pas installés : les en-têtes et l’extraction MIME doivent
|
Poppler ne sont pas installés : les en-têtes et l’extraction MIME doivent
|
||||||
rester consultables.
|
rester consultables.
|
||||||
|
|
||||||
## Test sur une copie d’enquête réelle
|
Ce parcours doit rester exclusivement synthétique : aucune enquête, preuve,
|
||||||
|
base SQLite, pièce jointe ou donnée réelle ne doit être utilisée.
|
||||||
Ne jamais commencer sur l’unique exemplaire d’une enquête. Copier le dossier
|
|
||||||
complet, vérifier que la copie s’ouvre, conserver une sauvegarde distincte,
|
|
||||||
puis lancer Labfy Investigation uniquement sur cette copie.
|
|
||||||
|
|
||||||
Ne jamais transmettre à Codex une preuve, une base SQLite, une pièce jointe
|
|
||||||
ou une donnée sensible. En cas d’anomalie, relever uniquement les étapes, les
|
|
||||||
messages techniques expurgés et le comportement observé.
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
|
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
|
||||||
typedef struct EmlAnalysis EmlAnalysis;
|
typedef struct EmlAnalysis EmlAnalysis;
|
||||||
|
typedef struct {
|
||||||
|
char *type_identifier;
|
||||||
|
char *value_raw;
|
||||||
|
char *value_normalized;
|
||||||
|
char *role;
|
||||||
|
char *source_header;
|
||||||
|
guint occurrence;
|
||||||
|
char *provenance_kind;
|
||||||
|
} EmlObservation;
|
||||||
/**
|
/**
|
||||||
* @brief Analyse uniquement les en-têtes d'un fichier EML local.
|
* @brief Analyse uniquement les en-têtes d'un fichier EML local.
|
||||||
* @param file_path Chemin du fichier original en lecture seule.
|
* @param file_path Chemin du fichier original en lecture seule.
|
||||||
|
|
@ -27,6 +36,10 @@ const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
||||||
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
|
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
|
||||||
/** @brief Retourne les domaines uniques extraits. */
|
/** @brief Retourne les domaines uniques extraits. */
|
||||||
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *analysis);
|
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *analysis);
|
||||||
|
/** @brief Retourne les observations sémantiques ordonnées. */
|
||||||
|
const GPtrArray *eml_analysis_get_observations(const EmlAnalysis *analysis);
|
||||||
|
/** @brief Libère une observation autonome. */
|
||||||
|
void eml_observation_free(EmlObservation *observation);
|
||||||
/** @brief Retourne les adresses IP uniques extraites. */
|
/** @brief Retourne les adresses IP uniques extraites. */
|
||||||
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
|
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
|
||||||
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
|
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ typedef struct {
|
||||||
char *value;
|
char *value;
|
||||||
char *verification_status;
|
char *verification_status;
|
||||||
char *provenance_kind;
|
char *provenance_kind;
|
||||||
|
char *value_raw;
|
||||||
|
char *role;
|
||||||
|
char *source_header;
|
||||||
|
guint occurrence;
|
||||||
|
gboolean promote_to_entity;
|
||||||
} EmlEntityProposal;
|
} 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,
|
||||||
|
|
@ -20,6 +25,11 @@ EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
|
||||||
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
|
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
|
||||||
const char *type_identifier, const char *value,
|
const char *type_identifier, const char *value,
|
||||||
const char *verification_status, const char *provenance_kind);
|
const char *verification_status, const char *provenance_kind);
|
||||||
|
EmlEntityProposal *eml_entity_proposal_new_observation(
|
||||||
|
const char *type_identifier, const char *value_raw,
|
||||||
|
const char *value_normalized, const char *role,
|
||||||
|
const char *source_header, guint occurrence,
|
||||||
|
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);
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,6 +44,12 @@ void eml_entity_proposal_free(EmlEntityProposal *proposal);
|
||||||
*/
|
*/
|
||||||
gboolean eml_integration_apply(Database *database,
|
gboolean eml_integration_apply(Database *database,
|
||||||
const char *evidence_identifier, const GPtrArray *proposals,
|
const char *evidence_identifier, const GPtrArray *proposals,
|
||||||
guint *out_created, guint *out_reused, GError **error);
|
guint *out_observations, guint *out_created, guint *out_reused,
|
||||||
|
GError **error);
|
||||||
|
|
||||||
|
/** @brief Annule une promotion sans supprimer l'observation. */
|
||||||
|
gboolean eml_integration_remove_promotion(Database *database,
|
||||||
|
const char *observation_identifier, gboolean *out_entity_deleted,
|
||||||
|
gboolean *out_entity_shared, GError **error);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#define LABFY_INVESTIGATION_EVIDENCE_ENTITY_DAO_H
|
#define LABFY_INVESTIGATION_EVIDENCE_ENTITY_DAO_H
|
||||||
|
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
|
#include "models/evidence_observation.h"
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
|
||||||
|
|
@ -213,6 +214,27 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
|
||||||
GError **error
|
GError **error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_add_observation(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
|
const char *entity_type,
|
||||||
|
const char *value_raw, const char *value_normalized,
|
||||||
|
const char *role, const char *provenance_kind,
|
||||||
|
const char *source_header, guint occurrence,
|
||||||
|
const char *verification_status, const char *created_at,
|
||||||
|
char **out_observation_identifier, GError **error);
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_promote_observation(
|
||||||
|
EvidenceEntityDao *dao, const char *observation_identifier,
|
||||||
|
const char *entity_identifier, const char *promoted_at,
|
||||||
|
const char *promotion_kind, GError **error);
|
||||||
|
|
||||||
|
/** @brief Construit le texte persistant affiché dans la fiche preuve. */
|
||||||
|
char *evidence_entity_dao_format_observations(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
|
GError **error);
|
||||||
|
GPtrArray *evidence_entity_dao_list_observations(
|
||||||
|
EvidenceEntityDao *dao, const char *evidence_identifier, GError **error);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ 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);
|
bool schema_install_v10(Database *database);
|
||||||
|
bool schema_install_v11(Database *database);
|
||||||
|
bool schema_install_v12(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.
|
||||||
|
|
|
||||||
18
include/models/evidence_observation.h
Normal file
18
include/models/evidence_observation.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#ifndef LABFY_INVESTIGATION_EVIDENCE_OBSERVATION_H
|
||||||
|
#define LABFY_INVESTIGATION_EVIDENCE_OBSERVATION_H
|
||||||
|
#include <glib.h>
|
||||||
|
typedef struct {
|
||||||
|
char *identifier;
|
||||||
|
char *value;
|
||||||
|
char *type_identifier;
|
||||||
|
char *role;
|
||||||
|
char *source_header;
|
||||||
|
guint occurrence;
|
||||||
|
char *provenance_kind;
|
||||||
|
char *verification_status;
|
||||||
|
char *integrated_at;
|
||||||
|
char *entity_identifier;
|
||||||
|
char *promotion_kind;
|
||||||
|
} EvidenceObservation;
|
||||||
|
void evidence_observation_free(EvidenceObservation *observation);
|
||||||
|
#endif
|
||||||
|
|
@ -497,6 +497,10 @@ void main_window_set_selected_evidence(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
const EvidenceRecord *evidence_record
|
const EvidenceRecord *evidence_record
|
||||||
);
|
);
|
||||||
|
void main_window_set_evidence_observations(MainWindow *main_window,
|
||||||
|
const GPtrArray *observations);
|
||||||
|
void main_window_set_observation_remove_callback(MainWindow *main_window,
|
||||||
|
void (*callback)(const char *, gpointer), gpointer user_data);
|
||||||
void main_window_set_eml_analysis_available(
|
void main_window_set_eml_analysis_available(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
gboolean available
|
gboolean available
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include "core/investigation_node.h"
|
#include "core/investigation_node.h"
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
|
#include "models/evidence_observation.h"
|
||||||
#include "models/entity_record.h"
|
#include "models/entity_record.h"
|
||||||
#include "models/osint_action_catalog.h"
|
#include "models/osint_action_catalog.h"
|
||||||
|
|
||||||
|
|
@ -153,6 +154,8 @@ typedef void (*WorkspaceExtractMetadataCallback)(const char *evidence_identifier
|
||||||
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
||||||
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
||||||
const char *evidence_identifier, gpointer user_data);
|
const char *evidence_identifier, gpointer user_data);
|
||||||
|
typedef void (*WorkspaceObservationRemoveCallback)(
|
||||||
|
const char *observation_identifier, gpointer user_data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Crée une nouvelle zone de travail.
|
* @brief Crée une nouvelle zone de travail.
|
||||||
|
|
@ -203,6 +206,10 @@ void workspace_set_selected_evidence(
|
||||||
Workspace *workspace,
|
Workspace *workspace,
|
||||||
const EvidenceRecord *evidence_record
|
const EvidenceRecord *evidence_record
|
||||||
);
|
);
|
||||||
|
void workspace_set_evidence_observations(Workspace *workspace,
|
||||||
|
const GPtrArray *observations);
|
||||||
|
void workspace_set_observation_remove_callback(Workspace *workspace,
|
||||||
|
WorkspaceObservationRemoveCallback callback, gpointer user_data);
|
||||||
void workspace_set_eml_analysis_available(
|
void workspace_set_eml_analysis_available(
|
||||||
Workspace *workspace,
|
Workspace *workspace,
|
||||||
gboolean available
|
gboolean available
|
||||||
|
|
|
||||||
151
opencode.json.backup-7b
Normal file
151
opencode.json.backup-7b
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"enabled_providers": [
|
||||||
|
"llama.cpp"
|
||||||
|
],
|
||||||
|
"provider": {
|
||||||
|
"llama.cpp": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "llama-server local",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://127.0.0.1:8080/v1",
|
||||||
|
"timeout": 600000,
|
||||||
|
"chunkTimeout": 180000
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"qwen3-8b": {
|
||||||
|
"name": "Qwen3 8B — Architecte et reviewer",
|
||||||
|
"limit": {
|
||||||
|
"context": 32768,
|
||||||
|
"output": 2048
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qwen2.5-coder-7b": {
|
||||||
|
"name": "Qwen2.5-Coder 7B — Développeur C17",
|
||||||
|
"limit": {
|
||||||
|
"context": 32768,
|
||||||
|
"output": 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": "llama.cpp/qwen3-8b",
|
||||||
|
"small_model": "llama.cpp/qwen3-8b",
|
||||||
|
"default_agent": "orchestrateur",
|
||||||
|
"subagent_depth": 1,
|
||||||
|
"instructions": [
|
||||||
|
".opencode/labfy.md"
|
||||||
|
],
|
||||||
|
"compaction": {
|
||||||
|
"auto": true,
|
||||||
|
"prune": true,
|
||||||
|
"reserved": 8000
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"external_directory": "deny",
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": "ask"
|
||||||
|
},
|
||||||
|
"agent": {
|
||||||
|
"orchestrateur": {
|
||||||
|
"description": "Agent principal. Inspecte le dépôt, prépare le plan, consulte l’architecte, délègue la production de code C17, applique les changements autorisés, compile, teste et présente le résultat.",
|
||||||
|
"mode": "primary",
|
||||||
|
"model": "llama.cpp/qwen3-8b",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"steps": 40,
|
||||||
|
"permission": {
|
||||||
|
"read": "allow",
|
||||||
|
"glob": "allow",
|
||||||
|
"grep": "allow",
|
||||||
|
"list": "allow",
|
||||||
|
"edit": "ask",
|
||||||
|
"bash": {
|
||||||
|
"*": "ask",
|
||||||
|
"pwd": "allow",
|
||||||
|
"ls *": "allow",
|
||||||
|
"grep *": "allow",
|
||||||
|
"rg *": "allow",
|
||||||
|
"head *": "allow",
|
||||||
|
"tail *": "allow",
|
||||||
|
"file *": "allow",
|
||||||
|
"git status*": "allow",
|
||||||
|
"git diff*": "allow",
|
||||||
|
"git log*": "allow",
|
||||||
|
"make -j8*": "allow",
|
||||||
|
"make test*": "allow",
|
||||||
|
"git commit*": "deny",
|
||||||
|
"git push*": "deny",
|
||||||
|
"git reset*": "deny",
|
||||||
|
"git clean*": "deny",
|
||||||
|
"git checkout*": "deny",
|
||||||
|
"git restore*": "deny",
|
||||||
|
"rm *": "deny",
|
||||||
|
"sudo *": "deny",
|
||||||
|
"doas *": "deny"
|
||||||
|
},
|
||||||
|
"task": {
|
||||||
|
"*": "deny",
|
||||||
|
"architecte-reviewer": "allow",
|
||||||
|
"coder-c17": "allow"
|
||||||
|
},
|
||||||
|
"external_directory": "deny",
|
||||||
|
"todowrite": "allow",
|
||||||
|
"lsp": "allow",
|
||||||
|
"question": "allow",
|
||||||
|
"skill": "deny",
|
||||||
|
"websearch": "allow",
|
||||||
|
"webfetch": "allow"
|
||||||
|
},
|
||||||
|
"prompt": "Tu pilotes le travail dans Labfy Investigation. Pour une nouvelle API, une migration, une modification transversale ou un changement sensible, consulte architecte-reviewer avant l’implémentation. Transmets ensuite à coder-c17 tout le contexte nécessaire : objectif, fichiers pertinents, interfaces, extraits de code, contraintes et diagnostics. Le Coder ne dispose d’aucun outil : il renvoie uniquement du code ou un patch textuel. Tu restes seul responsable de l’application des changements, de l’exécution de make -j8, des tests, de git diff --check et de la validation finale. Ne crée jamais de commit ni de push sans autorisation explicite."
|
||||||
|
},
|
||||||
|
"coder-c17": {
|
||||||
|
"description": "Spécialiste ISO C17 sans outils. Reçoit le contexte transmis par l’orchestrateur et produit du code ou un patch textuel précis.",
|
||||||
|
"mode": "all",
|
||||||
|
"model": "llama.cpp/qwen2.5-coder-7b",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"permission": {
|
||||||
|
"read": "deny",
|
||||||
|
"glob": "deny",
|
||||||
|
"grep": "deny",
|
||||||
|
"list": "deny",
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": "deny",
|
||||||
|
"task": "deny",
|
||||||
|
"external_directory": "deny",
|
||||||
|
"todowrite": "deny",
|
||||||
|
"lsp": "deny",
|
||||||
|
"skill": "deny",
|
||||||
|
"question": "deny",
|
||||||
|
"webfetch": "deny",
|
||||||
|
"websearch": "deny",
|
||||||
|
"doom_loop": "deny"
|
||||||
|
},
|
||||||
|
"prompt": "Tu es le spécialiste C17 de Labfy Investigation. Tu ne disposes d’aucun outil et tu ne peux ni lire le dépôt, ni modifier un fichier, ni compiler, ni exécuter un test. Travaille uniquement à partir du contexte transmis. Produis du code ISO C17 compatible avec -std=c17 -Wall -Wextra -Wpedantic -Werror. Respecte les interfaces existantes, les noms snake_case, les préfixes de modules, l’ownership GLib, l’architecture GTK4/DAO/services et SQLite. Retourne de préférence un diff unifié applicable, ou des blocs de code complets avec les chemins concernés. Signale clairement les hypothèses. Ne prétends jamais avoir lu, modifié, compilé ou testé quoi que ce soit."
|
||||||
|
},
|
||||||
|
"architecte-reviewer": {
|
||||||
|
"description": "Architecte et reviewer en lecture seule. Contrôle l’architecture, la cohérence des API, l’ownership, GTK4/GLib, SQLite, les risques de régression et les tests manquants.",
|
||||||
|
"mode": "subagent",
|
||||||
|
"model": "llama.cpp/qwen3-8b",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"steps": 30,
|
||||||
|
"permission": {
|
||||||
|
"read": "allow",
|
||||||
|
"glob": "allow",
|
||||||
|
"grep": "allow",
|
||||||
|
"list": "allow",
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": "deny",
|
||||||
|
"task": "deny",
|
||||||
|
"external_directory": "deny",
|
||||||
|
"todowrite": "deny",
|
||||||
|
"lsp": "allow",
|
||||||
|
"skill": "deny",
|
||||||
|
"question": "deny",
|
||||||
|
"webfetch": "deny",
|
||||||
|
"websearch": "deny"
|
||||||
|
},
|
||||||
|
"prompt": "Tu es l’architecte et reviewer de Labfy Investigation. Tu travailles strictement en lecture seule. Examine le plan, les fichiers pertinents et le diff. Vérifie la séparation widgets/modèles/DAO/services, la cohérence entre .h et .c, l’ownership et les durées de vie GLib/GTK, les transactions SQLite, la gestion d’erreurs, la sécurité, les régressions et les tests manquants. Classe les remarques par gravité : bloquant, important, amélioration. Propose des corrections précises sans modifier le dépôt et sans créer de commit."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -150,6 +150,22 @@ struct Application
|
||||||
char *pending_entity_selection_identifier;
|
char *pending_entity_selection_identifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void application_on_evidence_selected(
|
||||||
|
const char *evidence_identifier, gpointer user_data);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Application *application;
|
||||||
|
char *observation_identifier;
|
||||||
|
} ApplicationObservationRemovalContext;
|
||||||
|
|
||||||
|
static void application_observation_removal_context_free(
|
||||||
|
ApplicationObservationRemovalContext *context)
|
||||||
|
{
|
||||||
|
if (context == NULL) return;
|
||||||
|
g_free(context->observation_identifier);
|
||||||
|
g_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import.
|
* @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import.
|
||||||
*/
|
*/
|
||||||
|
|
@ -3798,7 +3814,8 @@ static void application_on_eml_selection_completed(
|
||||||
ApplicationEmlReviewContext *context = user_data;
|
ApplicationEmlReviewContext *context = user_data;
|
||||||
Application *application = context != NULL ? context->application : NULL;
|
Application *application = context != NULL ? context->application : NULL;
|
||||||
const InvestigationProject *project = NULL;
|
const InvestigationProject *project = NULL;
|
||||||
GError *error = NULL; guint created = 0, reused = 0; char *message = NULL;
|
GError *error = NULL; guint observations = 0, created = 0, reused = 0;
|
||||||
|
char *message = NULL;
|
||||||
if (proposals == NULL)
|
if (proposals == NULL)
|
||||||
{
|
{
|
||||||
application_remove_staging_directory(context != NULL
|
application_remove_staging_directory(context != NULL
|
||||||
|
|
@ -3808,19 +3825,26 @@ static void application_on_eml_selection_completed(
|
||||||
}
|
}
|
||||||
if (application == NULL || application->session == NULL ||
|
if (application == NULL || application->session == NULL ||
|
||||||
!eml_integration_apply(investigation_session_get_database(application->session),
|
!eml_integration_apply(investigation_session_get_database(application->session),
|
||||||
context->evidence_identifier, proposals, &created, &reused, &error))
|
context->evidence_identifier, proposals, &observations,
|
||||||
|
&created, &reused, &error))
|
||||||
application_present_error(application, "Intégration EML impossible",
|
application_present_error(application, "Intégration EML impossible",
|
||||||
error != NULL ? error->message : "La transaction a échoué.");
|
error != NULL ? error->message : "La transaction a échoué.");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
message = g_strdup_printf("%u entité(s) créée(s), %u réutilisée(s).",
|
message = g_strdup_printf(
|
||||||
created, reused);
|
"%u observation(s) conservée(s), %u entité(s) créée(s), "
|
||||||
|
"%u réutilisée(s).", observations, created, reused);
|
||||||
application_message_dialog_present(main_window_get_window(application->main_window),
|
application_message_dialog_present(main_window_get_window(application->main_window),
|
||||||
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
|
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
|
||||||
project = investigation_session_get_project(application->session);
|
if (created + reused > 0)
|
||||||
application_start_graph_loading(application,
|
{
|
||||||
investigation_project_get_database_path(project));
|
project = investigation_session_get_project(application->session);
|
||||||
|
application_start_graph_loading(application,
|
||||||
|
investigation_project_get_database_path(project));
|
||||||
|
}
|
||||||
(void) application_refresh_evidence_models(application, NULL);
|
(void) application_refresh_evidence_models(application, NULL);
|
||||||
|
application_on_evidence_selected(context->evidence_identifier,
|
||||||
|
application);
|
||||||
}
|
}
|
||||||
g_free(message); g_clear_error(&error); g_ptr_array_unref(proposals);
|
g_free(message); g_clear_error(&error); g_ptr_array_unref(proposals);
|
||||||
application_remove_staging_directory(context != NULL
|
application_remove_staging_directory(context != NULL
|
||||||
|
|
@ -5677,6 +5701,21 @@ static void application_on_evidence_selected(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
evidence_record
|
evidence_record
|
||||||
);
|
);
|
||||||
|
{
|
||||||
|
EvidenceEntityDao *observation_dao =
|
||||||
|
evidence_entity_dao_new(database, &error);
|
||||||
|
GPtrArray *observations = observation_dao != NULL
|
||||||
|
? evidence_entity_dao_list_observations(observation_dao,
|
||||||
|
evidence_identifier, &error) : NULL;
|
||||||
|
if (error == NULL)
|
||||||
|
main_window_set_evidence_observations(application->main_window,
|
||||||
|
observations);
|
||||||
|
else
|
||||||
|
g_warning("Observations EML indisponibles : %s", error->message);
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_clear_pointer(&observations, g_ptr_array_unref);
|
||||||
|
evidence_entity_dao_free(observation_dao);
|
||||||
|
}
|
||||||
|
|
||||||
project = investigation_session_get_project(application->session);
|
project = investigation_session_get_project(application->session);
|
||||||
investigation_root = project != NULL
|
investigation_root = project != NULL
|
||||||
|
|
@ -8019,6 +8058,60 @@ static void application_on_reset_graph_layout_requested(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void application_on_observation_remove_confirmed(
|
||||||
|
gboolean confirmed, gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationObservationRemovalContext *context = user_data;
|
||||||
|
Application *application = context != NULL ? context->application : NULL;
|
||||||
|
GError *error = NULL; gboolean deleted = FALSE, shared = FALSE;
|
||||||
|
if (!confirmed || application == NULL || application->session == NULL)
|
||||||
|
{ application_observation_removal_context_free(context); return; }
|
||||||
|
if (!eml_integration_remove_promotion(
|
||||||
|
investigation_session_get_database(application->session),
|
||||||
|
context->observation_identifier, &deleted, &shared, &error))
|
||||||
|
application_present_error(application, "Retrait du graphe impossible",
|
||||||
|
error != NULL ? error->message : "La transaction a échoué.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (deleted)
|
||||||
|
{
|
||||||
|
const InvestigationProject *project =
|
||||||
|
investigation_session_get_project(application->session);
|
||||||
|
application_start_graph_loading(application,
|
||||||
|
investigation_project_get_database_path(project));
|
||||||
|
}
|
||||||
|
char *selected_evidence = g_strdup(
|
||||||
|
application->selected_evidence_identifier);
|
||||||
|
application_on_evidence_selected(selected_evidence, application);
|
||||||
|
g_free(selected_evidence);
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
deleted ? "Observation conservée et nœud orphelin supprimé." :
|
||||||
|
shared ? "Observation détachée ; entité partagée conservée." :
|
||||||
|
"Observation retirée du graphe.");
|
||||||
|
}
|
||||||
|
g_clear_error(&error);
|
||||||
|
application_observation_removal_context_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_observation_remove_requested(
|
||||||
|
const char *observation_identifier, gpointer user_data)
|
||||||
|
{
|
||||||
|
Application *application = user_data;
|
||||||
|
if (application == NULL ||
|
||||||
|
!g_uuid_string_is_valid(observation_identifier)) return;
|
||||||
|
ApplicationObservationRemovalContext *context =
|
||||||
|
g_new0(ApplicationObservationRemovalContext, 1);
|
||||||
|
context->application = application;
|
||||||
|
context->observation_identifier = g_strdup(observation_identifier);
|
||||||
|
application_message_dialog_present_confirmation(
|
||||||
|
main_window_get_window(application->main_window),
|
||||||
|
APPLICATION_MESSAGE_DIALOG_WARNING, "Retirer du graphe",
|
||||||
|
"L’observation restera dans la fiche. Le nœud sera supprimé "
|
||||||
|
"uniquement s’il n’est utilisé par aucune autre preuve, observation "
|
||||||
|
"ou relation.", "Retirer du graphe",
|
||||||
|
application_on_observation_remove_confirmed, context);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Ferme proprement l'application.
|
* @brief Ferme proprement l'application.
|
||||||
*
|
*
|
||||||
|
|
@ -8102,6 +8195,8 @@ static void application_on_activate(
|
||||||
application_on_evidence_selected,
|
application_on_evidence_selected,
|
||||||
application
|
application
|
||||||
);
|
);
|
||||||
|
main_window_set_observation_remove_callback(application->main_window,
|
||||||
|
application_on_observation_remove_requested, application);
|
||||||
|
|
||||||
main_window_set_verify_evidence_callback(
|
main_window_set_verify_evidence_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
|
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "core/eml_analyzer.h"
|
#include "core/eml_analyzer.h"
|
||||||
|
#include <gio/gio.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
|
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
|
||||||
|
|
@ -15,9 +16,35 @@ struct EmlAnalysis
|
||||||
GPtrArray *ips;
|
GPtrArray *ips;
|
||||||
GPtrArray *sender_ips;
|
GPtrArray *sender_ips;
|
||||||
GPtrArray *destination_ips;
|
GPtrArray *destination_ips;
|
||||||
|
GPtrArray *observations;
|
||||||
char *raw_headers;
|
char *raw_headers;
|
||||||
char *date_utc;
|
char *date_utc;
|
||||||
};
|
};
|
||||||
|
void eml_observation_free(EmlObservation *observation)
|
||||||
|
{
|
||||||
|
if (observation == NULL) return;
|
||||||
|
g_free(observation->type_identifier);
|
||||||
|
g_free(observation->value_raw);
|
||||||
|
g_free(observation->value_normalized);
|
||||||
|
g_free(observation->role);
|
||||||
|
g_free(observation->source_header);
|
||||||
|
g_free(observation->provenance_kind);
|
||||||
|
g_free(observation);
|
||||||
|
}
|
||||||
|
static void eml_analyzer_add_observation(EmlAnalysis *analysis,
|
||||||
|
const char *type, const char *raw, const char *normalized,
|
||||||
|
const char *role, const char *header, guint occurrence)
|
||||||
|
{
|
||||||
|
EmlObservation *observation = g_new0(EmlObservation, 1);
|
||||||
|
observation->type_identifier = g_strdup(type);
|
||||||
|
observation->value_raw = g_strdup(raw);
|
||||||
|
observation->value_normalized = g_strdup(normalized);
|
||||||
|
observation->role = g_strdup(role);
|
||||||
|
observation->source_header = g_strdup(header);
|
||||||
|
observation->occurrence = occurrence;
|
||||||
|
observation->provenance_kind = g_strdup("header");
|
||||||
|
g_ptr_array_add(analysis->observations, observation);
|
||||||
|
}
|
||||||
|
|
||||||
static gint eml_analyzer_month_number(const char *month)
|
static gint eml_analyzer_month_number(const char *month)
|
||||||
{
|
{
|
||||||
|
|
@ -153,6 +180,129 @@ static void eml_analyzer_extract_regex(GRegex *regex, const char *text,
|
||||||
}
|
}
|
||||||
g_match_info_free(matches);
|
g_match_info_free(matches);
|
||||||
}
|
}
|
||||||
|
static gboolean eml_analyzer_valid_domain(const char *value)
|
||||||
|
{
|
||||||
|
gboolean has_dot = FALSE;
|
||||||
|
const char *label = value;
|
||||||
|
GInetAddress *address = value != NULL
|
||||||
|
? g_inet_address_new_from_string(value) : NULL;
|
||||||
|
if (value == NULL) return FALSE;
|
||||||
|
if (address != NULL) { g_object_unref(address); return FALSE; }
|
||||||
|
for (const char *cursor = value; *cursor != '\0'; cursor++)
|
||||||
|
{
|
||||||
|
if (*cursor == '.')
|
||||||
|
{
|
||||||
|
if (cursor == label || cursor[-1] == '-') return FALSE;
|
||||||
|
has_dot = TRUE; label = cursor + 1;
|
||||||
|
}
|
||||||
|
else if (!(g_ascii_isalnum(*cursor) || *cursor == '-') ||
|
||||||
|
(cursor == label && *cursor == '-')) return FALSE;
|
||||||
|
}
|
||||||
|
if (!has_dot || *label == '\0' || label[strlen(label) - 1] == '-')
|
||||||
|
return FALSE;
|
||||||
|
for (const char *cursor = label; *cursor != '\0'; cursor++)
|
||||||
|
if (g_ascii_isalpha(*cursor)) return strlen(label) >= 2;
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
static void eml_analyzer_observe_email_header(EmlAnalysis *analysis,
|
||||||
|
GRegex *email_regex, const char *header, const char *role)
|
||||||
|
{
|
||||||
|
const GPtrArray *values = eml_analysis_get_header_values(analysis, header);
|
||||||
|
for (guint index = 0; values != NULL && index < values->len; index++)
|
||||||
|
{
|
||||||
|
const char *value = g_ptr_array_index((GPtrArray *) values, index);
|
||||||
|
GMatchInfo *matches = NULL;
|
||||||
|
g_regex_match(email_regex, value, 0, &matches);
|
||||||
|
while (g_match_info_matches(matches))
|
||||||
|
{
|
||||||
|
char *raw = g_match_info_fetch(matches, 1);
|
||||||
|
char *normalized = g_ascii_strdown(raw, -1);
|
||||||
|
eml_analyzer_add_unique(analysis->emails, normalized, FALSE);
|
||||||
|
eml_analyzer_add_observation(analysis, "email_address", raw,
|
||||||
|
normalized, role, header, index + 1);
|
||||||
|
const char *at = strrchr(normalized, '@');
|
||||||
|
if (at != NULL && eml_analyzer_valid_domain(at + 1))
|
||||||
|
{
|
||||||
|
eml_analyzer_add_unique(analysis->domains, at + 1, TRUE);
|
||||||
|
eml_analyzer_add_observation(analysis, "domain_name", at + 1,
|
||||||
|
at + 1, role, header, index + 1);
|
||||||
|
}
|
||||||
|
g_free(normalized); g_free(raw);
|
||||||
|
if (!g_match_info_next(matches, NULL)) break;
|
||||||
|
}
|
||||||
|
g_match_info_free(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void eml_analyzer_observe_received(EmlAnalysis *analysis,
|
||||||
|
GRegex *ip_regex, GRegex *host_regex)
|
||||||
|
{
|
||||||
|
const GPtrArray *values = eml_analysis_get_header_values(analysis, "received");
|
||||||
|
for (guint index = 0; values != NULL && index < values->len; index++)
|
||||||
|
{
|
||||||
|
const char *value = g_ptr_array_index((GPtrArray *) values, index);
|
||||||
|
GMatchInfo *matches = NULL;
|
||||||
|
g_regex_match(ip_regex, value, 0, &matches);
|
||||||
|
while (g_match_info_matches(matches))
|
||||||
|
{
|
||||||
|
char *raw = g_match_info_fetch(matches, 1);
|
||||||
|
GInetAddress *address = g_inet_address_new_from_string(raw);
|
||||||
|
if (address != NULL)
|
||||||
|
{
|
||||||
|
char *normalized = g_inet_address_to_string(address);
|
||||||
|
eml_analyzer_add_unique(analysis->ips, normalized, FALSE);
|
||||||
|
eml_analyzer_add_observation(analysis, "ip_address", raw,
|
||||||
|
normalized, "smtp_relay", "received", index + 1);
|
||||||
|
g_free(normalized); g_object_unref(address);
|
||||||
|
}
|
||||||
|
g_free(raw);
|
||||||
|
if (!g_match_info_next(matches, NULL)) break;
|
||||||
|
}
|
||||||
|
g_match_info_free(matches);
|
||||||
|
g_regex_match(host_regex, value, 0, &matches);
|
||||||
|
while (g_match_info_matches(matches))
|
||||||
|
{
|
||||||
|
char *raw = g_match_info_fetch(matches, 1);
|
||||||
|
char *normalized = g_ascii_strdown(raw, -1);
|
||||||
|
if (eml_analyzer_valid_domain(normalized))
|
||||||
|
{
|
||||||
|
eml_analyzer_add_unique(analysis->domains, normalized, FALSE);
|
||||||
|
eml_analyzer_add_observation(analysis, "domain_name", raw,
|
||||||
|
normalized, "smtp_relay", "received", index + 1);
|
||||||
|
}
|
||||||
|
g_free(normalized); g_free(raw);
|
||||||
|
if (!g_match_info_next(matches, NULL)) break;
|
||||||
|
}
|
||||||
|
g_match_info_free(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void eml_analyzer_observe_received_ipv6(EmlAnalysis *analysis,
|
||||||
|
GRegex *regex)
|
||||||
|
{
|
||||||
|
const GPtrArray *values = eml_analysis_get_header_values(analysis, "received");
|
||||||
|
for (guint index = 0; values != NULL && index < values->len; index++)
|
||||||
|
{
|
||||||
|
const char *value = g_ptr_array_index((GPtrArray *) values, index);
|
||||||
|
GMatchInfo *matches = NULL;
|
||||||
|
g_regex_match(regex, value, 0, &matches);
|
||||||
|
while (g_match_info_matches(matches))
|
||||||
|
{
|
||||||
|
char *raw = g_match_info_fetch(matches, 1);
|
||||||
|
GInetAddress *address = g_inet_address_new_from_string(raw);
|
||||||
|
if (address != NULL &&
|
||||||
|
g_inet_address_get_family(address) == G_SOCKET_FAMILY_IPV6)
|
||||||
|
{
|
||||||
|
char *normalized = g_inet_address_to_string(address);
|
||||||
|
eml_analyzer_add_unique(analysis->ips, normalized, FALSE);
|
||||||
|
eml_analyzer_add_observation(analysis, "ip_address", raw,
|
||||||
|
normalized, "smtp_relay", "received", index + 1);
|
||||||
|
g_free(normalized);
|
||||||
|
}
|
||||||
|
g_clear_object(&address); g_free(raw);
|
||||||
|
if (!g_match_info_next(matches, NULL)) break;
|
||||||
|
}
|
||||||
|
g_match_info_free(matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
/** @brief Extrait les IP d'une portion nommée d'un en-tête Received. */
|
/** @brief Extrait les IP d'une portion nommée d'un en-tête Received. */
|
||||||
static void eml_analyzer_extract_received_part(GRegex *part_regex,
|
static void eml_analyzer_extract_received_part(GRegex *part_regex,
|
||||||
GRegex *ip_regex, const char *received, GPtrArray *values)
|
GRegex *ip_regex, const char *received, GPtrArray *values)
|
||||||
|
|
@ -199,6 +349,7 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
|
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
|
||||||
GString *current_value = NULL;
|
GString *current_value = NULL;
|
||||||
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
|
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
|
||||||
|
GRegex *ipv6_regex = NULL;
|
||||||
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
|
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
if (file_path == NULL || file_path[0] == '\0')
|
if (file_path == NULL || file_path[0] == '\0')
|
||||||
|
|
@ -226,6 +377,8 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
analysis->ips = g_ptr_array_new_with_free_func(g_free);
|
analysis->ips = g_ptr_array_new_with_free_func(g_free);
|
||||||
analysis->sender_ips = g_ptr_array_new_with_free_func(g_free);
|
analysis->sender_ips = g_ptr_array_new_with_free_func(g_free);
|
||||||
analysis->destination_ips = g_ptr_array_new_with_free_func(g_free);
|
analysis->destination_ips = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
analysis->observations = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) eml_observation_free);
|
||||||
analysis->raw_headers = g_utf8_make_valid(data, (gssize) header_size);
|
analysis->raw_headers = g_utf8_make_valid(data, (gssize) header_size);
|
||||||
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
|
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
|
||||||
current_value = g_string_new(NULL);
|
current_value = g_string_new(NULL);
|
||||||
|
|
@ -245,13 +398,42 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
if (current_name != NULL) eml_analyzer_add_header(analysis,
|
if (current_name != NULL) eml_analyzer_add_header(analysis,
|
||||||
current_name, current_value->str);
|
current_name, current_value->str);
|
||||||
email_regex = g_regex_new("([A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})", 0, 0, NULL);
|
email_regex = g_regex_new("([A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})", 0, 0, NULL);
|
||||||
domain_regex = g_regex_new("(?:@|[.\\s<([])([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)", 0, 0, NULL);
|
domain_regex = g_regex_new("(?i)\\b([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+)\\b", 0, 0, NULL);
|
||||||
ip_regex = g_regex_new("(?:^|[^0-9])([0-9]{1,3}(?:\\.[0-9]{1,3}){3})(?:[^0-9]|$)", 0, 0, NULL);
|
ip_regex = g_regex_new("(?:^|[^0-9])([0-9]{1,3}(?:\\.[0-9]{1,3}){3})(?:[^0-9]|$)", 0, 0, NULL);
|
||||||
|
ipv6_regex = g_regex_new("(?i)(?:\\[)?([0-9a-f]*:[0-9a-f:]+)(?:\\])?", 0, 0, NULL);
|
||||||
received_from_regex = g_regex_new("(?i)\\bfrom\\b(.*?)\\bby\\b", 0, 0, NULL);
|
received_from_regex = g_regex_new("(?i)\\bfrom\\b(.*?)\\bby\\b", 0, 0, NULL);
|
||||||
received_by_regex = g_regex_new("(?i)\\bby\\b(.*?)(?:\\bwith\\b|;|$)", 0, 0, NULL);
|
received_by_regex = g_regex_new("(?i)\\bby\\b(.*?)(?:\\bwith\\b|;|$)", 0, 0, NULL);
|
||||||
eml_analyzer_extract_regex(email_regex, analysis->raw_headers, analysis->emails, TRUE);
|
static const struct { const char *header; const char *role; } email_headers[] = {
|
||||||
eml_analyzer_extract_regex(domain_regex, analysis->raw_headers, analysis->domains, TRUE);
|
{ "from", "from" }, { "sender", "sender" },
|
||||||
eml_analyzer_extract_regex(ip_regex, analysis->raw_headers, analysis->ips, FALSE);
|
{ "reply-to", "reply_to" }, { "return-path", "return_path" },
|
||||||
|
{ "to", "to" }, { "cc", "cc" }, { "bcc", "bcc" }
|
||||||
|
};
|
||||||
|
for (guint i = 0; i < G_N_ELEMENTS(email_headers); i++)
|
||||||
|
eml_analyzer_observe_email_header(analysis, email_regex,
|
||||||
|
email_headers[i].header, email_headers[i].role);
|
||||||
|
const char *message_id = eml_analysis_get_first_header(analysis, "message-id");
|
||||||
|
if (message_id != NULL)
|
||||||
|
{
|
||||||
|
GMatchInfo *matches = NULL;
|
||||||
|
g_regex_match(email_regex, message_id, 0, &matches);
|
||||||
|
if (g_match_info_matches(matches))
|
||||||
|
{
|
||||||
|
char *identifier = g_match_info_fetch(matches, 1);
|
||||||
|
const char *at = strrchr(identifier, '@');
|
||||||
|
if (at != NULL && eml_analyzer_valid_domain(at + 1))
|
||||||
|
{
|
||||||
|
char *normalized = g_ascii_strdown(at + 1, -1);
|
||||||
|
eml_analyzer_add_unique(analysis->domains, normalized, FALSE);
|
||||||
|
eml_analyzer_add_observation(analysis, "domain_name", at + 1,
|
||||||
|
normalized, "message_id_domain", "message-id", 1);
|
||||||
|
g_free(normalized);
|
||||||
|
}
|
||||||
|
g_free(identifier);
|
||||||
|
}
|
||||||
|
g_match_info_free(matches);
|
||||||
|
}
|
||||||
|
eml_analyzer_observe_received(analysis, ip_regex, domain_regex);
|
||||||
|
eml_analyzer_observe_received_ipv6(analysis, ipv6_regex);
|
||||||
const GPtrArray *received_values = eml_analysis_get_header_values(
|
const GPtrArray *received_values = eml_analysis_get_header_values(
|
||||||
analysis, "received");
|
analysis, "received");
|
||||||
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
|
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
|
||||||
|
|
@ -269,6 +451,7 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
cleanup:
|
cleanup:
|
||||||
g_clear_pointer(&email_regex, g_regex_unref); g_clear_pointer(&domain_regex, g_regex_unref);
|
g_clear_pointer(&email_regex, g_regex_unref); g_clear_pointer(&domain_regex, g_regex_unref);
|
||||||
g_clear_pointer(&ip_regex, g_regex_unref); g_clear_pointer(¤t_name, g_free);
|
g_clear_pointer(&ip_regex, g_regex_unref); g_clear_pointer(¤t_name, g_free);
|
||||||
|
g_clear_pointer(&ipv6_regex, g_regex_unref);
|
||||||
g_clear_pointer(&received_from_regex, g_regex_unref);
|
g_clear_pointer(&received_from_regex, g_regex_unref);
|
||||||
g_clear_pointer(&received_by_regex, g_regex_unref);
|
g_clear_pointer(&received_by_regex, g_regex_unref);
|
||||||
if (current_value != NULL) g_string_free(current_value, TRUE);
|
if (current_value != NULL) g_string_free(current_value, TRUE);
|
||||||
|
|
@ -282,6 +465,7 @@ void eml_analysis_free(EmlAnalysis *analysis)
|
||||||
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
|
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
|
||||||
g_ptr_array_unref(analysis->sender_ips);
|
g_ptr_array_unref(analysis->sender_ips);
|
||||||
g_ptr_array_unref(analysis->destination_ips);
|
g_ptr_array_unref(analysis->destination_ips);
|
||||||
|
g_ptr_array_unref(analysis->observations);
|
||||||
g_free(analysis->raw_headers); g_free(analysis->date_utc); g_free(analysis);
|
g_free(analysis->raw_headers); g_free(analysis->date_utc); g_free(analysis);
|
||||||
}
|
}
|
||||||
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
||||||
|
|
@ -300,6 +484,7 @@ const char *eml_analysis_get_first_header(const EmlAnalysis *analysis,
|
||||||
}
|
}
|
||||||
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *a) { return a != NULL ? a->emails : NULL; }
|
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *a) { return a != NULL ? a->emails : NULL; }
|
||||||
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *a) { return a != NULL ? a->domains : NULL; }
|
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *a) { return a != NULL ? a->domains : NULL; }
|
||||||
|
const GPtrArray *eml_analysis_get_observations(const EmlAnalysis *a) { return a != NULL ? a->observations : NULL; }
|
||||||
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->ips : NULL; }
|
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->ips : NULL; }
|
||||||
const GPtrArray *eml_analysis_get_sender_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->sender_ips : NULL; }
|
const GPtrArray *eml_analysis_get_sender_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->sender_ips : NULL; }
|
||||||
const GPtrArray *eml_analysis_get_destination_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->destination_ips : NULL; }
|
const GPtrArray *eml_analysis_get_destination_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->destination_ips : NULL; }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#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 "database/statement.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)
|
||||||
|
|
@ -26,16 +27,40 @@ EmlEntityProposal *eml_entity_proposal_new_with_metadata(const char *type,
|
||||||
? verification_status : "proposed");
|
? verification_status : "proposed");
|
||||||
proposal->provenance_kind = g_strdup(provenance_kind != NULL
|
proposal->provenance_kind = g_strdup(provenance_kind != NULL
|
||||||
? provenance_kind : "header");
|
? provenance_kind : "header");
|
||||||
|
proposal->value_raw = g_strdup(value);
|
||||||
|
proposal->role = g_strdup("other");
|
||||||
|
proposal->source_header = g_strdup("manual");
|
||||||
|
proposal->occurrence = 1;
|
||||||
if (proposal->type_identifier == NULL || proposal->value == NULL ||
|
if (proposal->type_identifier == NULL || proposal->value == NULL ||
|
||||||
proposal->verification_status == NULL || proposal->provenance_kind == 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;
|
||||||
}
|
}
|
||||||
|
EmlEntityProposal *eml_entity_proposal_new_observation(const char *type,
|
||||||
|
const char *raw, const char *normalized, const char *role,
|
||||||
|
const char *source_header, guint occurrence,
|
||||||
|
const char *verification_status, const char *provenance_kind)
|
||||||
|
{
|
||||||
|
EmlEntityProposal *proposal = eml_entity_proposal_new_with_metadata(type,
|
||||||
|
normalized, verification_status, provenance_kind);
|
||||||
|
if (proposal == NULL) return NULL;
|
||||||
|
g_free(proposal->value_raw); proposal->value_raw = g_strdup(raw);
|
||||||
|
g_free(proposal->role); proposal->role = g_strdup(role);
|
||||||
|
g_free(proposal->source_header);
|
||||||
|
proposal->source_header = g_strdup(source_header);
|
||||||
|
proposal->occurrence = occurrence;
|
||||||
|
if (proposal->value_raw == NULL || proposal->role == NULL ||
|
||||||
|
proposal->source_header == NULL || occurrence == 0)
|
||||||
|
{ eml_entity_proposal_free(proposal); return NULL; }
|
||||||
|
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->type_identifier); g_free(proposal->value);
|
||||||
g_free(proposal->verification_status); g_free(proposal->provenance_kind);
|
g_free(proposal->verification_status); g_free(proposal->provenance_kind);
|
||||||
|
g_free(proposal->value_raw); g_free(proposal->role);
|
||||||
|
g_free(proposal->source_header);
|
||||||
g_free(proposal);
|
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. */
|
||||||
|
|
@ -53,7 +78,8 @@ static const EntityRecord *eml_integration_find_existing(const GPtrArray *entiti
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
|
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
|
||||||
const GPtrArray *proposals, guint *out_created, guint *out_reused, GError **error)
|
const GPtrArray *proposals, guint *out_observations,
|
||||||
|
guint *out_created, guint *out_reused, GError **error)
|
||||||
{
|
{
|
||||||
EntityDao *entity_dao = NULL;
|
EntityDao *entity_dao = NULL;
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
|
|
@ -62,6 +88,7 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
char *timestamp = NULL;
|
char *timestamp = NULL;
|
||||||
guint created = 0;
|
guint created = 0;
|
||||||
guint reused = 0;
|
guint reused = 0;
|
||||||
|
guint observations = 0;
|
||||||
gboolean active = FALSE;
|
gboolean active = FALSE;
|
||||||
gboolean success = FALSE;
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
|
@ -71,6 +98,8 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
*out_created = 0;
|
*out_created = 0;
|
||||||
if (out_reused != NULL)
|
if (out_reused != NULL)
|
||||||
*out_reused = 0;
|
*out_reused = 0;
|
||||||
|
if (out_observations != NULL)
|
||||||
|
*out_observations = 0;
|
||||||
|
|
||||||
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
|
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -84,6 +113,10 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
const EmlEntityProposal *proposal = g_ptr_array_index(
|
const EmlEntityProposal *proposal = g_ptr_array_index(
|
||||||
(GPtrArray *) proposals, i);
|
(GPtrArray *) proposals, i);
|
||||||
if (proposal == NULL ||
|
if (proposal == NULL ||
|
||||||
|
proposal->role == NULL || proposal->role[0] == '\0' ||
|
||||||
|
proposal->source_header == NULL ||
|
||||||
|
proposal->source_header[0] == '\0' ||
|
||||||
|
proposal->occurrence == 0 ||
|
||||||
!controlled_vocab_is_valid_verification_status(
|
!controlled_vocab_is_valid_verification_status(
|
||||||
proposal->verification_status) ||
|
proposal->verification_status) ||
|
||||||
!controlled_vocab_is_valid_provenance_kind(
|
!controlled_vocab_is_valid_provenance_kind(
|
||||||
|
|
@ -116,6 +149,17 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
for (guint i = 0; i < proposals->len; i++)
|
for (guint i = 0; i < proposals->len; i++)
|
||||||
{
|
{
|
||||||
EmlEntityProposal *proposal = g_ptr_array_index((GPtrArray *) proposals, i);
|
EmlEntityProposal *proposal = g_ptr_array_index((GPtrArray *) proposals, i);
|
||||||
|
char *observation_identifier = NULL;
|
||||||
|
if (!evidence_entity_dao_add_observation(link_dao,
|
||||||
|
evidence_identifier, proposal->type_identifier,
|
||||||
|
proposal->value_raw, proposal->value, proposal->role,
|
||||||
|
proposal->provenance_kind, proposal->source_header,
|
||||||
|
proposal->occurrence, proposal->verification_status,
|
||||||
|
timestamp, &observation_identifier, error))
|
||||||
|
goto cleanup;
|
||||||
|
observations++;
|
||||||
|
if (!proposal->promote_to_entity)
|
||||||
|
{ g_free(observation_identifier); continue; }
|
||||||
const EntityRecord *existing = eml_integration_find_existing(entities, proposal);
|
const EntityRecord *existing = eml_integration_find_existing(entities, proposal);
|
||||||
const char *identifier = existing != NULL
|
const char *identifier = existing != NULL
|
||||||
? entity_record_get_identifier(existing) : NULL;
|
? entity_record_get_identifier(existing) : NULL;
|
||||||
|
|
@ -129,7 +173,8 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
"Indicateur extrait des en-têtes de la preuve EML.", 50,
|
"Indicateur extrait des en-têtes de la preuve EML.", 50,
|
||||||
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
|
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
|
||||||
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
|
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
|
||||||
{ entity_record_free(new_record); g_free(new_identifier); goto cleanup; }
|
{ entity_record_free(new_record); g_free(observation_identifier);
|
||||||
|
g_free(new_identifier); goto cleanup; }
|
||||||
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
||||||
}
|
}
|
||||||
else reused++;
|
else reused++;
|
||||||
|
|
@ -137,13 +182,19 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
identifier, &linked, error) ||
|
identifier, &linked, error) ||
|
||||||
(!linked && !evidence_entity_dao_link(link_dao,
|
(!linked && !evidence_entity_dao_link(link_dao,
|
||||||
evidence_identifier, identifier, error)))
|
evidence_identifier, identifier, error)))
|
||||||
{ g_free(new_identifier); goto cleanup; }
|
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
||||||
|
if (!evidence_entity_dao_promote_observation(link_dao,
|
||||||
|
observation_identifier, identifier, timestamp,
|
||||||
|
existing != NULL ? "reused" : "created", error))
|
||||||
|
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
||||||
|
g_free(observation_identifier);
|
||||||
g_free(new_identifier);
|
g_free(new_identifier);
|
||||||
}
|
}
|
||||||
if (!database_transaction_commit(database)) goto cleanup;
|
if (!database_transaction_commit(database)) goto cleanup;
|
||||||
active = FALSE; success = TRUE;
|
active = FALSE; success = TRUE;
|
||||||
if (out_created != NULL) *out_created = created;
|
if (out_created != NULL) *out_created = created;
|
||||||
if (out_reused != NULL) *out_reused = reused;
|
if (out_reused != NULL) *out_reused = reused;
|
||||||
|
if (out_observations != NULL) *out_observations = observations;
|
||||||
cleanup:
|
cleanup:
|
||||||
if (!success && active) database_transaction_rollback(database);
|
if (!success && active) database_transaction_rollback(database);
|
||||||
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
|
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
|
||||||
|
|
@ -151,3 +202,115 @@ cleanup:
|
||||||
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
|
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gboolean eml_integration_read_count(Database *database,
|
||||||
|
const char *sql, const char *identifier, gint64 *out_count)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||||
|
gboolean success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, identifier) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
|
||||||
|
database_statement_column_int64(statement, 0, out_count);
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean eml_integration_remove_promotion(Database *database,
|
||||||
|
const char *observation_identifier, gboolean *out_entity_deleted,
|
||||||
|
gboolean *out_entity_shared, GError **error)
|
||||||
|
{
|
||||||
|
static const char *read_sql =
|
||||||
|
"SELECT evidence_id,entity_id FROM evidence_entity_observations "
|
||||||
|
"WHERE id=?;";
|
||||||
|
static const char *detach_sql =
|
||||||
|
"UPDATE evidence_entity_observations SET entity_id=NULL,"
|
||||||
|
"promoted_at=NULL,promotion_kind=NULL WHERE id=? AND entity_id=?;";
|
||||||
|
static const char *same_evidence_sql =
|
||||||
|
"SELECT COUNT(*) FROM evidence_entity_observations "
|
||||||
|
"WHERE evidence_id=?1 AND entity_id=?2;";
|
||||||
|
static const char *unlink_sql =
|
||||||
|
"DELETE FROM preuve_entites WHERE preuve_id=?1 AND entite_id=?2;";
|
||||||
|
static const char *dependency_sql =
|
||||||
|
"SELECT "
|
||||||
|
"(SELECT COUNT(*) FROM evidence_entity_observations WHERE entity_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM preuve_entites WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM relations WHERE entite_source_id=?1 OR entite_cible_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM tag_entites WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM recherche_entites WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM entite_chronologie WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM hypothese_entites WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM osint_execution_entities WHERE entity_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM comptes_sociaux WHERE entite_id=?1)+"
|
||||||
|
"(SELECT COUNT(*) FROM person_roles WHERE entity_id=?1);";
|
||||||
|
static const char *delete_sql = "DELETE FROM entites WHERE id=?;";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
char *evidence_identifier = NULL, *entity_identifier = NULL;
|
||||||
|
gint64 count = 0; gboolean active = FALSE, success = FALSE;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (out_entity_deleted != NULL) *out_entity_deleted = FALSE;
|
||||||
|
if (out_entity_shared != NULL) *out_entity_shared = FALSE;
|
||||||
|
if (database == NULL || !g_uuid_string_is_valid(observation_identifier))
|
||||||
|
return FALSE;
|
||||||
|
if (!database_transaction_begin(database)) return FALSE;
|
||||||
|
active = TRUE;
|
||||||
|
statement = database_statement_prepare(database, read_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, observation_identifier) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW ||
|
||||||
|
!database_statement_column_text(statement, 0, &evidence_identifier) ||
|
||||||
|
!database_statement_column_text(statement, 1, &entity_identifier))
|
||||||
|
goto cleanup;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
if (entity_identifier == NULL)
|
||||||
|
{
|
||||||
|
g_set_error_literal(error,
|
||||||
|
g_quark_from_static_string("eml-integration-error"), 4,
|
||||||
|
"Cette observation n’est pas ajoutée au graphe.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
statement = database_statement_prepare(database, detach_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, observation_identifier) ||
|
||||||
|
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
goto cleanup;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
statement = database_statement_prepare(database, same_evidence_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, evidence_identifier) ||
|
||||||
|
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW ||
|
||||||
|
!database_statement_column_int64(statement, 0, &count))
|
||||||
|
goto cleanup;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
statement = database_statement_prepare(database, unlink_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, evidence_identifier) ||
|
||||||
|
!database_statement_bind_text(statement, 2, entity_identifier) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
goto cleanup;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
}
|
||||||
|
if (!eml_integration_read_count(database, dependency_sql,
|
||||||
|
entity_identifier, &count)) goto cleanup;
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
statement = database_statement_prepare(database, delete_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, entity_identifier) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
goto cleanup;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
if (out_entity_deleted != NULL) *out_entity_deleted = TRUE;
|
||||||
|
}
|
||||||
|
else if (out_entity_shared != NULL) *out_entity_shared = TRUE;
|
||||||
|
if (!database_transaction_commit(database)) goto cleanup;
|
||||||
|
active = FALSE; success = TRUE;
|
||||||
|
cleanup:
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
if (!success && active) database_transaction_rollback(database);
|
||||||
|
g_free(evidence_identifier); g_free(entity_identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1067,3 +1067,218 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_add_observation(EvidenceEntityDao *dao,
|
||||||
|
const char *evidence_identifier, const char *entity_type,
|
||||||
|
const char *value_raw,
|
||||||
|
const char *value_normalized, const char *role,
|
||||||
|
const char *provenance_kind, const char *source_header,
|
||||||
|
guint occurrence, const char *verification_status,
|
||||||
|
const char *created_at, char **out_observation_identifier, GError **error)
|
||||||
|
{
|
||||||
|
static const char *sql =
|
||||||
|
"INSERT OR IGNORE INTO evidence_entity_observations("
|
||||||
|
"id,evidence_id,entity_type,value_raw,value_normalized,role,"
|
||||||
|
"provenance_kind,source_header,occurrence,verification_status,"
|
||||||
|
"observed_at,integrated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?);";
|
||||||
|
static const char *find_sql =
|
||||||
|
"SELECT id FROM evidence_entity_observations WHERE evidence_id=? "
|
||||||
|
"AND entity_type=? AND value_normalized=? AND role=? "
|
||||||
|
"AND source_header=? AND occurrence=? AND provenance_kind=? "
|
||||||
|
"AND extraction_id IS NULL LIMIT 1;";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
char *new_identifier = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (out_observation_identifier != NULL) *out_observation_identifier = NULL;
|
||||||
|
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
|
||||||
|
"L'identifiant de preuve est invalide.", error) ||
|
||||||
|
entity_type == NULL ||
|
||||||
|
value_raw == NULL || value_normalized == NULL || role == NULL ||
|
||||||
|
source_header == NULL || occurrence == 0 || created_at == NULL)
|
||||||
|
return FALSE;
|
||||||
|
new_identifier = g_uuid_string_random();
|
||||||
|
statement = database_statement_prepare(dao->database, sql);
|
||||||
|
if (statement == NULL) goto cleanup;
|
||||||
|
success =
|
||||||
|
database_statement_bind_text(statement, 1, new_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, evidence_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 3, entity_type) &&
|
||||||
|
database_statement_bind_text(statement, 4, value_raw) &&
|
||||||
|
database_statement_bind_text(statement, 5, value_normalized) &&
|
||||||
|
database_statement_bind_text(statement, 6, role) &&
|
||||||
|
database_statement_bind_text(statement, 7, provenance_kind) &&
|
||||||
|
database_statement_bind_text(statement, 8, source_header) &&
|
||||||
|
database_statement_bind_int64(statement, 9, occurrence) &&
|
||||||
|
database_statement_bind_text(statement, 10, verification_status) &&
|
||||||
|
database_statement_bind_text(statement, 11, created_at) &&
|
||||||
|
database_statement_bind_text(statement, 12, created_at) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
database_statement_finalize(statement); statement = NULL;
|
||||||
|
if (!success) goto cleanup;
|
||||||
|
statement = database_statement_prepare(dao->database, find_sql);
|
||||||
|
success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, evidence_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, entity_type) &&
|
||||||
|
database_statement_bind_text(statement, 3, value_normalized) &&
|
||||||
|
database_statement_bind_text(statement, 4, role) &&
|
||||||
|
database_statement_bind_text(statement, 5, source_header) &&
|
||||||
|
database_statement_bind_int64(statement, 6, occurrence) &&
|
||||||
|
database_statement_bind_text(statement, 7, provenance_kind) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
|
||||||
|
database_statement_column_text(statement, 0,
|
||||||
|
out_observation_identifier);
|
||||||
|
cleanup:
|
||||||
|
if (!success)
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible d’enregistrer l’observation preuve-entité");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
g_free(new_identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean evidence_entity_dao_promote_observation(EvidenceEntityDao *dao,
|
||||||
|
const char *observation_identifier, const char *entity_identifier,
|
||||||
|
const char *promoted_at, const char *promotion_kind, GError **error)
|
||||||
|
{
|
||||||
|
static const char *sql =
|
||||||
|
"UPDATE evidence_entity_observations SET entity_id=?,promoted_at=?,"
|
||||||
|
"promotion_kind=? WHERE id=? AND (entity_id IS NULL OR entity_id=?);";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (dao == NULL || !g_uuid_string_is_valid(observation_identifier) ||
|
||||||
|
!g_uuid_string_is_valid(entity_identifier) || promoted_at == NULL ||
|
||||||
|
(g_strcmp0(promotion_kind, "created") != 0 &&
|
||||||
|
g_strcmp0(promotion_kind, "reused") != 0)) return FALSE;
|
||||||
|
statement = database_statement_prepare(dao->database, sql);
|
||||||
|
success = statement != NULL &&
|
||||||
|
database_statement_bind_text(statement, 1, entity_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 2, promoted_at) &&
|
||||||
|
database_statement_bind_text(statement, 3, promotion_kind) &&
|
||||||
|
database_statement_bind_text(statement, 4, observation_identifier) &&
|
||||||
|
database_statement_bind_text(statement, 5, entity_identifier) &&
|
||||||
|
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
|
||||||
|
if (!success)
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de promouvoir l’observation");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *evidence_entity_dao_format_observations(EvidenceEntityDao *dao,
|
||||||
|
const char *evidence_identifier, GError **error)
|
||||||
|
{
|
||||||
|
static const char *sql =
|
||||||
|
"SELECT COALESCE(o.value_normalized,o.value_raw),o.entity_type,o.role,"
|
||||||
|
"o.source_header,o.occurrence,o.provenance_kind,o.verification_status,"
|
||||||
|
"o.integrated_at,o.entity_id,o.promotion_kind "
|
||||||
|
"FROM evidence_entity_observations o WHERE o.evidence_id=? "
|
||||||
|
"ORDER BY o.source_header,o.occurrence,1,o.role;";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
GString *text = NULL;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
|
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
|
||||||
|
"L'identifiant de preuve est invalide.", error)) return NULL;
|
||||||
|
statement = database_statement_prepare(dao->database, sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, evidence_identifier))
|
||||||
|
goto failure;
|
||||||
|
text = g_string_new(NULL);
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
DatabaseStatementStepResult step = database_statement_step(statement);
|
||||||
|
if (step == DATABASE_STATEMENT_STEP_DONE) break;
|
||||||
|
if (step != DATABASE_STATEMENT_STEP_ROW) goto failure;
|
||||||
|
char *value = NULL, *type = NULL, *role = NULL, *header = NULL;
|
||||||
|
char *provenance = NULL, *status = NULL, *date = NULL;
|
||||||
|
char *entity = NULL, *promotion = NULL; int64_t occurrence = 0;
|
||||||
|
if (!database_statement_column_text(statement, 0, &value) ||
|
||||||
|
!database_statement_column_text(statement, 1, &type) ||
|
||||||
|
!database_statement_column_text(statement, 2, &role) ||
|
||||||
|
!database_statement_column_text(statement, 3, &header) ||
|
||||||
|
!database_statement_column_int64(statement, 4, &occurrence) ||
|
||||||
|
!database_statement_column_text(statement, 5, &provenance) ||
|
||||||
|
!database_statement_column_text(statement, 6, &status) ||
|
||||||
|
!database_statement_column_text(statement, 7, &date) ||
|
||||||
|
!database_statement_column_text(statement, 8, &entity) ||
|
||||||
|
!database_statement_column_text(statement, 9, &promotion))
|
||||||
|
{ g_free(value); g_free(type); g_free(role); g_free(header);
|
||||||
|
g_free(provenance); g_free(status); g_free(date); g_free(entity);
|
||||||
|
g_free(promotion); goto failure; }
|
||||||
|
g_string_append_printf(text, "%s%s — %s — rôle : %s — origine : %s"
|
||||||
|
" #%lld — provenance : %s — validation : %s — date : %s — "
|
||||||
|
"graphe : %s%s%s", text->len > 0 ? "\n" : "",
|
||||||
|
value, type, role, header, (long long) occurrence, provenance,
|
||||||
|
status, date, entity != NULL ? "promue" : "non ajoutée",
|
||||||
|
promotion != NULL ? " (" : "", promotion != NULL ? promotion : "");
|
||||||
|
if (promotion != NULL) g_string_append_c(text, ')');
|
||||||
|
g_free(value); g_free(type); g_free(role); g_free(header);
|
||||||
|
g_free(provenance); g_free(status); g_free(date); g_free(entity);
|
||||||
|
g_free(promotion);
|
||||||
|
}
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return g_string_free(text, FALSE);
|
||||||
|
failure:
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de charger les observations preuve-entité");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
if (text != NULL) g_string_free(text, TRUE);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
GPtrArray *evidence_entity_dao_list_observations(EvidenceEntityDao *dao,
|
||||||
|
const char *evidence_identifier, GError **error)
|
||||||
|
{
|
||||||
|
static const char *sql =
|
||||||
|
"SELECT id,COALESCE(value_normalized,value_raw),entity_type,role,"
|
||||||
|
"source_header,occurrence,provenance_kind,verification_status,"
|
||||||
|
"integrated_at,entity_id,promotion_kind "
|
||||||
|
"FROM evidence_entity_observations WHERE evidence_id=? "
|
||||||
|
"ORDER BY source_header,occurrence,2,role;";
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
GPtrArray *items = NULL;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
|
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
|
||||||
|
"L'identifiant de preuve est invalide.", error)) return NULL;
|
||||||
|
statement = database_statement_prepare(dao->database, sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, evidence_identifier))
|
||||||
|
goto failure;
|
||||||
|
items = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) evidence_observation_free);
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
DatabaseStatementStepResult step = database_statement_step(statement);
|
||||||
|
if (step == DATABASE_STATEMENT_STEP_DONE) break;
|
||||||
|
if (step != DATABASE_STATEMENT_STEP_ROW) goto failure;
|
||||||
|
EvidenceObservation *item = g_new0(EvidenceObservation, 1);
|
||||||
|
int64_t occurrence = 0;
|
||||||
|
if (!database_statement_column_text(statement, 0, &item->identifier) ||
|
||||||
|
!database_statement_column_text(statement, 1, &item->value) ||
|
||||||
|
!database_statement_column_text(statement, 2, &item->type_identifier) ||
|
||||||
|
!database_statement_column_text(statement, 3, &item->role) ||
|
||||||
|
!database_statement_column_text(statement, 4, &item->source_header) ||
|
||||||
|
!database_statement_column_int64(statement, 5, &occurrence) ||
|
||||||
|
!database_statement_column_text(statement, 6, &item->provenance_kind) ||
|
||||||
|
!database_statement_column_text(statement, 7, &item->verification_status) ||
|
||||||
|
!database_statement_column_text(statement, 8, &item->integrated_at) ||
|
||||||
|
!database_statement_column_text(statement, 9, &item->entity_identifier) ||
|
||||||
|
!database_statement_column_text(statement, 10, &item->promotion_kind))
|
||||||
|
{ evidence_observation_free(item); goto failure; }
|
||||||
|
item->occurrence = (guint) occurrence;
|
||||||
|
g_ptr_array_add(items, item);
|
||||||
|
}
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return items;
|
||||||
|
failure:
|
||||||
|
evidence_entity_dao_set_database_error(dao, error,
|
||||||
|
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de charger toutes les observations de la preuve");
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
g_clear_pointer(&items, g_ptr_array_unref);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 10
|
#define DATABASE_SCHEMA_VERSION_CURRENT 12
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "10"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "12"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -812,6 +812,36 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v10_to_v11(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v11(database) ||
|
||||||
|
!database_update_schema_version(database, "11") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V10 vers V11.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool database_migrate_v11_to_v12(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
if (database == NULL || !database_transaction_begin(database)) return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v12(database) ||
|
||||||
|
!database_update_schema_version(database, "12") ||
|
||||||
|
!database_transaction_commit(database)) goto rollback;
|
||||||
|
return true;
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V11 vers V12.");
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1041,6 +1071,14 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v9_to_v10(database)) return false;
|
if (!database_migrate_v9_to_v10(database)) return false;
|
||||||
schema_version = 10;
|
schema_version = 10;
|
||||||
break;
|
break;
|
||||||
|
case 10:
|
||||||
|
if (!database_migrate_v10_to_v11(database)) return false;
|
||||||
|
schema_version = 11;
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
if (!database_migrate_v11_to_v12(database)) return false;
|
||||||
|
schema_version = 12;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,18 @@ bool schema_install_v10(Database *database)
|
||||||
"la migration SQLite V10");
|
"la migration SQLite V10");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v11(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v11.sql",
|
||||||
|
"la migration SQLite V11");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool schema_install_v12(Database *database)
|
||||||
|
{
|
||||||
|
return schema_execute_file(database, "database/schema_v12.sql",
|
||||||
|
"la migration SQLite V12");
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
11
src/models/evidence_observation.c
Normal file
11
src/models/evidence_observation.c
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#include "models/evidence_observation.h"
|
||||||
|
void evidence_observation_free(EvidenceObservation *observation)
|
||||||
|
{
|
||||||
|
if (observation == NULL) return;
|
||||||
|
g_free(observation->identifier); g_free(observation->value);
|
||||||
|
g_free(observation->type_identifier); g_free(observation->role);
|
||||||
|
g_free(observation->source_header); g_free(observation->provenance_kind);
|
||||||
|
g_free(observation->verification_status); g_free(observation->integrated_at);
|
||||||
|
g_free(observation->entity_identifier); g_free(observation->promotion_kind);
|
||||||
|
g_free(observation);
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,54 @@
|
||||||
#include "views/eml_analysis_dialog.h"
|
#include "views/eml_analysis_dialog.h"
|
||||||
#include "widgets/controlled_vocab_dropdown.h"
|
#include "widgets/controlled_vocab_dropdown.h"
|
||||||
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
||||||
|
GtkWidget *summary_label;
|
||||||
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
|
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
|
||||||
} EmlAnalysisDialogState;
|
} EmlAnalysisDialogState;
|
||||||
|
static void eml_analysis_dialog_update_summary(GtkCheckButton *button,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
EmlAnalysisDialogState *state = data; guint kept = 0, promoted = 0;
|
||||||
|
(void) button;
|
||||||
|
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
|
||||||
|
row != NULL; row = gtk_widget_get_next_sibling(row))
|
||||||
|
{
|
||||||
|
GtkWidget *keep = g_object_get_data(G_OBJECT(row), "eml-keep");
|
||||||
|
GtkWidget *promote = g_object_get_data(G_OBJECT(row), "eml-promote");
|
||||||
|
gboolean active = keep != NULL &&
|
||||||
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(keep));
|
||||||
|
if (promote != NULL)
|
||||||
|
{
|
||||||
|
gtk_widget_set_sensitive(promote, active);
|
||||||
|
if (!active)
|
||||||
|
gtk_check_button_set_active(GTK_CHECK_BUTTON(promote), FALSE);
|
||||||
|
}
|
||||||
|
if (active) kept++;
|
||||||
|
if (promote != NULL &&
|
||||||
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote))) promoted++;
|
||||||
|
}
|
||||||
|
char *summary = g_strdup_printf(
|
||||||
|
"%u observation(s) seront conservée(s) dans la fiche ; "
|
||||||
|
"%u seront promue(s) en entité(s).", kept, promoted);
|
||||||
|
gtk_label_set_text(GTK_LABEL(state->summary_label), summary);
|
||||||
|
g_free(summary);
|
||||||
|
}
|
||||||
|
static void eml_analysis_dialog_bind_summary(EmlAnalysisDialogState *state,
|
||||||
|
GtkWidget *content)
|
||||||
|
{
|
||||||
|
state->summary_label = gtk_label_new(
|
||||||
|
"0 observation sera conservée dans la fiche ; "
|
||||||
|
"0 sera promue en entité.");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(state->summary_label), 0.0f);
|
||||||
|
gtk_box_append(GTK_BOX(content), state->summary_label);
|
||||||
|
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
|
||||||
|
row != NULL; row = gtk_widget_get_next_sibling(row))
|
||||||
|
{
|
||||||
|
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-keep"),
|
||||||
|
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
|
||||||
|
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-promote"),
|
||||||
|
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
|
||||||
|
}
|
||||||
|
}
|
||||||
/** @brief Libère l'état de révision. */
|
/** @brief Libère l'état de révision. */
|
||||||
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
|
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
|
||||||
/** @brief Signale une annulation une seule fois. */
|
/** @brief Signale une annulation une seule fois. */
|
||||||
|
|
@ -30,7 +76,7 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
|
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
|
||||||
child != NULL; child = gtk_widget_get_next_sibling(child))
|
child != NULL; child = gtk_widget_get_next_sibling(child))
|
||||||
{
|
{
|
||||||
GtkWidget *check = GTK_IS_BOX(child) ? gtk_widget_get_first_child(child) : NULL;
|
GtkWidget *check = g_object_get_data(G_OBJECT(child), "eml-keep");
|
||||||
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
||||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
||||||
{
|
{
|
||||||
|
|
@ -39,10 +85,23 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
||||||
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
||||||
"eml-provenance");
|
"eml-provenance");
|
||||||
g_ptr_array_add(selected, eml_entity_proposal_new_with_metadata(
|
const char *raw = g_object_get_data(G_OBJECT(child), "eml-raw");
|
||||||
type, value,
|
const char *role = g_object_get_data(G_OBJECT(child), "eml-role");
|
||||||
|
const char *header = g_object_get_data(G_OBJECT(child), "eml-header");
|
||||||
|
guint occurrence = GPOINTER_TO_UINT(g_object_get_data(
|
||||||
|
G_OBJECT(child), "eml-occurrence"));
|
||||||
|
EmlEntityProposal *proposal = eml_entity_proposal_new_observation(
|
||||||
|
type, raw != NULL ? raw : value, value,
|
||||||
|
role != NULL ? role : "other",
|
||||||
|
header != NULL ? header : "manual",
|
||||||
|
occurrence > 0 ? occurrence : 1,
|
||||||
controlled_vocab_dropdown_get_selected_code(status),
|
controlled_vocab_dropdown_get_selected_code(status),
|
||||||
controlled_vocab_dropdown_get_selected_code(provenance)));
|
controlled_vocab_dropdown_get_selected_code(provenance));
|
||||||
|
GtkWidget *promote = g_object_get_data(G_OBJECT(child),
|
||||||
|
"eml-promote");
|
||||||
|
proposal->promote_to_entity = promote != NULL &&
|
||||||
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote));
|
||||||
|
g_ptr_array_add(selected, proposal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selected->len == 0)
|
if (selected->len == 0)
|
||||||
|
|
@ -52,6 +111,54 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
else g_ptr_array_unref(selected);
|
else g_ptr_array_unref(selected);
|
||||||
gtk_window_close(state->window);
|
gtk_window_close(state->window);
|
||||||
}
|
}
|
||||||
|
static void eml_analysis_dialog_add_observations(GtkWidget *box,
|
||||||
|
const GPtrArray *observations)
|
||||||
|
{
|
||||||
|
for (guint i = 0; observations != NULL && i < observations->len; i++)
|
||||||
|
{
|
||||||
|
const EmlObservation *observation = g_ptr_array_index(
|
||||||
|
(GPtrArray *) observations, i);
|
||||||
|
char *text = g_strdup_printf("%s — %s — rôle : %s — origine : %s #%u",
|
||||||
|
observation->type_identifier, observation->value_normalized,
|
||||||
|
observation->role, observation->source_header,
|
||||||
|
observation->occurrence);
|
||||||
|
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
GtkWidget *check = gtk_check_button_new_with_label(
|
||||||
|
"Conserver dans la fiche");
|
||||||
|
GtkWidget *value_label = gtk_label_new(text);
|
||||||
|
GtkWidget *promote = gtk_check_button_new_with_label(
|
||||||
|
"Promouvoir en entité");
|
||||||
|
gtk_widget_set_sensitive(promote, FALSE);
|
||||||
|
GtkWidget *status = controlled_vocab_dropdown_new(
|
||||||
|
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
||||||
|
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
||||||
|
CONTROLLED_VOCAB_PROVENANCE_KIND, observation->provenance_kind);
|
||||||
|
g_object_set_data_full(G_OBJECT(row), "eml-type",
|
||||||
|
g_strdup(observation->type_identifier), g_free);
|
||||||
|
g_object_set_data_full(G_OBJECT(row), "eml-value",
|
||||||
|
g_strdup(observation->value_normalized), g_free);
|
||||||
|
g_object_set_data_full(G_OBJECT(row), "eml-raw",
|
||||||
|
g_strdup(observation->value_raw), g_free);
|
||||||
|
g_object_set_data_full(G_OBJECT(row), "eml-role",
|
||||||
|
g_strdup(observation->role), g_free);
|
||||||
|
g_object_set_data_full(G_OBJECT(row), "eml-header",
|
||||||
|
g_strdup(observation->source_header), g_free);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-occurrence",
|
||||||
|
GUINT_TO_POINTER(observation->occurrence));
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-status", status);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-keep", check);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
|
||||||
|
gtk_widget_set_hexpand(check, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(row), check);
|
||||||
|
gtk_box_append(GTK_BOX(row), value_label);
|
||||||
|
gtk_box_append(GTK_BOX(row), promote);
|
||||||
|
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 les propositions d'un type sous forme de cases décochées. */
|
/** @brief Ajoute les propositions d'un type sous forme de cases décochées. */
|
||||||
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
||||||
const char *type, const char *label, const GPtrArray *values)
|
const char *type, const char *label, const GPtrArray *values)
|
||||||
|
|
@ -61,7 +168,12 @@ 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 *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(
|
||||||
|
"Conserver dans la fiche");
|
||||||
|
GtkWidget *value_label = gtk_label_new(text);
|
||||||
|
GtkWidget *promote = gtk_check_button_new_with_label(
|
||||||
|
"Promouvoir en entité");
|
||||||
|
gtk_widget_set_sensitive(promote, FALSE);
|
||||||
GtkWidget *status = controlled_vocab_dropdown_new(
|
GtkWidget *status = controlled_vocab_dropdown_new(
|
||||||
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
||||||
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
||||||
|
|
@ -71,8 +183,12 @@ static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
||||||
g_object_set_data_full(G_OBJECT(row), "eml-value", g_strdup(value), 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-status", status);
|
||||||
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-keep", check);
|
||||||
|
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
|
||||||
gtk_widget_set_hexpand(check, TRUE);
|
gtk_widget_set_hexpand(check, TRUE);
|
||||||
gtk_box_append(GTK_BOX(row), check);
|
gtk_box_append(GTK_BOX(row), check);
|
||||||
|
gtk_box_append(GTK_BOX(row), value_label);
|
||||||
|
gtk_box_append(GTK_BOX(row), promote);
|
||||||
gtk_box_append(GTK_BOX(row), status);
|
gtk_box_append(GTK_BOX(row), status);
|
||||||
gtk_box_append(GTK_BOX(row), provenance);
|
gtk_box_append(GTK_BOX(row), provenance);
|
||||||
gtk_box_append(GTK_BOX(box), row); g_free(text);
|
gtk_box_append(GTK_BOX(box), row); g_free(text);
|
||||||
|
|
@ -162,17 +278,13 @@ void eml_analysis_dialog_present(GtkWindow *parent,
|
||||||
destination_ips);
|
destination_ips);
|
||||||
gtk_box_append(GTK_BOX(content), grid);
|
gtk_box_append(GTK_BOX(content), grid);
|
||||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||||
"Sélection explicite des entités à intégrer"));
|
"Choisir les observations à conserver ; la promotion vers le graphe "
|
||||||
|
"est une action séparée."));
|
||||||
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "email_address",
|
eml_analysis_dialog_add_observations(state->proposals_box,
|
||||||
"Email", eml_analysis_get_email_addresses(analysis));
|
eml_analysis_get_observations(analysis));
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "domain_name",
|
|
||||||
"Domaine", eml_analysis_get_domains(analysis));
|
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "ip_address",
|
|
||||||
"IP expéditeur", eml_analysis_get_sender_ip_addresses(analysis));
|
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "ip_address",
|
|
||||||
"IP destinataire", eml_analysis_get_destination_ip_addresses(analysis));
|
|
||||||
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
||||||
|
eml_analysis_dialog_bind_summary(state, content);
|
||||||
gtk_box_append(GTK_BOX(content), gtk_label_new("En-têtes bruts (lecture seule)"));
|
gtk_box_append(GTK_BOX(content), gtk_label_new("En-têtes bruts (lecture seule)"));
|
||||||
raw = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
|
raw = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
|
||||||
gtk_text_view_set_monospace(GTK_TEXT_VIEW(raw), TRUE);
|
gtk_text_view_set_monospace(GTK_TEXT_VIEW(raw), TRUE);
|
||||||
|
|
@ -384,12 +496,8 @@ void eml_analysis_dialog_present_pipeline(
|
||||||
gtk_box_append(GTK_BOX(content), details);
|
gtk_box_append(GTK_BOX(content), details);
|
||||||
|
|
||||||
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "email_address",
|
eml_analysis_dialog_add_observations(state->proposals_box,
|
||||||
"E-mail", eml_analysis_get_email_addresses(result->analysis));
|
eml_analysis_get_observations(result->analysis));
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "domain_name",
|
|
||||||
"Domaine", eml_analysis_get_domains(result->analysis));
|
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "ip_address",
|
|
||||||
"IP expéditeur", eml_analysis_get_sender_ip_addresses(result->analysis));
|
|
||||||
GPtrArray *valid_ibans = g_ptr_array_new();
|
GPtrArray *valid_ibans = g_ptr_array_new();
|
||||||
for (guint index = 0; result->bank_proposals != NULL &&
|
for (guint index = 0; result->bank_proposals != NULL &&
|
||||||
index < result->bank_proposals->len; index++)
|
index < result->bank_proposals->len; index++)
|
||||||
|
|
@ -406,6 +514,7 @@ void eml_analysis_dialog_present_pipeline(
|
||||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||||
"Propositions à intégrer explicitement"));
|
"Propositions à intégrer explicitement"));
|
||||||
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
gtk_box_append(GTK_BOX(content), state->proposals_box);
|
||||||
|
eml_analysis_dialog_bind_summary(state, content);
|
||||||
|
|
||||||
GtkWidget *scroll = gtk_scrolled_window_new();
|
GtkWidget *scroll = gtk_scrolled_window_new();
|
||||||
gtk_widget_set_vexpand(scroll, TRUE);
|
gtk_widget_set_vexpand(scroll, TRUE);
|
||||||
|
|
|
||||||
|
|
@ -1995,6 +1995,22 @@ void main_window_set_selected_evidence(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_evidence_observations(MainWindow *main_window,
|
||||||
|
const GPtrArray *observations)
|
||||||
|
{
|
||||||
|
if (main_window != NULL)
|
||||||
|
workspace_set_evidence_observations(main_window->workspace,
|
||||||
|
observations);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main_window_set_observation_remove_callback(MainWindow *main_window,
|
||||||
|
void (*callback)(const char *, gpointer), gpointer user_data)
|
||||||
|
{
|
||||||
|
if (main_window != NULL)
|
||||||
|
workspace_set_observation_remove_callback(main_window->workspace,
|
||||||
|
callback, user_data);
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_eml_analysis_available(
|
void main_window_set_eml_analysis_available(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
gboolean available)
|
gboolean available)
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ struct Workspace
|
||||||
GtkWidget *evidence_internal_name_label;
|
GtkWidget *evidence_internal_name_label;
|
||||||
GtkWidget *evidence_identifier_label;
|
GtkWidget *evidence_identifier_label;
|
||||||
GtkWidget *evidence_sha256_label;
|
GtkWidget *evidence_sha256_label;
|
||||||
|
GtkWidget *evidence_observations_box;
|
||||||
GtkWidget *verify_evidence_button;
|
GtkWidget *verify_evidence_button;
|
||||||
GtkWidget *edit_evidence_button;
|
GtkWidget *edit_evidence_button;
|
||||||
GtkWidget *analyze_eml_button;
|
GtkWidget *analyze_eml_button;
|
||||||
|
|
@ -123,6 +124,8 @@ struct Workspace
|
||||||
gpointer extract_metadata_user_data;
|
gpointer extract_metadata_user_data;
|
||||||
WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback;
|
WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback;
|
||||||
gpointer recover_pdf_password_user_data;
|
gpointer recover_pdf_password_user_data;
|
||||||
|
WorkspaceObservationRemoveCallback observation_remove_callback;
|
||||||
|
gpointer observation_remove_user_data;
|
||||||
|
|
||||||
WorkspaceGraphNodeMovedCallback
|
WorkspaceGraphNodeMovedCallback
|
||||||
graph_node_moved_callback;
|
graph_node_moved_callback;
|
||||||
|
|
@ -1746,6 +1749,15 @@ Workspace *workspace_new(void)
|
||||||
10,
|
10,
|
||||||
"SHA-256"
|
"SHA-256"
|
||||||
);
|
);
|
||||||
|
GtkWidget *observations_title = gtk_label_new(
|
||||||
|
"Observations extraites");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(observations_title), 0.0f);
|
||||||
|
gtk_widget_add_css_class(observations_title, "heading");
|
||||||
|
workspace->evidence_observations_box =
|
||||||
|
gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
|
gtk_grid_attach(GTK_GRID(evidence_grid), observations_title, 0, 11, 1, 1);
|
||||||
|
gtk_grid_attach(GTK_GRID(evidence_grid),
|
||||||
|
workspace->evidence_observations_box, 1, 11, 1, 1);
|
||||||
|
|
||||||
if (workspace->evidence_type_label == NULL ||
|
if (workspace->evidence_type_label == NULL ||
|
||||||
workspace->evidence_integrity_label == NULL ||
|
workspace->evidence_integrity_label == NULL ||
|
||||||
|
|
@ -1757,7 +1769,8 @@ Workspace *workspace_new(void)
|
||||||
workspace->evidence_relative_path_label == NULL ||
|
workspace->evidence_relative_path_label == NULL ||
|
||||||
workspace->evidence_internal_name_label == NULL ||
|
workspace->evidence_internal_name_label == NULL ||
|
||||||
workspace->evidence_identifier_label == NULL ||
|
workspace->evidence_identifier_label == NULL ||
|
||||||
workspace->evidence_sha256_label == NULL)
|
workspace->evidence_sha256_label == NULL ||
|
||||||
|
workspace->evidence_observations_box == NULL)
|
||||||
{
|
{
|
||||||
workspace_free(
|
workspace_free(
|
||||||
workspace
|
workspace
|
||||||
|
|
@ -2576,6 +2589,82 @@ Workspace *workspace_new(void)
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void workspace_on_remove_observation_clicked(GtkButton *button,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
Workspace *workspace = user_data;
|
||||||
|
const char *identifier = g_object_get_data(G_OBJECT(button),
|
||||||
|
"observation-identifier");
|
||||||
|
if (workspace != NULL && workspace->observation_remove_callback != NULL)
|
||||||
|
workspace->observation_remove_callback(identifier,
|
||||||
|
workspace->observation_remove_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void workspace_set_evidence_observations(Workspace *workspace,
|
||||||
|
const GPtrArray *observations)
|
||||||
|
{
|
||||||
|
if (workspace == NULL || workspace->evidence_observations_box == NULL)
|
||||||
|
return;
|
||||||
|
GtkWidget *child = gtk_widget_get_first_child(
|
||||||
|
workspace->evidence_observations_box);
|
||||||
|
while (child != NULL)
|
||||||
|
{
|
||||||
|
GtkWidget *next = gtk_widget_get_next_sibling(child);
|
||||||
|
gtk_box_remove(GTK_BOX(workspace->evidence_observations_box), child);
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
if (observations == NULL || observations->len == 0)
|
||||||
|
{
|
||||||
|
gtk_box_append(GTK_BOX(workspace->evidence_observations_box),
|
||||||
|
gtk_label_new("Aucune observation confirmée pour cette preuve."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (guint index = 0; index < observations->len; index++)
|
||||||
|
{
|
||||||
|
const EvidenceObservation *item = g_ptr_array_index(
|
||||||
|
(GPtrArray *) observations, index);
|
||||||
|
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
|
char *text = g_strdup_printf(
|
||||||
|
"%s\nType : %s — rôle : %s\nSource : %s #%u — provenance : %s\n"
|
||||||
|
"Validation : %s — intégrée : %s\nGraphe : %s",
|
||||||
|
item->value, item->type_identifier, item->role,
|
||||||
|
item->source_header, item->occurrence, item->provenance_kind,
|
||||||
|
item->verification_status, item->integrated_at,
|
||||||
|
item->entity_identifier == NULL ? "Non ajoutée" :
|
||||||
|
(g_strcmp0(item->promotion_kind, "created") == 0
|
||||||
|
? "Entité créée" :
|
||||||
|
g_strcmp0(item->promotion_kind, "reused") == 0
|
||||||
|
? "Entité existante réutilisée" : "Promotion historique"));
|
||||||
|
GtkWidget *label = gtk_label_new(text);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||||
|
gtk_label_set_selectable(GTK_LABEL(label), TRUE);
|
||||||
|
gtk_widget_set_hexpand(label, TRUE);
|
||||||
|
gtk_box_append(GTK_BOX(row), label);
|
||||||
|
if (item->entity_identifier != NULL)
|
||||||
|
{
|
||||||
|
GtkWidget *remove = gtk_button_new_with_label("Retirer du graphe");
|
||||||
|
gtk_widget_add_css_class(remove, "flat");
|
||||||
|
gtk_widget_set_tooltip_text(remove,
|
||||||
|
"Détache cette observation sans supprimer son contenu.");
|
||||||
|
g_object_set_data_full(G_OBJECT(remove), "observation-identifier",
|
||||||
|
g_strdup(item->identifier), g_free);
|
||||||
|
g_signal_connect(remove, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_remove_observation_clicked), workspace);
|
||||||
|
gtk_box_append(GTK_BOX(row), remove);
|
||||||
|
}
|
||||||
|
gtk_box_append(GTK_BOX(workspace->evidence_observations_box), row);
|
||||||
|
g_free(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void workspace_set_observation_remove_callback(Workspace *workspace,
|
||||||
|
WorkspaceObservationRemoveCallback callback, gpointer user_data)
|
||||||
|
{
|
||||||
|
if (workspace == NULL) return;
|
||||||
|
workspace->observation_remove_callback = callback;
|
||||||
|
workspace->observation_remove_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget *workspace_get_widget(
|
GtkWidget *workspace_get_widget(
|
||||||
const Workspace *workspace
|
const Workspace *workspace
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "10") == 0);
|
assert(strcmp(schema_version, "12") == 0);
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
test_database_assert_table_exists(database, "graph_viewport");
|
test_database_assert_table_exists(database, "graph_viewport");
|
||||||
|
|
@ -992,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"10"
|
"12"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ static void test_eml_analyzer_headers(void)
|
||||||
"Subject: Synthetic fixture\r\n"
|
"Subject: Synthetic fixture\r\n"
|
||||||
"Date: Wed, 22 Jul 2026 12:00:00 +0200\r\n"
|
"Date: Wed, 22 Jul 2026 12:00:00 +0200\r\n"
|
||||||
"Message-ID: <id-123@example.test>\r\n"
|
"Message-ID: <id-123@example.test>\r\n"
|
||||||
|
"MIME-Version: 1.0\r\n"
|
||||||
"Received: from mail.example.test (mail.example.test [192.0.2.10])\r\n"
|
"Received: from mail.example.test (mail.example.test [192.0.2.10])\r\n"
|
||||||
" by mx.example.net ([198.51.100.20]) with ESMTP; Wed, 22 Jul 2026 10:00:00 +0000\r\n"
|
" by mx.example.net ([198.51.100.20]) with ESMTP; Wed, 22 Jul 2026 10:00:00 +0000\r\n"
|
||||||
"Received: from localhost ([127.0.0.1]) by mail.example.test\r\n"
|
"Received: from localhost ([127.0.0.1]) by mail.example.test\r\n"
|
||||||
|
|
@ -27,6 +28,7 @@ static void test_eml_analyzer_headers(void)
|
||||||
EmlAnalysis *analysis = NULL;
|
EmlAnalysis *analysis = NULL;
|
||||||
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
|
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
|
||||||
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
|
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
|
||||||
|
const GPtrArray *domains = NULL, *observations = NULL;
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
|
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
|
||||||
assert(directory != NULL && error == NULL);
|
assert(directory != NULL && error == NULL);
|
||||||
|
|
@ -56,12 +58,56 @@ static void test_eml_analyzer_headers(void)
|
||||||
"192.0.2.10") == 0);
|
"192.0.2.10") == 0);
|
||||||
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
|
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
|
||||||
"198.51.100.20") == 0);
|
"198.51.100.20") == 0);
|
||||||
|
domains = eml_analysis_get_domains(analysis);
|
||||||
|
for (guint i = 0; i < domains->len; i++)
|
||||||
|
{
|
||||||
|
const char *domain = g_ptr_array_index((GPtrArray *) domains, i);
|
||||||
|
assert(strcmp(domain, "192.0.2.10") != 0);
|
||||||
|
assert(strcmp(domain, "198.51.100.20") != 0);
|
||||||
|
assert(strcmp(domain, "1.0") != 0);
|
||||||
|
}
|
||||||
|
observations = eml_analysis_get_observations(analysis);
|
||||||
|
assert(observations != NULL && observations->len > 0);
|
||||||
|
gboolean found_from = FALSE, found_received_ip = FALSE;
|
||||||
|
for (guint i = 0; i < observations->len; i++)
|
||||||
|
{
|
||||||
|
const EmlObservation *observation = g_ptr_array_index(
|
||||||
|
(GPtrArray *) observations, i);
|
||||||
|
if (strcmp(observation->value_normalized, "sender@example.test") == 0 &&
|
||||||
|
strcmp(observation->role, "from") == 0 &&
|
||||||
|
strcmp(observation->source_header, "from") == 0)
|
||||||
|
found_from = TRUE;
|
||||||
|
if (strcmp(observation->value_normalized, "192.0.2.10") == 0 &&
|
||||||
|
strcmp(observation->role, "smtp_relay") == 0 &&
|
||||||
|
strcmp(observation->source_header, "received") == 0)
|
||||||
|
found_received_ip = TRUE;
|
||||||
|
}
|
||||||
|
assert(found_from && found_received_ip);
|
||||||
eml_analysis_free(analysis);
|
eml_analysis_free(analysis);
|
||||||
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
|
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
|
||||||
g_free(path); g_free(directory);
|
g_free(path); g_free(directory);
|
||||||
}
|
}
|
||||||
|
static void test_eml_analyzer_manual_fixture_regression(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
EmlAnalysis *analysis = eml_analyzer_analyze_file(
|
||||||
|
"tests/fixtures/eml/manual_smoke_test.eml", &error);
|
||||||
|
assert(analysis != NULL && error == NULL);
|
||||||
|
const GPtrArray *domains = eml_analysis_get_domains(analysis);
|
||||||
|
for (guint index = 0; index < domains->len; index++)
|
||||||
|
{
|
||||||
|
const char *domain = g_ptr_array_index((GPtrArray *) domains, index);
|
||||||
|
assert(strcmp(domain, "192.0.2.10") != 0);
|
||||||
|
assert(strcmp(domain, "198.51.100.20") != 0);
|
||||||
|
assert(strcmp(domain, "1.0") != 0);
|
||||||
|
}
|
||||||
|
const GPtrArray *observations = eml_analysis_get_observations(analysis);
|
||||||
|
assert(observations != NULL && observations->len >= 10);
|
||||||
|
eml_analysis_free(analysis);
|
||||||
|
}
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
test_eml_analyzer_headers();
|
test_eml_analyzer_headers();
|
||||||
|
test_eml_analyzer_manual_fixture_regression();
|
||||||
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
|
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
139
tests/test_eml_integration.c
Normal file
139
tests/test_eml_integration.c
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#include "core/eml_integration.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
#include <assert.h>
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static void execute_done(Database *database, const char *sql)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||||
|
assert(statement != NULL);
|
||||||
|
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE);
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gint64 count_rows(Database *database, const char *table)
|
||||||
|
{
|
||||||
|
char *sql = g_strdup_printf("SELECT COUNT(*) FROM %s;", table);
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||||
|
int64_t count = -1;
|
||||||
|
g_free(sql);
|
||||||
|
assert(statement != NULL);
|
||||||
|
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||||
|
assert(database_statement_column_int64(statement, 0, &count));
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
static char *read_observation_identifier(Database *database, const char *role)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database,
|
||||||
|
"SELECT id FROM evidence_entity_observations WHERE role=?;");
|
||||||
|
char *identifier = NULL;
|
||||||
|
assert(statement != NULL && database_statement_bind_text(statement, 1, role));
|
||||||
|
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||||
|
assert(database_statement_column_text(statement, 0, &identifier));
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
char *directory = g_dir_make_tmp("labfy-eml-integration-XXXXXX", &error);
|
||||||
|
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||||
|
const char *evidence_id = "10000000-0000-4000-8000-000000000099";
|
||||||
|
assert(directory != NULL && error == NULL);
|
||||||
|
assert(database_initialize(path, "Synthétique", directory));
|
||||||
|
Database *database = database_open(path);
|
||||||
|
assert(database != NULL);
|
||||||
|
execute_done(database,
|
||||||
|
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
|
||||||
|
"imported_at,updated_at,status,locked,original_name) VALUES("
|
||||||
|
"'10000000-0000-4000-8000-000000000099','fixture.eml',"
|
||||||
|
"'01_Preuves_Originales/fixture.eml',5,1,"
|
||||||
|
"'0000000000000000000000000000000000000000000000000000000000000000',"
|
||||||
|
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',0,"
|
||||||
|
"'fixture.eml');");
|
||||||
|
GPtrArray *proposals = g_ptr_array_new_with_free_func(
|
||||||
|
(GDestroyNotify) eml_entity_proposal_free);
|
||||||
|
EmlEntityProposal *proposal = eml_entity_proposal_new_observation(
|
||||||
|
"email_address", "Sender@Example.test", "sender@example.test",
|
||||||
|
"from", "from", 1, "confirmed", "header");
|
||||||
|
g_ptr_array_add(proposals, proposal);
|
||||||
|
guint observations = 0, created = 0, reused = 0;
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(error == NULL && observations == 1 && created == 0 && reused == 0);
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 1);
|
||||||
|
assert(count_rows(database, "entites") == 0);
|
||||||
|
assert(count_rows(database, "preuve_entites") == 0);
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 1);
|
||||||
|
assert(count_rows(database, "entites") == 0);
|
||||||
|
proposal->promote_to_entity = TRUE;
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(created == 1 && count_rows(database, "entites") == 1);
|
||||||
|
assert(count_rows(database, "preuve_entites") == 1);
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(created == 0 && reused == 1 && count_rows(database, "entites") == 1);
|
||||||
|
char *from_observation = read_observation_identifier(database, "from");
|
||||||
|
gboolean deleted = FALSE, shared = FALSE;
|
||||||
|
assert(eml_integration_remove_promotion(database, from_observation,
|
||||||
|
&deleted, &shared, &error));
|
||||||
|
assert(deleted && !shared);
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 1);
|
||||||
|
assert(count_rows(database, "entites") == 0);
|
||||||
|
assert(count_rows(database, "preuve_entites") == 0);
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(created == 1);
|
||||||
|
EmlEntityProposal *second = eml_entity_proposal_new_observation(
|
||||||
|
"email_address", "Sender@Example.test", "sender@example.test",
|
||||||
|
"reply_to", "reply-to", 1, "confirmed", "header");
|
||||||
|
second->promote_to_entity = TRUE;
|
||||||
|
g_ptr_array_add(proposals, second);
|
||||||
|
assert(eml_integration_apply(database, evidence_id, proposals,
|
||||||
|
&observations, &created, &reused, &error));
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 2);
|
||||||
|
assert(count_rows(database, "entites") == 1);
|
||||||
|
assert(eml_integration_remove_promotion(database, from_observation,
|
||||||
|
&deleted, &shared, &error));
|
||||||
|
assert(!deleted && shared);
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 2);
|
||||||
|
assert(count_rows(database, "entites") == 1);
|
||||||
|
assert(count_rows(database, "preuve_entites") == 1);
|
||||||
|
execute_done(database,
|
||||||
|
"INSERT INTO entites(id,type_id,valeur,label,description,confiance,"
|
||||||
|
"created_at,updated_at,status) VALUES("
|
||||||
|
"'20000000-0000-4000-8000-000000000099',1,'other@example.test',"
|
||||||
|
"'other@example.test',NULL,50,'2026-07-28T08:00:00Z',"
|
||||||
|
"'2026-07-28T08:00:00Z','active');");
|
||||||
|
execute_done(database,
|
||||||
|
"INSERT INTO relations(id,entite_source_id,entite_cible_id,"
|
||||||
|
"type_relation,label,justification,confiance,created_at,updated_at,"
|
||||||
|
"status,relation_type_id) SELECT "
|
||||||
|
"'30000000-0000-4000-8000-000000000099',o.entity_id,"
|
||||||
|
"'20000000-0000-4000-8000-000000000099','supports',NULL,NULL,50,"
|
||||||
|
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',rt.id "
|
||||||
|
"FROM evidence_entity_observations o,relation_types rt "
|
||||||
|
"WHERE o.role='reply_to' AND rt.code='supports';");
|
||||||
|
char *reply_observation = read_observation_identifier(database, "reply_to");
|
||||||
|
assert(eml_integration_remove_promotion(database, reply_observation,
|
||||||
|
&deleted, &shared, &error));
|
||||||
|
assert(!deleted && shared);
|
||||||
|
assert(count_rows(database, "relations") == 1);
|
||||||
|
assert(count_rows(database, "evidence_entity_observations") == 2);
|
||||||
|
g_free(reply_observation);
|
||||||
|
g_free(from_observation);
|
||||||
|
g_ptr_array_unref(proposals);
|
||||||
|
database_close(database);
|
||||||
|
assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
|
||||||
|
g_free(path); g_free(directory);
|
||||||
|
puts("EmlIntegration : tous les tests sont valides.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -523,6 +523,7 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
|
|
||||||
GError *error =
|
GError *error =
|
||||||
NULL;
|
NULL;
|
||||||
|
char *observation_identifier = NULL;
|
||||||
|
|
||||||
test_evidence_entity_dao_insert_evidence(
|
test_evidence_entity_dao_insert_evidence(
|
||||||
&fixture,
|
&fixture,
|
||||||
|
|
@ -565,6 +566,30 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
|
|
||||||
assert(error == NULL);
|
assert(error == NULL);
|
||||||
assert(association_exists);
|
assert(association_exists);
|
||||||
|
assert(evidence_entity_dao_add_observation(
|
||||||
|
fixture.evidence_entity_dao, evidence_identifier,
|
||||||
|
"email_address", "Contact@Example.org", "contact@example.org",
|
||||||
|
"from", "header", "from", 1, "confirmed",
|
||||||
|
"2026-07-28T08:00:00Z", &observation_identifier, &error));
|
||||||
|
assert(error == NULL);
|
||||||
|
assert(g_uuid_string_is_valid(observation_identifier));
|
||||||
|
g_clear_pointer(&observation_identifier, g_free);
|
||||||
|
/* Réapplication idempotente : ni entité ni observation dupliquée. */
|
||||||
|
assert(evidence_entity_dao_add_observation(
|
||||||
|
fixture.evidence_entity_dao, evidence_identifier,
|
||||||
|
"email_address", "Contact@Example.org", "contact@example.org",
|
||||||
|
"from", "header", "from", 1, "confirmed",
|
||||||
|
"2026-07-28T08:00:00Z", &observation_identifier, &error));
|
||||||
|
assert(g_uuid_string_is_valid(observation_identifier));
|
||||||
|
char *summary = evidence_entity_dao_format_observations(
|
||||||
|
fixture.evidence_entity_dao, evidence_identifier, &error);
|
||||||
|
assert(error == NULL && summary != NULL);
|
||||||
|
assert(strstr(summary, "contact@example.org") != NULL);
|
||||||
|
assert(strstr(summary, "rôle : from") != NULL);
|
||||||
|
assert(strstr(summary, "origine : from #1") != NULL);
|
||||||
|
assert(strchr(summary, '\n') == NULL);
|
||||||
|
g_free(summary);
|
||||||
|
g_free(observation_identifier);
|
||||||
|
|
||||||
test_evidence_entity_dao_fixture_clear(
|
test_evidence_entity_dao_fixture_clear(
|
||||||
&fixture
|
&fixture
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue