feat(email): extract forensic metadata from EML evidence
This commit is contained in:
parent
655780a446
commit
a683266468
15 changed files with 916 additions and 2 deletions
11
Makefile
11
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)
|
||||
|
||||
|
|
|
|||
41
include/core/eml_analyzer.h
Normal file
41
include/core/eml_analyzer.h
Normal 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
|
||||
31
include/core/eml_integration.h
Normal file
31
include/core/eml_integration.h
Normal 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
|
||||
29
include/core/eml_processing.h
Normal file
29
include/core/eml_processing.h
Normal 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
|
||||
23
include/views/eml_analysis_dialog.h
Normal file
23
include/views/eml_analysis_dialog.h
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 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,
|
||||
|
|
|
|||
204
src/core/eml_analyzer.c
Normal file
204
src/core/eml_analyzer.c
Normal 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(¤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; }
|
||||
114
src/core/eml_integration.c
Normal file
114
src/core/eml_integration.c
Normal 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
69
src/core/eml_processing.c
Normal 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; }
|
||||
186
src/views/eml_analysis_dialog.c
Normal file
186
src/views/eml_analysis_dialog.c
Normal 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
64
tests/test_eml_analyzer.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue