feat(email): extract forensic metadata from EML evidence

This commit is contained in:
grayTerminal-sh 2026-07-22 16:09:22 +02:00
parent 655780a446
commit a683266468
15 changed files with 916 additions and 2 deletions

View file

@ -124,6 +124,7 @@ TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
TEST_EML_ANALYZER := tests/test_eml_analyzer
all: $(TARGET)
@ -643,6 +644,9 @@ $(TEST_PERSON_ENTITY_SERVICE): \
src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \
@ -721,7 +725,8 @@ test: \
$(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE)
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -780,6 +785,7 @@ test: \
@$(TEST_SOCIAL_ACCOUNT_SERVICE)
@$(TEST_SOCIAL_PLATFORM)
@$(TEST_PERSON_ENTITY_SERVICE)
@$(TEST_EML_ANALYZER)
@echo "Tous les tests sont valides."
%.o: %.c
@ -844,7 +850,8 @@ clean:
$(TEST_EVIDENCE_RECLASSIFICATION) \
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE)
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER)
-include $(DEP)

View file

@ -0,0 +1,41 @@
/******************************************************************************
* @file eml_analyzer.h
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_ANALYZER_H
#define LABFY_INVESTIGATION_EML_ANALYZER_H
#include <glib.h>
G_BEGIN_DECLS
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
typedef struct EmlAnalysis EmlAnalysis;
/**
* @brief Analyse uniquement les en-têtes d'un fichier EML local.
* @param file_path Chemin du fichier original en lecture seule.
* @param error Destination facultative d'une erreur.
* @return Nouvelle analyse, ou NULL.
*/
EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error);
/** @brief Libère une analyse. */
void eml_analysis_free(EmlAnalysis *analysis);
/** @brief Retourne la première valeur d'un en-tête, ou NULL. */
const char *eml_analysis_get_first_header(const EmlAnalysis *analysis,
const char *header_name);
/** @brief Retourne les valeurs ordonnées d'un en-tête, ou NULL. */
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
const char *header_name);
/** @brief Retourne les adresses email uniques extraites. */
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
/** @brief Retourne les domaines uniques extraits. */
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *analysis);
/** @brief Retourne les adresses IP uniques extraites. */
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
const GPtrArray *eml_analysis_get_sender_ip_addresses(
const EmlAnalysis *analysis);
/** @brief Retourne les IP présentes dans la partie `by` des Received. */
const GPtrArray *eml_analysis_get_destination_ip_addresses(
const EmlAnalysis *analysis);
/** @brief Retourne une copie UTF-8 des en-têtes bruts. */
const char *eml_analysis_get_raw_headers(const EmlAnalysis *analysis);
G_END_DECLS
#endif

View file

@ -0,0 +1,31 @@
/******************************************************************************
* @file eml_integration.h
* @brief Intégration transactionnelle de propositions issues d'un EML.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_INTEGRATION_H
#define LABFY_INVESTIGATION_EML_INTEGRATION_H
#include "database/database.h"
#include <glib.h>
G_BEGIN_DECLS
/** @brief Proposition d'entité explicitement sélectionnable. */
typedef struct { char *type_identifier; char *value; } EmlEntityProposal;
/** @brief Crée une proposition possédée. */
EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
const char *value);
/** @brief Libère une proposition. */
void eml_entity_proposal_free(EmlEntityProposal *proposal);
/**
* @brief Intègre atomiquement des entités et les lie à la preuve EML.
* @param database Connexion ouverte empruntée.
* @param evidence_identifier UUID de la preuve source.
* @param proposals Tableau de EmlEntityProposal sélectionnées.
* @param out_created Nombre d'entités créées.
* @param out_reused Nombre d'entités existantes réutilisées.
* @param error Destination facultative d'une erreur.
* @return TRUE après validation de la transaction.
*/
gboolean eml_integration_apply(Database *database,
const char *evidence_identifier, const GPtrArray *proposals,
guint *out_created, guint *out_reused, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,29 @@
/******************************************************************************
* @file eml_processing.h
* @brief Préparation vérifiée d'une copie de travail EML.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_PROCESSING_H
#define LABFY_INVESTIGATION_EML_PROCESSING_H
#include "core/eml_analyzer.h"
#include "models/evidence_record.h"
#include <glib.h>
G_BEGIN_DECLS
/** @brief Résultat opaque possédant la copie et son analyse. */
typedef struct EmlProcessingResult EmlProcessingResult;
/**
* @brief Copie une preuve vers les extractions puis analyse uniquement la copie.
* @param investigation_root Racine canonique de l'enquête.
* @param evidence_record Preuve EML empruntée.
* @param error Destination facultative d'une erreur.
* @return Nouveau résultat, ou NULL.
*/
EmlProcessingResult *eml_processing_prepare(const char *investigation_root,
const EvidenceRecord *evidence_record, GError **error);
/** @brief Libère le résultat sans supprimer la copie vérifiée. */
void eml_processing_result_free(EmlProcessingResult *result);
/** @brief Retourne le chemin de la copie de travail. */
const char *eml_processing_result_get_copy_path(const EmlProcessingResult *result);
/** @brief Retourne l'analyse empruntée de la copie. */
const EmlAnalysis *eml_processing_result_get_analysis(const EmlProcessingResult *result);
G_END_DECLS
#endif

View file

@ -0,0 +1,23 @@
/******************************************************************************
* @file eml_analysis_dialog.h
* @brief Présentation en lecture seule d'une analyse EML.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
#define LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
#include "core/eml_processing.h"
#include "core/eml_integration.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
/** @brief Callback recevant les propositions sélectionnées, ou NULL. */
typedef void (*EmlAnalysisDialogCallback)(GPtrArray *proposals,
gpointer user_data);
/**
* @brief Affiche les métadonnées et indicateurs extraits d'une copie EML.
* @param parent Fenêtre parente.
* @param result Résultat emprunté pendant la construction de la fenêtre.
*/
void eml_analysis_dialog_present(GtkWindow *parent,
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
gpointer user_data);
G_END_DECLS
#endif

View file

@ -98,6 +98,9 @@ typedef void (*MainWindowEditEvidenceCallback)(
const char *evidence_identifier,
gpointer user_data
);
/** @brief Callback appelé pour analyser une preuve EML. */
typedef void (*MainWindowAnalyzeEmlCallback)(const char *evidence_identifier,
gpointer user_data);
/**
* @brief Callback appelé après le déplacement effectif d'un nœud.
@ -175,6 +178,9 @@ void main_window_set_edit_evidence_callback(
MainWindowEditEvidenceCallback callback,
gpointer user_data
);
/** @brief Définit le callback d'analyse locale d'une preuve EML. */
void main_window_set_analyze_eml_callback(MainWindow *main_window,
MainWindowAnalyzeEmlCallback callback, gpointer user_data);
/**
* @brief Définit le callback de fin de déplacement d'un nœud.

View file

@ -100,6 +100,9 @@ typedef void (*WorkspaceEditEvidenceCallback)(
const char *evidence_identifier,
gpointer user_data
);
/** @brief Callback appelé pour analyser une preuve EML. */
typedef void (*WorkspaceAnalyzeEmlCallback)(const char *evidence_identifier,
gpointer user_data);
/**
* @brief Crée une nouvelle zone de travail.
@ -221,6 +224,9 @@ void workspace_set_edit_evidence_callback(
WorkspaceEditEvidenceCallback callback,
gpointer user_data
);
/** @brief Définit le callback d'analyse locale d'une preuve EML. */
void workspace_set_analyze_eml_callback(Workspace *workspace,
WorkspaceAnalyzeEmlCallback callback, gpointer user_data);
/**
* @brief Définit le callback de vérification de la preuve affichée.

View file

@ -49,9 +49,12 @@
#include "core/relation_service.h"
#include "core/social_account_service.h"
#include "core/person_entity_service.h"
#include "core/eml_processing.h"
#include "core/eml_integration.h"
#include "database/database.h"
#include "views/create_social_account_dialog.h"
#include "views/create_person_dialog.h"
#include "views/eml_analysis_dialog.h"
#include <gtk/gtk.h>
#include <errno.h>
@ -212,6 +215,9 @@ typedef struct
char *execution_identifier;
GPtrArray *proposals;
} ApplicationOsintReviewContext;
/** @brief Contexte possédé pendant la révision d'une analyse EML. */
typedef struct { Application *application; char *evidence_identifier; }
ApplicationEmlReviewContext;
static void application_start_graph_loading(
Application *application,
@ -3289,6 +3295,75 @@ cleanup:
evidence_dao_free(evidence_dao);
}
/** @brief Libère le contexte de révision EML. */
static void application_eml_review_context_free(ApplicationEmlReviewContext *context)
{ if (context == NULL) return; g_free(context->evidence_identifier); g_free(context); }
/** @brief Intègre la sélection EML puis actualise le graphe. */
static void application_on_eml_selection_completed(
GPtrArray *proposals, gpointer user_data)
{
ApplicationEmlReviewContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
const InvestigationProject *project = NULL;
GError *error = NULL; guint created = 0, reused = 0; char *message = NULL;
if (proposals == NULL)
{ application_eml_review_context_free(context); return; }
if (application == NULL || application->session == NULL ||
!eml_integration_apply(investigation_session_get_database(application->session),
context->evidence_identifier, proposals, &created, &reused, &error))
application_present_error(application, "Intégration EML impossible",
error != NULL ? error->message : "La transaction a échoué.");
else
{
message = g_strdup_printf("%u entité(s) créée(s), %u réutilisée(s).",
created, reused);
application_message_dialog_present(main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
project = investigation_session_get_project(application->session);
application_start_graph_loading(application,
investigation_project_get_database_path(project));
}
g_free(message); g_clear_error(&error); g_ptr_array_unref(proposals);
application_eml_review_context_free(context);
}
/** @brief Prépare une copie vérifiée puis affiche l'analyse EML locale. */
static void application_on_analyze_eml_requested(
const char *evidence_identifier, gpointer user_data)
{
Application *application = user_data;
const InvestigationProject *project = NULL;
EvidenceDao *dao = NULL;
EvidenceRecord *record = NULL;
EmlProcessingResult *result = NULL;
ApplicationEmlReviewContext *context = NULL;
GError *error = NULL;
if (application == NULL || application->session == NULL ||
evidence_identifier == NULL) return;
dao = evidence_dao_new(
investigation_session_get_database(application->session), &error);
if (dao != NULL) record = evidence_dao_find_by_identifier(
dao, evidence_identifier, &error);
project = investigation_session_get_project(application->session);
if (record != NULL && project != NULL)
result = eml_processing_prepare(
investigation_project_get_root_path(project), record, &error);
if (result == NULL)
application_present_error(application, "Analyse EML impossible",
error != NULL ? error->message : "La copie de travail n'a pas pu être analysée.");
else
{
context = g_new0(ApplicationEmlReviewContext, 1);
context->application = application;
context->evidence_identifier = g_strdup(evidence_identifier);
eml_analysis_dialog_present(main_window_get_window(application->main_window),
result, application_on_eml_selection_completed, context);
}
eml_processing_result_free(result); evidence_record_free(record);
evidence_dao_free(dao); g_clear_error(&error);
}
/**
* @brief Prépare et démarre limport asynchrone dun fichier.
*/
@ -6231,6 +6306,8 @@ static void application_on_activate(
application_on_edit_evidence_requested,
application
);
main_window_set_analyze_eml_callback(application->main_window,
application_on_analyze_eml_requested, application);
main_window_set_graph_node_moved_callback(
application->main_window,

204
src/core/eml_analyzer.c Normal file
View file

@ -0,0 +1,204 @@
/******************************************************************************
* @file eml_analyzer.c
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
******************************************************************************/
#include "core/eml_analyzer.h"
#include <string.h>
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
#define EML_ANALYZER_MAX_HEADER_SIZE (2U * 1024U * 1024U)
struct EmlAnalysis
{
GHashTable *headers;
GPtrArray *emails;
GPtrArray *domains;
GPtrArray *ips;
GPtrArray *sender_ips;
GPtrArray *destination_ips;
char *raw_headers;
};
/** @brief Libère un tableau de valeurs d'en-tête. */
static void eml_analyzer_values_free(gpointer data)
{
g_ptr_array_unref(data);
}
/** @brief Ajoute une chaîne normalisée si elle n'existe pas déjà. */
static void eml_analyzer_add_unique(GPtrArray *values, const char *value,
gboolean lowercase)
{
char *copy = value != NULL ? g_strdup(value) : NULL;
if (copy == NULL) return;
g_strstrip(copy);
if (lowercase)
{
char *lower = g_utf8_strdown(copy, -1); g_free(copy); copy = lower;
}
if (copy == NULL || copy[0] == '\0') { g_free(copy); return; }
for (guint i = 0; i < values->len; i++)
if (g_ascii_strcasecmp(g_ptr_array_index(values, i), copy) == 0)
{ g_free(copy); return; }
g_ptr_array_add(values, copy);
}
/** @brief Extrait toutes les occurrences reconnues par une expression. */
static void eml_analyzer_extract_regex(GRegex *regex, const char *text,
GPtrArray *values, gboolean lowercase)
{
GMatchInfo *matches = NULL;
if (regex == NULL || text == NULL) return;
g_regex_match(regex, text, 0, &matches);
while (g_match_info_matches(matches))
{
char *match = g_match_info_fetch(matches, 1);
eml_analyzer_add_unique(values, match, lowercase);
g_free(match);
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. */
static void eml_analyzer_extract_received_part(GRegex *part_regex,
GRegex *ip_regex, const char *received, GPtrArray *values)
{
GMatchInfo *match = NULL;
char *part = NULL;
if (part_regex == NULL || ip_regex == NULL || received == NULL)
return;
if (g_regex_match(part_regex, received, 0, &match))
{
part = g_match_info_fetch(match, 1);
eml_analyzer_extract_regex(ip_regex, part, values, FALSE);
g_free(part);
}
g_match_info_free(match);
}
/** @brief Ajoute un en-tête déplié au résultat. */
static gboolean eml_analyzer_add_header(EmlAnalysis *analysis,
const char *name, const char *value)
{
GPtrArray *values = NULL;
char *key = NULL, *safe_value = NULL;
if (analysis == NULL || name == NULL || value == NULL) return FALSE;
key = g_ascii_strdown(name, -1);
safe_value = g_utf8_make_valid(value, -1);
if (key == NULL || safe_value == NULL) { g_free(key); g_free(safe_value); return FALSE; }
g_strstrip(key); g_strstrip(safe_value);
values = g_hash_table_lookup(analysis->headers, key);
if (values == NULL)
{
values = g_ptr_array_new_with_free_func(g_free);
g_hash_table_insert(analysis->headers, key, values); key = NULL;
}
g_ptr_array_add(values, safe_value); g_free(key); return TRUE;
}
EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
{
EmlAnalysis *analysis = NULL;
GError *local_error = NULL;
GMappedFile *mapped = NULL;
const char *data = NULL, *separator = NULL;
gsize size = 0, header_size = 0;
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
GString *current_value = NULL;
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (file_path == NULL || file_path[0] == '\0')
{ g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Le chemin du fichier EML est invalide."); return NULL; }
mapped = g_mapped_file_new(file_path, FALSE, &local_error);
if (mapped == NULL) { g_propagate_error(error, local_error); return NULL; }
size = g_mapped_file_get_length(mapped); data = g_mapped_file_get_contents(mapped);
if (size == 0 || size > EML_ANALYZER_MAX_FILE_SIZE)
{ g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Le fichier EML est vide ou dépasse la limite de 25 Mio."); goto cleanup; }
separator = g_strstr_len(data, (gssize) size, "\r\n\r\n");
header_size = separator != NULL ? (gsize) (separator - data) : 0;
if (separator == NULL)
{ separator = g_strstr_len(data, (gssize) size, "\n\n");
header_size = separator != NULL ? (gsize) (separator - data) : 0; }
if (separator == NULL || header_size == 0 || header_size > EML_ANALYZER_MAX_HEADER_SIZE)
{ g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"Le fichier ne contient pas un bloc d'en-têtes EML valide."); goto cleanup; }
analysis = g_new0(EmlAnalysis, 1);
analysis->headers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
eml_analyzer_values_free);
analysis->emails = g_ptr_array_new_with_free_func(g_free);
analysis->domains = 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->destination_ips = g_ptr_array_new_with_free_func(g_free);
analysis->raw_headers = g_utf8_make_valid(data, (gssize) header_size);
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
current_value = g_string_new(NULL);
for (guint i = 0; lines[i] != NULL; i++)
{
char *line = lines[i]; g_strchomp(line);
if ((line[0] == ' ' || line[0] == '\t') && current_name != NULL)
{ g_string_append_c(current_value, ' '); g_string_append(current_value, g_strstrip(line)); continue; }
if (current_name != NULL)
{ eml_analyzer_add_header(analysis, current_name, current_value->str);
g_clear_pointer(&current_name, g_free); g_string_truncate(current_value, 0); }
char *colon = strchr(line, ':');
if (colon == NULL || colon == line) continue;
current_name = g_strndup(line, (gsize) (colon - line));
g_string_assign(current_value, g_strstrip(colon + 1));
}
if (current_name != NULL) eml_analyzer_add_header(analysis,
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);
domain_regex = g_regex_new("(?:@|[.\\s<([])([A-Za-z0-9-]+(?:\\.[A-Za-z0-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);
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);
eml_analyzer_extract_regex(email_regex, analysis->raw_headers, analysis->emails, TRUE);
eml_analyzer_extract_regex(domain_regex, analysis->raw_headers, analysis->domains, TRUE);
eml_analyzer_extract_regex(ip_regex, analysis->raw_headers, analysis->ips, FALSE);
const GPtrArray *received_values = eml_analysis_get_header_values(
analysis, "received");
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
{
const char *received = g_ptr_array_index(
(GPtrArray *) received_values, i);
eml_analyzer_extract_received_part(received_from_regex, ip_regex,
received, analysis->sender_ips);
eml_analyzer_extract_received_part(received_by_regex, ip_regex,
received, analysis->destination_ips);
}
cleanup:
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(&current_name, g_free);
g_clear_pointer(&received_from_regex, g_regex_unref);
g_clear_pointer(&received_by_regex, g_regex_unref);
if (current_value != NULL) g_string_free(current_value, TRUE);
g_strfreev(lines); g_free(utf8); g_mapped_file_unref(mapped);
return analysis;
}
void eml_analysis_free(EmlAnalysis *analysis)
{
if (analysis == NULL) return;
g_hash_table_unref(analysis->headers); g_ptr_array_unref(analysis->emails);
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
g_ptr_array_unref(analysis->sender_ips);
g_ptr_array_unref(analysis->destination_ips);
g_free(analysis->raw_headers); g_free(analysis);
}
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
const char *name)
{
char *key = NULL; GPtrArray *values = NULL;
if (analysis == NULL || name == NULL) return NULL;
key = g_ascii_strdown(name, -1); values = g_hash_table_lookup(analysis->headers, key);
g_free(key); return values;
}
const char *eml_analysis_get_first_header(const EmlAnalysis *analysis,
const char *name)
{
const GPtrArray *values = eml_analysis_get_header_values(analysis, name);
return values != NULL && values->len > 0 ? g_ptr_array_index((GPtrArray *) values, 0) : 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_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_destination_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->destination_ips : NULL; }
const char *eml_analysis_get_raw_headers(const EmlAnalysis *a) { return a != NULL ? a->raw_headers : NULL; }

114
src/core/eml_integration.c Normal file
View file

@ -0,0 +1,114 @@
/******************************************************************************
* @file eml_integration.c
* @brief Intégration transactionnelle de propositions issues d'un EML.
******************************************************************************/
#include "core/eml_integration.h"
#include "dao/entity_dao.h"
#include "dao/evidence_entity_dao.h"
#include "database/transaction.h"
#include "models/entity_record.h"
EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value)
{
EmlEntityProposal *proposal = NULL;
if (type == NULL || type[0] == '\0' || value == NULL || value[0] == '\0') return NULL;
proposal = g_new0(EmlEntityProposal, 1);
proposal->type_identifier = g_strdup(type); proposal->value = g_strdup(value);
if (proposal->type_identifier == NULL || proposal->value == NULL)
{ eml_entity_proposal_free(proposal); return NULL; }
return proposal;
}
void eml_entity_proposal_free(EmlEntityProposal *proposal)
{
if (proposal == NULL) return;
g_free(proposal->type_identifier); g_free(proposal->value); g_free(proposal);
}
/** @brief Recherche une entité existante avec le même type et la même valeur. */
static const EntityRecord *eml_integration_find_existing(const GPtrArray *entities,
const EmlEntityProposal *proposal)
{
for (guint i = 0; entities != NULL && i < entities->len; i++)
{
const EntityRecord *record = g_ptr_array_index((GPtrArray *) entities, i);
if (g_strcmp0(entity_record_get_type_identifier(record),
proposal->type_identifier) == 0 &&
g_ascii_strcasecmp(entity_record_get_value(record), proposal->value) == 0)
return record;
}
return NULL;
}
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
const GPtrArray *proposals, guint *out_created, guint *out_reused, GError **error)
{
EntityDao *entity_dao = NULL;
EvidenceEntityDao *link_dao = NULL;
GPtrArray *entities = NULL;
GDateTime *now = NULL;
char *timestamp = NULL;
guint created = 0;
guint reused = 0;
gboolean active = FALSE;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_created != NULL)
*out_created = 0;
if (out_reused != NULL)
*out_reused = 0;
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
{
g_set_error_literal(error,
g_quark_from_static_string("eml-integration-error"), 1,
"Sélectionnez au moins une proposition EML.");
return FALSE;
}
if (!database_transaction_begin(database))
return FALSE;
active = TRUE;
entity_dao = entity_dao_new(database, error);
link_dao = evidence_entity_dao_new(database, error);
if (entity_dao == NULL || link_dao == NULL) goto cleanup;
entities = entity_dao_list_all(entity_dao, error); if (entities == NULL) goto cleanup;
now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
if (timestamp == NULL) goto cleanup;
for (guint i = 0; i < proposals->len; i++)
{
EmlEntityProposal *proposal = g_ptr_array_index((GPtrArray *) proposals, i);
const EntityRecord *existing = eml_integration_find_existing(entities, proposal);
const char *identifier = existing != NULL
? entity_record_get_identifier(existing) : NULL;
char *new_identifier = NULL; EntityRecord *new_record = NULL;
gboolean linked = FALSE;
if (existing == NULL)
{
new_identifier = g_uuid_string_random(); identifier = new_identifier;
new_record = entity_record_new(identifier, proposal->type_identifier,
proposal->value, proposal->value,
"Indicateur extrait des en-têtes de la preuve EML.", 50,
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
{ entity_record_free(new_record); g_free(new_identifier); goto cleanup; }
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
}
else reused++;
if (!evidence_entity_dao_exists(link_dao, evidence_identifier,
identifier, &linked, error) ||
(!linked && !evidence_entity_dao_link(link_dao,
evidence_identifier, identifier, error)))
{ g_free(new_identifier); goto cleanup; }
g_free(new_identifier);
}
if (!database_transaction_commit(database)) goto cleanup;
active = FALSE; success = TRUE;
if (out_created != NULL) *out_created = created;
if (out_reused != NULL) *out_reused = reused;
cleanup:
if (!success && active) database_transaction_rollback(database);
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
g_clear_pointer(&entities, g_ptr_array_unref);
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
return success;
}

69
src/core/eml_processing.c Normal file
View file

@ -0,0 +1,69 @@
/******************************************************************************
* @file eml_processing.c
* @brief Préparation vérifiée d'une copie de travail EML.
******************************************************************************/
#include "core/eml_processing.h"
#include "core/evidence_copy.h"
#include "core/file_hash.h"
#include <string.h>
struct EmlProcessingResult { char *copy_path; EmlAnalysis *analysis; };
EmlProcessingResult *eml_processing_prepare(const char *root,
const EvidenceRecord *record, GError **error)
{
EmlProcessingResult *result = NULL;
EvidenceCopyResult *copy = NULL;
const char *identifier = evidence_record_get_identifier(record);
const char *relative = evidence_record_get_relative_path(record);
const char *expected_hash = evidence_record_get_sha256(record);
char *source = NULL, *directory = NULL, *name = NULL, *destination = NULL;
char *existing_hash = NULL; guint64 existing_size = 0;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (root == NULL || identifier == NULL || relative == NULL || expected_hash == NULL)
{ g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
"La preuve EML ou la racine de l'enquête est invalide."); return NULL; }
source = g_build_filename(root, relative, NULL);
directory = g_build_filename(root, "02_Preuves_Traitees", "Extractions", NULL);
name = g_strdup_printf("%s_email_headers.eml", identifier);
destination = g_build_filename(directory, name, NULL);
if (g_file_test(destination, G_FILE_TEST_EXISTS))
{
if (!file_hash_compute_sha256(destination, NULL, &existing_hash,
&existing_size, error) || strcmp(existing_hash, expected_hash) != 0)
{
if (error != NULL && *error == NULL) g_set_error_literal(error,
G_FILE_ERROR, G_FILE_ERROR_FAILED,
"La copie EML existante ne correspond pas à la preuve originale.");
goto cleanup;
}
}
else
{
copy = evidence_copy_file(source, directory, name, NULL, error);
if (copy == NULL || strcmp(evidence_copy_result_get_sha256(copy),
expected_hash) != 0)
{
if (error != NULL && *error == NULL) g_set_error_literal(error,
G_FILE_ERROR, G_FILE_ERROR_FAILED,
"La copie EML ne correspond pas à l'empreinte enregistrée.");
goto cleanup;
}
}
result = g_new0(EmlProcessingResult, 1);
result->copy_path = g_strdup(destination);
result->analysis = eml_analyzer_analyze_file(destination, error);
if (result->copy_path == NULL || result->analysis == NULL)
{ eml_processing_result_free(result); result = NULL; }
cleanup:
evidence_copy_result_free(copy); g_free(existing_hash);
g_free(source); g_free(directory); g_free(name); g_free(destination);
return result;
}
void eml_processing_result_free(EmlProcessingResult *result)
{
if (result == NULL) return;
eml_analysis_free(result->analysis); g_free(result->copy_path); g_free(result);
}
const char *eml_processing_result_get_copy_path(const EmlProcessingResult *result)
{ return result != NULL ? result->copy_path : NULL; }
const EmlAnalysis *eml_processing_result_get_analysis(const EmlProcessingResult *result)
{ return result != NULL ? result->analysis : NULL; }

View file

@ -0,0 +1,186 @@
/******************************************************************************
* @file eml_analysis_dialog.c
* @brief Présentation en lecture seule d'une analyse EML.
******************************************************************************/
#include "views/eml_analysis_dialog.h"
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
} EmlAnalysisDialogState;
/** @brief Libère l'état de révision. */
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
/** @brief Signale une annulation une seule fois. */
static void eml_analysis_dialog_cancel(EmlAnalysisDialogState *state)
{ if (state == NULL || state->completed) return; state->completed = TRUE;
if (state->callback != NULL) state->callback(NULL, state->user_data); }
/** @brief Traite la fermeture native. */
static gboolean eml_analysis_dialog_on_window_close(GtkWindow *window, gpointer data)
{ (void) window; eml_analysis_dialog_cancel(data); return FALSE; }
/** @brief Ferme la fenêtre de résultat. */
static void eml_analysis_dialog_on_close(GtkButton *button, gpointer data)
{ EmlAnalysisDialogState *state = data; (void) button;
eml_analysis_dialog_cancel(state); gtk_window_close(state->window); }
/** @brief Transmet uniquement les propositions explicitement cochées. */
static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
{
EmlAnalysisDialogState *state = data;
GPtrArray *selected = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_entity_proposal_free);
(void) button;
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
child != NULL; child = gtk_widget_get_next_sibling(child))
{
const char *type = g_object_get_data(G_OBJECT(child), "eml-type");
const char *value = g_object_get_data(G_OBJECT(child), "eml-value");
if (GTK_IS_CHECK_BUTTON(child) && gtk_check_button_get_active(
GTK_CHECK_BUTTON(child)))
g_ptr_array_add(selected, eml_entity_proposal_new(type, value));
}
if (selected->len == 0)
{ g_ptr_array_unref(selected); return; }
state->completed = TRUE;
if (state->callback != NULL) state->callback(selected, state->user_data);
else g_ptr_array_unref(selected);
gtk_window_close(state->window);
}
/** @brief Ajoute les propositions d'un type sous forme de cases décochées. */
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
const char *type, const char *label, const GPtrArray *values)
{
for (guint i = 0; values != NULL && i < values->len; i++)
{
const char *value = g_ptr_array_index((GPtrArray *) values, i);
char *text = g_strdup_printf("%s : %s", label, value);
GtkWidget *check = gtk_check_button_new_with_label(text);
g_object_set_data_full(G_OBJECT(check), "eml-type", g_strdup(type), g_free);
g_object_set_data_full(G_OBJECT(check), "eml-value", g_strdup(value), g_free);
gtk_box_append(GTK_BOX(box), check); g_free(text);
}
}
/** @brief Ajoute une ligne de métadonnée sélectionnable. */
static void eml_analysis_dialog_add_field(GtkGrid *grid, int row,
const char *title, const char *value)
{
GtkWidget *name = gtk_label_new(title);
GtkWidget *content = gtk_label_new(value != NULL ? value : "Non présent");
gtk_label_set_xalign(GTK_LABEL(name), 0.0f);
gtk_label_set_xalign(GTK_LABEL(content), 0.0f);
gtk_label_set_selectable(GTK_LABEL(content), TRUE);
gtk_label_set_wrap(GTK_LABEL(content), TRUE);
gtk_widget_set_valign(name, GTK_ALIGN_START);
gtk_grid_attach(grid, name, 0, row, 1, 1);
gtk_grid_attach(grid, content, 1, row, 1, 1);
}
/** @brief Concatène un tableau de chaînes pour son affichage. */
static char *eml_analysis_dialog_join(const GPtrArray *values)
{
GString *text = g_string_new(NULL);
for (guint i = 0; values != NULL && i < values->len; i++)
g_string_append_printf(text, "%s%s", i > 0 ? "\n" : "",
(const char *) g_ptr_array_index((GPtrArray *) values, i));
return g_string_free(text, FALSE);
}
void eml_analysis_dialog_present(GtkWindow *parent,
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
gpointer user_data)
{
const EmlAnalysis *analysis = eml_processing_result_get_analysis(result);
GtkWindow *window = NULL;
GtkWidget *box = NULL, *content = NULL, *scroll = NULL;
GtkWidget *grid = NULL, *raw_scroll = NULL, *raw = NULL;
GtkWidget *close = NULL, *integrate = NULL, *actions = NULL;
EmlAnalysisDialogState *state = NULL;
char *received = NULL, *emails = NULL, *domains = NULL;
char *sender_ips = NULL, *destination_ips = NULL;
if (parent == NULL || analysis == NULL) return;
state = g_new0(EmlAnalysisDialogState, 1);
state->callback = callback; state->user_data = user_data;
window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(window, "Analyse locale du courriel EML");
gtk_window_set_transient_for(window, parent); gtk_window_set_modal(window, TRUE);
gtk_window_set_default_size(window, 720, 560);
state->window = window;
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(box, 14); gtk_widget_set_margin_end(box, 14);
gtk_widget_set_margin_top(box, 14); gtk_widget_set_margin_bottom(box, 14);
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_box_append(GTK_BOX(content), gtk_label_new(
"Analyse effectuée sur la copie vérifiée de 02_Preuves_Traitees/Extractions."));
grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
eml_analysis_dialog_add_field(GTK_GRID(grid), 0, "Copie analysée",
eml_processing_result_get_copy_path(result));
eml_analysis_dialog_add_field(GTK_GRID(grid), 1, "From",
eml_analysis_get_first_header(analysis, "from"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 2, "Reply-To",
eml_analysis_get_first_header(analysis, "reply-to"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 3, "To",
eml_analysis_get_first_header(analysis, "to"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 4, "Sujet",
eml_analysis_get_first_header(analysis, "subject"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 5, "Date",
eml_analysis_get_first_header(analysis, "date"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 6, "Message-ID",
eml_analysis_get_first_header(analysis, "message-id"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 7, "Authentification",
eml_analysis_get_first_header(analysis, "authentication-results"));
received = eml_analysis_dialog_join(
eml_analysis_get_header_values(analysis, "received"));
emails = eml_analysis_dialog_join(eml_analysis_get_email_addresses(analysis));
domains = eml_analysis_dialog_join(eml_analysis_get_domains(analysis));
sender_ips = eml_analysis_dialog_join(
eml_analysis_get_sender_ip_addresses(analysis));
destination_ips = eml_analysis_dialog_join(
eml_analysis_get_destination_ip_addresses(analysis));
eml_analysis_dialog_add_field(GTK_GRID(grid), 8, "Received (ordre original)", received);
eml_analysis_dialog_add_field(GTK_GRID(grid), 9, "Emails proposés", emails);
eml_analysis_dialog_add_field(GTK_GRID(grid), 10, "Domaines proposés", domains);
eml_analysis_dialog_add_field(GTK_GRID(grid), 11, "IP expéditeur",
sender_ips);
eml_analysis_dialog_add_field(GTK_GRID(grid), 12, "IP destinataire",
destination_ips);
gtk_box_append(GTK_BOX(content), grid);
gtk_box_append(GTK_BOX(content), gtk_label_new(
"Sélection explicite des entités à intégrer"));
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
eml_analysis_dialog_add_proposals(state->proposals_box, "email_address",
"Email", eml_analysis_get_email_addresses(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), gtk_label_new("En-têtes bruts (lecture seule)"));
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_wrap_mode(GTK_TEXT_VIEW(raw), GTK_WRAP_WORD_CHAR);
gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(raw)),
eml_analysis_get_raw_headers(analysis), -1);
raw_scroll = gtk_scrolled_window_new();
gtk_widget_set_size_request(raw_scroll, -1, 180);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(raw_scroll), raw);
gtk_box_append(GTK_BOX(content), raw_scroll);
scroll = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scroll, TRUE);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content);
gtk_box_append(GTK_BOX(box), scroll);
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(actions, GTK_ALIGN_END);
close = gtk_button_new_with_label("Fermer");
integrate = gtk_button_new_with_label("Intégrer la sélection");
gtk_widget_add_css_class(integrate, "suggested-action");
g_signal_connect(close, "clicked", G_CALLBACK(eml_analysis_dialog_on_close), state);
g_signal_connect(integrate, "clicked", G_CALLBACK(eml_analysis_dialog_on_integrate), state);
gtk_box_append(GTK_BOX(actions), close); gtk_box_append(GTK_BOX(actions), integrate);
gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(window, box);
g_signal_connect(window, "close-request",
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
g_object_set_data_full(G_OBJECT(window), "eml-dialog-state", state,
eml_analysis_dialog_state_free);
gtk_window_present(window);
g_free(received); g_free(emails); g_free(domains);
g_free(sender_ips); g_free(destination_ips);
}

View file

@ -108,6 +108,8 @@ struct MainWindow
MainWindowEditEvidenceCallback edit_evidence_callback;
gpointer edit_evidence_user_data;
MainWindowAnalyzeEmlCallback analyze_eml_callback;
gpointer analyze_eml_user_data;
MainWindowGraphNodeMovedCallback
graph_node_moved_callback;
@ -176,6 +178,15 @@ static void main_window_on_edit_evidence_requested(
main_window->edit_evidence_callback(
evidence_identifier, main_window->edit_evidence_user_data);
}
/** @brief Relaie la demande d'analyse EML vers l'application. */
static void main_window_on_analyze_eml_requested(const char *identifier,
gpointer data)
{
MainWindow *main_window = data;
if (main_window != NULL && main_window->analyze_eml_callback != NULL)
main_window->analyze_eml_callback(identifier,
main_window->analyze_eml_user_data);
}
/**
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
@ -847,6 +858,8 @@ MainWindow *main_window_new(
main_window_on_edit_evidence_requested,
main_window
);
workspace_set_analyze_eml_callback(main_window->workspace,
main_window_on_analyze_eml_requested, main_window);
workspace_widget = workspace_get_widget(
main_window->workspace
@ -1372,6 +1385,13 @@ void main_window_set_edit_evidence_callback(
main_window->edit_evidence_callback = callback;
main_window->edit_evidence_user_data = user_data;
}
void main_window_set_analyze_eml_callback(MainWindow *main_window,
MainWindowAnalyzeEmlCallback callback, gpointer user_data)
{
if (main_window == NULL) return;
main_window->analyze_eml_callback = callback;
main_window->analyze_eml_user_data = user_data;
}
void main_window_set_tree_selection_callback(
MainWindow *main_window,

View file

@ -86,6 +86,7 @@ struct Workspace
GtkWidget *evidence_sha256_label;
GtkWidget *verify_evidence_button;
GtkWidget *edit_evidence_button;
GtkWidget *analyze_eml_button;
char *selected_evidence_identifier;
@ -97,6 +98,8 @@ struct Workspace
WorkspaceEditEvidenceCallback edit_evidence_callback;
gpointer edit_evidence_user_data;
WorkspaceAnalyzeEmlCallback analyze_eml_callback;
gpointer analyze_eml_user_data;
WorkspaceGraphNodeMovedCallback
graph_node_moved_callback;
@ -851,6 +854,15 @@ static void workspace_on_edit_evidence_clicked(
workspace->selected_evidence_identifier,
workspace->edit_evidence_user_data);
}
/** @brief Transmet la demande d'analyse de la preuve EML affichée. */
static void workspace_on_analyze_eml_clicked(GtkButton *button, gpointer data)
{
Workspace *workspace = data; (void) button;
if (workspace != NULL && workspace->analyze_eml_callback != NULL &&
workspace->selected_evidence_identifier != NULL)
workspace->analyze_eml_callback(workspace->selected_evidence_identifier,
workspace->analyze_eml_user_data);
}
Workspace *workspace_new(void)
{
@ -1180,6 +1192,15 @@ Workspace *workspace_new(void)
g_signal_connect(workspace->edit_evidence_button, "clicked",
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
workspace->analyze_eml_button = gtk_button_new_with_label(
"Analyser les en-têtes EML");
gtk_widget_set_halign(workspace->analyze_eml_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
gtk_widget_set_tooltip_text(workspace->analyze_eml_button,
"Créer une copie vérifiée puis analyser localement ses en-têtes");
g_signal_connect(workspace->analyze_eml_button, "clicked",
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button);
evidence_separator =
gtk_separator_new(
@ -2109,6 +2130,8 @@ void workspace_set_selected_node(
}
if (workspace->edit_evidence_button != NULL)
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
if (workspace->analyze_eml_button != NULL)
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
if (node == NULL)
{
@ -2264,6 +2287,13 @@ void workspace_set_selected_evidence(
TRUE
);
gtk_widget_set_sensitive(workspace->edit_evidence_button, TRUE);
{
const char *name = evidence_record_get_original_name(evidence_record);
char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL;
gtk_widget_set_sensitive(workspace->analyze_eml_button,
lower != NULL && g_str_has_suffix(lower, ".eml"));
g_free(lower);
}
size_bytes =
evidence_record_get_size_bytes(
@ -2814,6 +2844,13 @@ void workspace_set_edit_evidence_callback(
workspace->edit_evidence_callback = callback;
workspace->edit_evidence_user_data = user_data;
}
void workspace_set_analyze_eml_callback(Workspace *workspace,
WorkspaceAnalyzeEmlCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->analyze_eml_callback = callback;
workspace->analyze_eml_user_data = user_data;
}
void workspace_set_graph_node_moved_callback(
Workspace *workspace,

64
tests/test_eml_analyzer.c Normal file
View file

@ -0,0 +1,64 @@
/******************************************************************************
* @file test_eml_analyzer.c
* @brief Tests de l'analyse locale d'en-têtes EML synthétiques.
******************************************************************************/
#include "core/eml_analyzer.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
/** @brief Vérifie le dépliage et les indicateurs extraits. */
static void test_eml_analyzer_headers(void)
{
static const char content[] =
"From: Example Sender <sender@example.test>\r\n"
"Reply-To: replies@reply.test\r\n"
"To: victim@example.net\r\n"
"Subject: Synthetic fixture\r\n"
"Message-ID: <id-123@example.test>\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"
"Received: from localhost ([127.0.0.1]) by mail.example.test\r\n"
"Authentication-Results: mx.example.net; spf=pass; dkim=pass; dmarc=pass\r\n"
"\r\nBody must not be parsed: hidden@body.test\r\n";
char *directory = NULL, *path = NULL;
EmlAnalysis *analysis = NULL;
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
GError *error = NULL;
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
assert(directory != NULL && error == NULL);
path = g_build_filename(directory, "synthetic.eml", NULL);
assert(g_file_set_contents(path, content, -1, &error));
analysis = eml_analyzer_analyze_file(path, &error);
assert(analysis != NULL && error == NULL);
assert(strcmp(eml_analysis_get_first_header(analysis, "from"),
"Example Sender <sender@example.test>") == 0);
received = eml_analysis_get_header_values(analysis, "Received");
assert(received != NULL && received->len == 2);
assert(strstr(g_ptr_array_index((GPtrArray *) received, 0), " by mx.example.net") != NULL);
emails = eml_analysis_get_email_addresses(analysis);
assert(emails != NULL && emails->len >= 3);
for (guint i = 0; i < emails->len; i++)
assert(strcmp(g_ptr_array_index((GPtrArray *) emails, i),
"hidden@body.test") != 0);
ips = eml_analysis_get_ip_addresses(analysis);
assert(ips != NULL && ips->len == 3);
sender_ips = eml_analysis_get_sender_ip_addresses(analysis);
destination_ips = eml_analysis_get_destination_ip_addresses(analysis);
assert(sender_ips != NULL && sender_ips->len == 2);
assert(destination_ips != NULL && destination_ips->len == 1);
assert(strcmp(g_ptr_array_index((GPtrArray *) sender_ips, 0),
"192.0.2.10") == 0);
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
"198.51.100.20") == 0);
eml_analysis_free(analysis);
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
g_free(path); g_free(directory);
}
int main(void)
{
test_eml_analyzer_headers();
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
}