diff --git a/Makefile b/Makefile index fc02f71..d78971a 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/include/core/eml_analyzer.h b/include/core/eml_analyzer.h new file mode 100644 index 0000000..0e44000 --- /dev/null +++ b/include/core/eml_analyzer.h @@ -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 +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 diff --git a/include/core/eml_integration.h b/include/core/eml_integration.h new file mode 100644 index 0000000..1a58b9d --- /dev/null +++ b/include/core/eml_integration.h @@ -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 +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 diff --git a/include/core/eml_processing.h b/include/core/eml_processing.h new file mode 100644 index 0000000..9db2caf --- /dev/null +++ b/include/core/eml_processing.h @@ -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 +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 diff --git a/include/views/eml_analysis_dialog.h b/include/views/eml_analysis_dialog.h new file mode 100644 index 0000000..1aaafe7 --- /dev/null +++ b/include/views/eml_analysis_dialog.h @@ -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 +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 diff --git a/include/views/main_window.h b/include/views/main_window.h index 19d441b..dc79869 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -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. diff --git a/include/widgets/workspace.h b/include/widgets/workspace.h index 433c9b6..46111da 100644 --- a/include/widgets/workspace.h +++ b/include/widgets/workspace.h @@ -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. diff --git a/src/core/application.c b/src/core/application.c index 4f988be..2458972 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -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 #include @@ -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 l’import asynchrone d’un 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, diff --git a/src/core/eml_analyzer.c b/src/core/eml_analyzer.c new file mode 100644 index 0000000..8a88acc --- /dev/null +++ b/src/core/eml_analyzer.c @@ -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 +#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(¤t_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(¤t_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; } diff --git a/src/core/eml_integration.c b/src/core/eml_integration.c new file mode 100644 index 0000000..24bffb1 --- /dev/null +++ b/src/core/eml_integration.c @@ -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; +} diff --git a/src/core/eml_processing.c b/src/core/eml_processing.c new file mode 100644 index 0000000..a1f1b16 --- /dev/null +++ b/src/core/eml_processing.c @@ -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 +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; } diff --git a/src/views/eml_analysis_dialog.c b/src/views/eml_analysis_dialog.c new file mode 100644 index 0000000..fd4eb3e --- /dev/null +++ b/src/views/eml_analysis_dialog.c @@ -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); +} diff --git a/src/views/main_window.c b/src/views/main_window.c index 55b9638..f2ddb7a 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -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, diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index a57859d..c92e6ad 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -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, diff --git a/tests/test_eml_analyzer.c b/tests/test_eml_analyzer.c new file mode 100644 index 0000000..5ad8fa5 --- /dev/null +++ b/tests/test_eml_analyzer.c @@ -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 +#include +#include +#include +#include +/** @brief Vérifie le dépliage et les indicateurs extraits. */ +static void test_eml_analyzer_headers(void) +{ + static const char content[] = + "From: Example Sender \r\n" + "Reply-To: replies@reply.test\r\n" + "To: victim@example.net\r\n" + "Subject: Synthetic fixture\r\n" + "Message-ID: \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 ") == 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; +}