feat(evidence): import multiple evidence files

This commit is contained in:
grayTerminal-sh 2026-07-22 13:28:08 +02:00
parent 9a4ba1de1c
commit 06fe3bcb21
5 changed files with 352 additions and 15 deletions

View file

@ -136,6 +136,8 @@ Le socle actuel comprend notamment :
- tâches asynchrones annulables ;
- gestionnaire de tâches ;
- panneau dactivité GTK ;
- import groupé de preuves avec confirmation globale, révision individuelle,
copie contrôlée, empreinte SHA-256 et bilan détaillé ;
- registre doutils externes ;
- exécution sécurisée par `GSubprocess` ;
- exécution doutils en tâche de fond ;

View file

@ -761,6 +761,11 @@ L'intégrité de la preuve est essentielle.
Les métadonnées permettent notamment de suivre les empreintes cryptographiques.
L'import groupé conserve ce modèle : chaque fichier sélectionné est révisé et
importé séparément. Les imports sont exécutés séquentiellement afin d'éviter
les écritures SQLite concurrentes. L'échec d'un fichier n'annule pas les
preuves déjà importées et apparaît dans le bilan final.
---
## 4.6 entites

View file

@ -29,6 +29,23 @@ typedef void (*FileDialogCallback)(
gpointer user_data
);
/**
* @brief Fonction appelée après une sélection multiple.
*
* paths est un tableau emprunté de chemins locaux possédés par le dialogue.
* Il n'est valide que pendant l'appel. Une annulation transmet NULL sans
* erreur.
*
* @param paths Chemins sélectionnés, ou NULL.
* @param error Erreur de sélection, ou NULL.
* @param user_data Données fournies lors de l'ouverture.
*/
typedef void (*FileDialogMultipleCallback)(
const GPtrArray *paths,
const GError *error,
gpointer user_data
);
/**
* @brief Ouvre un dialogue permettant de sélectionner un fichier.
*
@ -42,4 +59,16 @@ void file_dialog_select_file(
gpointer user_data
);
/**
* @brief Ouvre un dialogue permettant de sélectionner plusieurs fichiers.
* @param parent Fenêtre parente, éventuellement NULL.
* @param callback Fonction appelée à la fermeture du dialogue.
* @param user_data Données transmises au callback.
*/
void file_dialog_select_files(
GtkWindow *parent,
FileDialogMultipleCallback callback,
gpointer user_data
);
#endif

View file

@ -123,8 +123,21 @@ typedef struct
{
Application *application;
char *investigation_root_path;
gpointer batch_context;
} ApplicationEvidenceImportContext;
/** @brief État d'un import groupé traité séquentiellement. */
typedef struct
{
Application *application;
GPtrArray *file_paths;
GPtrArray *evidence_types;
GPtrArray *failures;
guint current_index;
guint success_count;
guint skipped_count;
} ApplicationEvidenceBatchContext;
/**
* @brief Contexte conservé jusqu'à la fin d'un chargement de graphe.
*/
@ -191,6 +204,14 @@ static void application_present_error(
const char *message
);
static void application_present_next_batch_metadata(
ApplicationEvidenceBatchContext *context
);
static void application_evidence_batch_context_free(
ApplicationEvidenceBatchContext *context
);
/** @brief Libère les données de révision possédées par le dialogue. */
static void application_osint_review_context_free(gpointer user_data)
{
@ -202,6 +223,24 @@ static void application_osint_review_context_free(gpointer user_data)
g_free(context);
}
/** @brief Démarre ou abandonne la préparation après confirmation globale. */
static void application_on_batch_selection_confirmed(
gboolean confirmed, gpointer user_data
)
{
ApplicationEvidenceBatchContext *context = user_data;
if (context == NULL) return;
if (confirmed) application_present_next_batch_metadata(context);
else
{
main_window_set_import_evidence_enabled(
context->application->main_window, TRUE);
main_window_set_status(context->application->main_window,
"Import groupé annulé avant toute copie.");
application_evidence_batch_context_free(context);
}
}
/** @brief Intègre les propositions confirmées puis recharge le graphe. */
static void application_on_osint_integration_confirmed(
GPtrArray *selected_proposals,
@ -351,6 +390,46 @@ static void application_evidence_import_context_free(
);
}
/** @brief Libère l'état possédé d'un import groupé. */
static void application_evidence_batch_context_free(
ApplicationEvidenceBatchContext *context
)
{
if (context == NULL) return;
g_clear_pointer(&context->file_paths, g_ptr_array_unref);
g_clear_pointer(&context->evidence_types, g_ptr_array_unref);
g_clear_pointer(&context->failures, g_ptr_array_unref);
g_free(context);
}
/** @brief Affiche le bilan final puis libère l'import groupé. */
static void application_finish_evidence_batch(
ApplicationEvidenceBatchContext *context
)
{
GString *details = NULL;
char *message = NULL;
if (context == NULL) return;
details = g_string_new(NULL);
for (guint index = 0U; index < context->failures->len; index++)
g_string_append_printf(details, "- %s\n",
(const char *) g_ptr_array_index(context->failures, index));
message = g_strdup_printf(
"%u preuve(s) importée(s), %u ignorée(s), %u échec(s).",
context->success_count, context->skipped_count, context->failures->len);
application_message_dialog_present_details(
main_window_get_window(context->application->main_window),
context->failures->len == 0U
? APPLICATION_MESSAGE_DIALOG_INFORMATION
: APPLICATION_MESSAGE_DIALOG_WARNING,
"Import groupé terminé", message,
details->len > 0U ? details->str : "Tous les fichiers ont été importés.");
main_window_set_import_evidence_enabled(
context->application->main_window, TRUE);
g_free(message); g_string_free(details, TRUE);
application_evidence_batch_context_free(context);
}
/**
* @brief Libère le contexte d'une vérification d'intégrité.
*/
@ -2740,6 +2819,8 @@ static void application_on_evidence_import_completed(
gboolean evidence_refreshed =
FALSE;
ApplicationEvidenceBatchContext *batch_context = NULL;
if (task == NULL ||
context == NULL)
{
@ -2749,16 +2830,17 @@ static void application_on_evidence_import_completed(
application =
context->application;
batch_context = context->batch_context;
if (application == NULL ||
application->main_window == NULL)
{
return;
}
main_window_set_import_evidence_enabled(
application->main_window,
application->session != NULL
);
if (batch_context == NULL)
main_window_set_import_evidence_enabled(
application->main_window, application->session != NULL);
state =
background_task_get_state(
@ -2775,6 +2857,14 @@ static void application_on_evidence_import_completed(
if (evidence_record == NULL)
{
if (batch_context != NULL)
{
g_ptr_array_add(batch_context->failures, g_strdup(
"Tâche terminée sans preuve exploitable."));
batch_context->current_index++;
application_present_next_batch_metadata(batch_context);
return;
}
application_present_error(
application,
"Import incomplet",
@ -2908,12 +2998,26 @@ static void application_on_evidence_import_completed(
status_message
);
if (batch_context != NULL)
{
batch_context->success_count++;
batch_context->current_index++;
application_present_next_batch_metadata(batch_context);
}
return;
}
if (state ==
BACKGROUND_TASK_STATE_CANCELLED)
{
if (batch_context != NULL)
{
batch_context->skipped_count++;
batch_context->current_index++;
application_present_next_batch_metadata(batch_context);
return;
}
if (application_session_matches_root(
application,
context->investigation_root_path
@ -2940,13 +3044,23 @@ static void application_on_evidence_import_completed(
: "erreur inconnue"
);
application_present_error(
application,
"Import impossible",
error != NULL
? error->message
: "La preuve na pas pu être importée."
);
if (batch_context != NULL)
{
const char *path = batch_context->current_index <
batch_context->file_paths->len
? g_ptr_array_index(batch_context->file_paths,
batch_context->current_index) : "fichier inconnu";
char *failure = g_strdup_printf("%s : %s", path,
error != NULL ? error->message : "échec de l'import");
g_ptr_array_add(batch_context->failures, failure);
batch_context->current_index++;
application_present_next_batch_metadata(batch_context);
}
else
application_present_error(
application, "Import impossible",
error != NULL ? error->message
: "La preuve na pas pu être importée.");
g_clear_error(
&error
@ -3033,7 +3147,8 @@ static void application_start_evidence_import(
const char *type_identifier,
const char *collected_at,
const char *source,
const char *description
const char *description,
ApplicationEvidenceBatchContext *batch_context
)
{
const InvestigationProject *project = NULL;
@ -3216,6 +3331,8 @@ static void application_start_evidence_import(
context->application =
application;
context->batch_context = batch_context;
context->investigation_root_path =
g_strdup(
root_path
@ -3426,7 +3543,8 @@ static void application_on_evidence_metadata_completed(
),
evidence_import_dialog_result_get_description(
result
)
),
NULL
);
evidence_import_dialog_result_free(
@ -3673,6 +3791,127 @@ static void application_on_evidence_file_selected(
);
}
/** @brief Traite les métadonnées du fichier courant d'un import groupé. */
static void application_on_batch_metadata_completed(
EvidenceImportDialogResult *result, gpointer user_data
)
{
ApplicationEvidenceBatchContext *context = user_data;
const char *file_path = NULL;
if (context == NULL) { evidence_import_dialog_result_free(result); return; }
if (result == NULL)
{
context->skipped_count++;
context->current_index++;
application_present_next_batch_metadata(context);
return;
}
file_path = g_ptr_array_index(context->file_paths, context->current_index);
application_start_evidence_import(
context->application, file_path,
evidence_import_dialog_result_get_type_identifier(result),
evidence_import_dialog_result_get_collected_at(result),
evidence_import_dialog_result_get_source(result),
evidence_import_dialog_result_get_description(result), context);
evidence_import_dialog_result_free(result);
}
/** @brief Présente les métadonnées du prochain fichier, ou le bilan final. */
static void application_present_next_batch_metadata(
ApplicationEvidenceBatchContext *context
)
{
GError *error = NULL;
const char *file_path = NULL;
char *status = NULL;
if (context == NULL) return;
if (context->current_index >= context->file_paths->len)
{
application_finish_evidence_batch(context);
return;
}
file_path = g_ptr_array_index(context->file_paths, context->current_index);
status = g_strdup_printf("Préparation %u/%u : %s",
context->current_index + 1U, context->file_paths->len, file_path);
main_window_set_status(context->application->main_window, status);
g_free(status);
if (!evidence_import_dialog_present(
main_window_get_window(context->application->main_window),
file_path, context->evidence_types,
application_on_batch_metadata_completed, context, &error))
{
char *failure = g_strdup_printf("%s : %s", file_path,
error != NULL ? error->message : "dialogue indisponible");
g_ptr_array_add(context->failures, failure);
g_clear_error(&error);
context->current_index++;
application_present_next_batch_metadata(context);
}
}
/** @brief Prépare un import groupé depuis les chemins sélectionnés. */
static void application_on_evidence_files_selected(
const GPtrArray *paths, const GError *error, gpointer user_data
)
{
Application *application = user_data;
ApplicationEvidenceBatchContext *context = NULL;
EvidenceTypeDao *dao = NULL;
GPtrArray *types = NULL;
GError *local_error = NULL;
if (application == NULL || application->main_window == NULL) return;
if (error != NULL)
{
application_present_error(application, "Sélection impossible", error->message);
return;
}
if (paths == NULL || paths->len == 0U)
{
main_window_set_status(application->main_window, "Import de preuve annulé.");
return;
}
if (paths->len == 1U)
{
application_on_evidence_file_selected(
g_ptr_array_index((GPtrArray *) paths, 0U), NULL, application);
return;
}
dao = evidence_type_dao_new(
investigation_session_get_database(application->session), &local_error);
if (dao != NULL) types = evidence_type_dao_list_all(dao, &local_error);
evidence_type_dao_free(dao);
if (types == NULL || types->len == 0U)
{
application_present_error(application, "Types de preuve indisponibles",
local_error != NULL ? local_error->message
: "Aucun type de preuve n'est disponible.");
g_clear_error(&local_error);
g_clear_pointer(&types, g_ptr_array_unref);
return;
}
context = g_new0(ApplicationEvidenceBatchContext, 1);
context->application = application;
context->file_paths = g_ptr_array_new_with_free_func(g_free);
context->evidence_types = types;
context->failures = g_ptr_array_new_with_free_func(g_free);
for (guint index = 0U; index < paths->len; index++)
g_ptr_array_add(context->file_paths,
g_strdup(g_ptr_array_index((GPtrArray *) paths, index)));
main_window_set_import_evidence_enabled(application->main_window, FALSE);
{
char *message = g_strdup_printf(
"%u fichiers ont été sélectionnés. Chaque fichier sera révisé "
"puis importé séparément avec sa propre empreinte SHA-256.",
paths->len);
application_message_dialog_present_confirmation(
main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION,
"Préparer l'import groupé", message, "Continuer",
application_on_batch_selection_confirmed, context);
g_free(message);
}
}
/**
* @brief Traite la demande d'import d'une preuve.
*
@ -3702,11 +3941,11 @@ static void application_on_import_evidence_requested(
return;
}
file_dialog_select_file(
file_dialog_select_files(
main_window_get_window(
application->main_window
),
application_on_evidence_file_selected,
application_on_evidence_files_selected,
application
);
}

View file

@ -16,6 +16,13 @@ typedef struct
gpointer user_data;
} FileDialogContext;
/** @brief Contexte conservé pendant une sélection multiple. */
typedef struct
{
FileDialogMultipleCallback callback;
gpointer user_data;
} FileDialogMultipleContext;
/**
* @brief Libère le contexte du dialogue.
*/
@ -234,3 +241,58 @@ void file_dialog_select_file(
dialog
);
}
/** @brief Traite le résultat du sélecteur multiple. */
static void file_dialog_on_files_selected(
GObject *source_object, GAsyncResult *result, gpointer user_data
)
{
FileDialogMultipleContext *context = user_data;
GListModel *files = NULL;
GPtrArray *paths = NULL;
GError *error = NULL;
files = gtk_file_dialog_open_multiple_finish(
GTK_FILE_DIALOG(source_object), result, &error);
if (files == NULL)
{
if (error != NULL && g_error_matches(
error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
context->callback(NULL, NULL, context->user_data);
else context->callback(NULL, error, context->user_data);
g_clear_error(&error); g_free(context); return;
}
paths = g_ptr_array_new_with_free_func(g_free);
for (guint index = 0U; index < g_list_model_get_n_items(files); index++)
{
GFile *file = g_list_model_get_item(files, index);
char *path = g_file_get_path(file);
if (path == NULL)
{
g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Un fichier sélectionné ne possède pas de chemin local.");
g_object_unref(file); break;
}
g_ptr_array_add(paths, path); g_object_unref(file);
}
if (error != NULL) context->callback(NULL, error, context->user_data);
else context->callback(paths, NULL, context->user_data);
g_clear_error(&error); g_ptr_array_unref(paths); g_object_unref(files);
g_free(context);
}
void file_dialog_select_files(
GtkWindow *parent, FileDialogMultipleCallback callback, gpointer user_data
)
{
GtkFileDialog *dialog = NULL;
FileDialogMultipleContext *context = NULL;
if (callback == NULL) return;
context = g_new0(FileDialogMultipleContext, 1);
context->callback = callback; context->user_data = user_data;
dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog, "Sélectionner des fichiers de preuve");
gtk_file_dialog_set_modal(dialog, TRUE);
gtk_file_dialog_open_multiple(dialog, parent, NULL,
file_dialog_on_files_selected, context);
g_object_unref(dialog);
}