From bb8295af85c8b48e165a544cc3080064c987e6db Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 12:54:24 +0200 Subject: [PATCH] feat(osint): display execution history --- README.md | 4 +- docs/database/DATABASE_ARCHITECTURE.md | 5 + include/dao/osint_execution_dao.h | 19 ++ .../views/osint_execution_history_dialog.h | 32 ++++ src/core/application.c | 68 +++++++ src/dao/osint_execution_dao.c | 123 +++++++++++-- src/views/osint_execution_history_dialog.c | 173 ++++++++++++++++++ src/widgets/workspace.c | 29 ++- tests/test_osint_execution_dao.c | 33 ++++ 9 files changed, 473 insertions(+), 13 deletions(-) create mode 100644 include/views/osint_execution_history_dialog.h create mode 100644 src/views/osint_execution_history_dialog.c diff --git a/README.md b/README.md index fcd51a1..e15ba8d 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,9 @@ Le socle actuel comprend notamment : - création transactionnelle des relations DNS `resolves_to`, `aliases_to` et `uses_name_server` depuis l'entité interrogée ; - provenance OSINT SQLite V3 conservant les arguments, sorties brutes, - empreinte SHA-256 et liaisons vers les entités et relations intégrées. + empreinte SHA-256 et liaisons vers les entités et relations intégrées ; +- historique OSINT contextuel en lecture seule avec détail des exécutions, + sorties standard et d'erreur, et objets créés ou réutilisés. Les outils actuellement présents dans le catalogue initial sont : diff --git a/docs/database/DATABASE_ARCHITECTURE.md b/docs/database/DATABASE_ARCHITECTURE.md index d3d1d9b..85e7d3c 100644 --- a/docs/database/DATABASE_ARCHITECTURE.md +++ b/docs/database/DATABASE_ARCHITECTURE.md @@ -1125,6 +1125,11 @@ transaction que l'intégration DNS. Les descriptions métier restent présentes pour la lisibilité, mais les entités DNS demeurent des résultats OSINT à vérifier et non des faits établis. +Le menu contextuel du workspace permet de consulter cet historique pour +l'entité ou la relation sélectionnée. La vue est strictement en lecture seule +et expose les métadonnées, les sorties brutes rendues en UTF-8, l'empreinte et +les objets liés avec leur disposition `created` ou `reused`. + --- # 6. Tables de liaison diff --git a/include/dao/osint_execution_dao.h b/include/dao/osint_execution_dao.h index 096fcdf..cc203ea 100644 --- a/include/dao/osint_execution_dao.h +++ b/include/dao/osint_execution_dao.h @@ -26,6 +26,25 @@ gboolean osint_execution_dao_insert( OsintExecutionRecord *osint_execution_dao_find_by_identifier( OsintExecutionDao *dao, const char *identifier, GError **error ); +/** + * @brief Liste les exécutions d'une sélection, de la plus récente à l'ancienne. + * + * @return Tableau possédé de OsintExecutionRecord, ou NULL en cas d'erreur. + */ +GPtrArray *osint_execution_dao_list_by_selection( + OsintExecutionDao *dao, const char *selection_kind, + const char *selection_identifier, GError **error +); +/** + * @brief Liste les objets liés à une exécution sous forme de libellés possédés. + * + * Chaque libellé indique la nature, l'UUID et la disposition created/reused. + * + * @return Tableau possédé de chaînes, ou NULL en cas d'erreur. + */ +GPtrArray *osint_execution_dao_list_linked_objects( + OsintExecutionDao *dao, const char *execution_identifier, GError **error +); /** @brief Lie une entité créée ou réutilisée à une exécution. */ gboolean osint_execution_dao_link_entity( OsintExecutionDao *dao, const char *execution_identifier, diff --git a/include/views/osint_execution_history_dialog.h b/include/views/osint_execution_history_dialog.h new file mode 100644 index 0000000..44d23b9 --- /dev/null +++ b/include/views/osint_execution_history_dialog.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * @file osint_execution_history_dialog.h + * @brief Consultation en lecture seule de l'historique OSINT. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_OSINT_EXECUTION_HISTORY_DIALOG_H +#define LABFY_INVESTIGATION_OSINT_EXECUTION_HISTORY_DIALOG_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Affiche les exécutions et leurs objets liés dans une fenêtre modale. + * + * La fonction conserve une référence sur les deux conteneurs jusqu'à la + * fermeture. records contient des OsintExecutionRecord. linked_objects associe + * chaque UUID d'exécution à un GPtrArray de chaînes. + * + * @param parent_window Fenêtre parente, ou NULL. + * @param records Historique non vide, de la plus récente à l'ancienne. + * @param linked_objects Table des objets liés par UUID d'exécution. + */ +void osint_execution_history_dialog_present( + GtkWindow *parent_window, + GPtrArray *records, + GHashTable *linked_objects +); + +G_END_DECLS + +#endif diff --git a/src/core/application.c b/src/core/application.c index e52e3cc..e9e7f37 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -34,6 +34,7 @@ #include "views/evidence_import_dialog.h" #include "views/create_relation_dialog.h" #include "views/osint_dns_review_dialog.h" +#include "views/osint_execution_history_dialog.h" #include "dao/evidence_dao.h" #include "dao/entity_dao.h" #include "dao/osint_execution_dao.h" @@ -1476,6 +1477,61 @@ static void application_start_dns_lookup( tool_task_free(tool_task); } +/** @brief Charge et présente l'historique OSINT d'une sélection. */ +static void application_present_osint_history( + Application *application, + const char *selection_kind, + const char *selection_identifier +) +{ + Database *database = NULL; + OsintExecutionDao *dao = NULL; + GPtrArray *records = NULL; + GHashTable *linked_objects = NULL; + GError *error = NULL; + if (application == NULL || application->session == NULL || + application->main_window == NULL) return; + database = investigation_session_get_database(application->session); + dao = osint_execution_dao_new(database, &error); + if (dao != NULL) records = osint_execution_dao_list_by_selection( + dao, selection_kind, selection_identifier, &error); + linked_objects = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref); + for (guint index = 0U; records != NULL && index < records->len; index++) + { + const OsintExecutionRecord *record = g_ptr_array_index(records, index); + GPtrArray *objects = osint_execution_dao_list_linked_objects( + dao, osint_execution_record_get_identifier(record), &error); + if (objects == NULL) break; + g_hash_table_insert(linked_objects, + g_strdup(osint_execution_record_get_identifier(record)), objects); + } + osint_execution_dao_free(dao); + if (error != NULL) + { + application_present_error(application, "Historique OSINT indisponible", + error->message); + g_clear_error(&error); + } + else if (records == NULL || records->len == 0U) + { + application_message_dialog_present( + main_window_get_window(application->main_window), + APPLICATION_MESSAGE_DIALOG_INFORMATION, + "Historique OSINT", + "Aucune exécution OSINT n'est enregistrée pour cette sélection." + ); + } + else + { + osint_execution_history_dialog_present( + main_window_get_window(application->main_window), + records, linked_objects); + } + g_clear_pointer(&records, g_ptr_array_unref); + g_hash_table_unref(linked_objects); +} + /** * @brief Traite une action OSINT relayée par la fenêtre principale. */ @@ -1488,6 +1544,18 @@ static void application_on_osint_action_requested( { Application *application = user_data; + if (g_strcmp0(action_identifier, "history-entity") == 0 || + g_strcmp0(action_identifier, "history-relation") == 0) + { + application_present_osint_history( + application, + g_strcmp0(action_identifier, "history-relation") == 0 + ? "relation" : "entity", + target_identifier + ); + return; + } + if (g_strcmp0(action_identifier, "dns-preview") == 0) { application_start_dns_lookup( diff --git a/src/dao/osint_execution_dao.c b/src/dao/osint_execution_dao.c index 1e29d08..740dc98 100644 --- a/src/dao/osint_execution_dao.c +++ b/src/dao/osint_execution_dao.c @@ -20,6 +20,21 @@ static const char *const find_sql = "selection_kind,target_value,arguments,started_at,finished_at,exit_code," "final_state,stdout_raw,stderr_raw,output_sha256 FROM osint_executions " "WHERE id=?;"; +static const char *const list_by_selection_sql = + "SELECT id,tool_identifier,tool_version,action_identifier,selection_id," + "selection_kind,target_value,arguments,started_at,finished_at,exit_code," + "final_state,stdout_raw,stderr_raw,output_sha256 FROM osint_executions " + "WHERE (selection_kind=? AND selection_id=?) " + "OR (?='entity' AND id IN (SELECT execution_id " + "FROM osint_execution_entities WHERE entity_id=?)) " + "OR (?='relation' AND id IN (SELECT execution_id " + "FROM osint_execution_relations WHERE relation_id=?)) " + "ORDER BY finished_at DESC,id DESC;"; +static const char *const list_linked_objects_sql = + "SELECT 'Entité',entity_id,disposition FROM osint_execution_entities " + "WHERE execution_id=? UNION ALL " + "SELECT 'Relation',relation_id,disposition FROM osint_execution_relations " + "WHERE execution_id=? ORDER BY 1,2;"; static void osint_execution_dao_set_error( OsintExecutionDao *dao, GError **error, const char *context @@ -101,22 +116,16 @@ cleanup: return success; } -OsintExecutionRecord *osint_execution_dao_find_by_identifier( - OsintExecutionDao *dao, const char *identifier, GError **error +static OsintExecutionRecord *osint_execution_dao_read_record( + DatabaseStatement *statement, GError **error ) { - DatabaseStatement *statement = NULL; OsintExecutionRecord *record = NULL; char *values[12] = {0}; GBytes *stdout_raw = NULL; GBytes *stderr_raw = NULL; int64_t exit_code = 0; bool exit_is_null = true; - if (dao == NULL || identifier == NULL) return NULL; - statement = database_statement_prepare(dao->database, find_sql); - if (statement == NULL || !database_statement_bind_text(statement, 1, identifier) || - database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW) - goto cleanup; if (!database_statement_column_text(statement, 0, &values[0]) || !database_statement_column_text(statement, 1, &values[1]) || !database_statement_column_text(statement, 2, &values[2]) || @@ -138,16 +147,108 @@ OsintExecutionRecord *osint_execution_dao_find_by_identifier( values[6], values[7], values[8], values[9], !exit_is_null, (gint) exit_code, values[10], stdout_raw, stderr_raw, values[11], error); cleanup: - if (record == NULL && error != NULL && *error == NULL) - osint_execution_dao_set_error(dao, error, - "Impossible de lire l'exécution OSINT"); for (guint index = 0; index < G_N_ELEMENTS(values); index++) g_free(values[index]); g_clear_pointer(&stdout_raw, g_bytes_unref); g_clear_pointer(&stderr_raw, g_bytes_unref); + return record; +} + +OsintExecutionRecord *osint_execution_dao_find_by_identifier( + OsintExecutionDao *dao, const char *identifier, GError **error +) +{ + DatabaseStatement *statement = NULL; + OsintExecutionRecord *record = NULL; + if (dao == NULL || identifier == NULL) return NULL; + statement = database_statement_prepare(dao->database, find_sql); + if (statement != NULL && database_statement_bind_text(statement, 1, identifier) && + database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW) + record = osint_execution_dao_read_record(statement, error); + if (record == NULL && error != NULL && *error == NULL) + osint_execution_dao_set_error(dao, error, + "Impossible de lire l'exécution OSINT"); database_statement_finalize(statement); return record; } +GPtrArray *osint_execution_dao_list_by_selection( + OsintExecutionDao *dao, const char *selection_kind, + const char *selection_identifier, GError **error +) +{ + DatabaseStatement *statement = NULL; + GPtrArray *records = NULL; + DatabaseStatementStepResult step = DATABASE_STATEMENT_STEP_ERROR; + if (dao == NULL || selection_identifier == NULL || + (g_strcmp0(selection_kind, "entity") != 0 && + g_strcmp0(selection_kind, "relation") != 0)) return NULL; + statement = database_statement_prepare(dao->database, list_by_selection_sql); + records = g_ptr_array_new_with_free_func( + (GDestroyNotify) osint_execution_record_free); + if (statement == NULL || records == NULL || + !database_statement_bind_text(statement, 1, selection_kind) || + !database_statement_bind_text(statement, 2, selection_identifier) || + !database_statement_bind_text(statement, 3, selection_kind) || + !database_statement_bind_text(statement, 4, selection_identifier) || + !database_statement_bind_text(statement, 5, selection_kind) || + !database_statement_bind_text(statement, 6, selection_identifier)) + goto failure; + while ((step = database_statement_step(statement)) == DATABASE_STATEMENT_STEP_ROW) + { + OsintExecutionRecord *record = osint_execution_dao_read_record(statement, error); + if (record == NULL) goto failure; + g_ptr_array_add(records, record); + } + if (step != DATABASE_STATEMENT_STEP_DONE) goto failure; + database_statement_finalize(statement); + return records; +failure: + osint_execution_dao_set_error(dao, error, + "Impossible de lister l'historique OSINT"); + database_statement_finalize(statement); + g_clear_pointer(&records, g_ptr_array_unref); + return NULL; +} + +GPtrArray *osint_execution_dao_list_linked_objects( + OsintExecutionDao *dao, const char *execution_identifier, GError **error +) +{ + DatabaseStatement *statement = NULL; + GPtrArray *objects = NULL; + DatabaseStatementStepResult step = DATABASE_STATEMENT_STEP_ERROR; + if (dao == NULL || execution_identifier == NULL) return NULL; + statement = database_statement_prepare(dao->database, list_linked_objects_sql); + objects = g_ptr_array_new_with_free_func(g_free); + if (statement == NULL || objects == NULL || + !database_statement_bind_text(statement, 1, execution_identifier) || + !database_statement_bind_text(statement, 2, execution_identifier)) + goto failure; + while ((step = database_statement_step(statement)) == DATABASE_STATEMENT_STEP_ROW) + { + char *kind = NULL; char *identifier = NULL; char *disposition = NULL; + char *description = NULL; + if (!database_statement_column_text(statement, 0, &kind) || + !database_statement_column_text(statement, 1, &identifier) || + !database_statement_column_text(statement, 2, &disposition)) + { g_free(kind); g_free(identifier); g_free(disposition); goto failure; } + description = g_strdup_printf("%s %s — %s", kind, identifier, + g_strcmp0(disposition, "created") == 0 ? "créée" : "réutilisée"); + g_free(kind); g_free(identifier); g_free(disposition); + if (description == NULL) goto failure; + g_ptr_array_add(objects, description); + } + if (step != DATABASE_STATEMENT_STEP_DONE) goto failure; + database_statement_finalize(statement); + return objects; +failure: + osint_execution_dao_set_error(dao, error, + "Impossible de lister les objets liés à l'exécution OSINT"); + database_statement_finalize(statement); + g_clear_pointer(&objects, g_ptr_array_unref); + return NULL; +} + static gboolean osint_execution_dao_link( OsintExecutionDao *dao, const char *table, const char *object_column, const char *execution_identifier, const char *object_identifier, diff --git a/src/views/osint_execution_history_dialog.c b/src/views/osint_execution_history_dialog.c new file mode 100644 index 0000000..472ee23 --- /dev/null +++ b/src/views/osint_execution_history_dialog.c @@ -0,0 +1,173 @@ +/****************************************************************************** + * @file osint_execution_history_dialog.c + * @brief Consultation en lecture seule de l'historique OSINT. + ******************************************************************************/ + +#include "views/osint_execution_history_dialog.h" + +#include "models/osint_execution_record.h" + +typedef struct +{ + GtkWindow *window; + GtkTextBuffer *details_buffer; + GPtrArray *records; + GHashTable *linked_objects; +} OsintExecutionHistoryDialogContext; + +/** @brief Libère les données conservées par la fenêtre. */ +static void osint_execution_history_dialog_context_free(gpointer data) +{ + OsintExecutionHistoryDialogContext *context = data; + if (context == NULL) return; + g_clear_pointer(&context->records, g_ptr_array_unref); + g_clear_pointer(&context->linked_objects, g_hash_table_unref); + g_free(context); +} + +/** @brief Convertit une sortie brute en texte UTF-8 affichable. */ +static char *osint_execution_history_dialog_output_to_text(GBytes *bytes) +{ + gconstpointer data = NULL; + gsize size = 0U; + if (bytes == NULL) return g_strdup(""); + data = g_bytes_get_data(bytes, &size); + return data != NULL && size > 0U + ? g_utf8_make_valid(data, (gssize) size) : g_strdup(""); +} + +/** @brief Construit le détail textuel complet d'une exécution. */ +static char *osint_execution_history_dialog_build_details( + const OsintExecutionRecord *record, GPtrArray *linked_objects +) +{ + GBytes *stdout_raw = osint_execution_record_ref_stdout(record); + GBytes *stderr_raw = osint_execution_record_ref_stderr(record); + char *stdout_text = osint_execution_history_dialog_output_to_text(stdout_raw); + char *stderr_text = osint_execution_history_dialog_output_to_text(stderr_raw); + GString *details = g_string_new(NULL); + g_string_append_printf(details, + "Exécution : %s\nOutil : %s%s%s\nAction : %s\nÉtat : %s\n" + "Sélection : %s %s\nCible : %s\nDébut : %s\nFin : %s\n" + "Code de sortie : ", + osint_execution_record_get_identifier(record), + osint_execution_record_get_tool_identifier(record), + osint_execution_record_get_tool_version(record) != NULL ? " — " : "", + osint_execution_record_get_tool_version(record) != NULL + ? osint_execution_record_get_tool_version(record) : "", + osint_execution_record_get_action_identifier(record), + osint_execution_record_get_final_state(record), + osint_execution_record_get_selection_kind(record), + osint_execution_record_get_selection_identifier(record), + osint_execution_record_get_target_value(record), + osint_execution_record_get_started_at(record), + osint_execution_record_get_finished_at(record)); + if (osint_execution_record_has_exit_code(record)) + g_string_append_printf(details, "%d", osint_execution_record_get_exit_code(record)); + else g_string_append(details, "indisponible"); + g_string_append_printf(details, + "\nArguments : %s\nSHA-256 : %s\n\nObjets liés :\n", + osint_execution_record_get_arguments(record), + osint_execution_record_get_output_sha256(record)); + if (linked_objects == NULL || linked_objects->len == 0U) + g_string_append(details, "Aucun objet intégré.\n"); + else for (guint index = 0U; index < linked_objects->len; index++) + g_string_append_printf(details, "- %s\n", + (const char *) g_ptr_array_index(linked_objects, index)); + g_string_append_printf(details, "\nSortie standard :\n%s\n\nSortie d'erreur :\n%s", + stdout_text[0] != '\0' ? stdout_text : "(vide)", + stderr_text[0] != '\0' ? stderr_text : "(vide)"); + g_bytes_unref(stdout_raw); g_bytes_unref(stderr_raw); + g_free(stdout_text); g_free(stderr_text); + return g_string_free(details, FALSE); +} + +/** @brief Affiche le détail correspondant au bouton d'historique activé. */ +static void osint_execution_history_dialog_on_record_clicked( + GtkButton *button, gpointer user_data +) +{ + OsintExecutionHistoryDialogContext *context = user_data; + guint encoded_index = GPOINTER_TO_UINT(g_object_get_data( + G_OBJECT(button), "osint-execution-index")); + OsintExecutionRecord *record = NULL; + GPtrArray *linked_objects = NULL; + char *details = NULL; + if (context == NULL || encoded_index == 0U || + encoded_index > context->records->len) return; + record = g_ptr_array_index(context->records, encoded_index - 1U); + linked_objects = g_hash_table_lookup(context->linked_objects, + osint_execution_record_get_identifier(record)); + details = osint_execution_history_dialog_build_details(record, linked_objects); + gtk_text_buffer_set_text(context->details_buffer, details, -1); + g_free(details); +} + +void osint_execution_history_dialog_present( + GtkWindow *parent_window, GPtrArray *records, GHashTable *linked_objects +) +{ + OsintExecutionHistoryDialogContext *context = NULL; + GtkWidget *main_box = NULL; GtkWidget *content_box = NULL; + GtkWidget *history_scroll = NULL; GtkWidget *history_box = NULL; + GtkWidget *details_scroll = NULL; GtkWidget *details_view = NULL; + GtkWidget *close_button = NULL; + if (records == NULL || records->len == 0U || linked_objects == NULL) return; + context = g_new0(OsintExecutionHistoryDialogContext, 1); + context->window = GTK_WINDOW(gtk_window_new()); + context->records = g_ptr_array_ref(records); + context->linked_objects = g_hash_table_ref(linked_objects); + gtk_window_set_title(context->window, "Historique OSINT"); + gtk_window_set_default_size(context->window, 980, 650); + gtk_window_set_modal(context->window, TRUE); + gtk_window_set_destroy_with_parent(context->window, TRUE); + if (parent_window != NULL) gtk_window_set_transient_for(context->window, parent_window); + g_object_set_data_full(G_OBJECT(context->window), "osint-history-context", + context, osint_execution_history_dialog_context_free); + main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); + gtk_widget_set_margin_start(main_box, 16); gtk_widget_set_margin_end(main_box, 16); + gtk_widget_set_margin_top(main_box, 16); gtk_widget_set_margin_bottom(main_box, 16); + content_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_widget_set_vexpand(content_box, TRUE); + history_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + history_scroll = gtk_scrolled_window_new(); + gtk_widget_set_size_request(history_scroll, 300, -1); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(history_scroll), history_box); + for (guint index = 0U; index < records->len; index++) + { + OsintExecutionRecord *record = g_ptr_array_index(records, index); + char *label = g_strdup_printf("%s\n%s — %s", + osint_execution_record_get_finished_at(record), + osint_execution_record_get_tool_identifier(record), + osint_execution_record_get_final_state(record)); + GtkWidget *button = gtk_button_new_with_label(label); + gtk_widget_set_halign(button, GTK_ALIGN_FILL); + g_object_set_data(G_OBJECT(button), "osint-execution-index", + GUINT_TO_POINTER(index + 1U)); + g_signal_connect(button, "clicked", + G_CALLBACK(osint_execution_history_dialog_on_record_clicked), context); + gtk_box_append(GTK_BOX(history_box), button); + g_free(label); + } + details_view = gtk_text_view_new(); + context->details_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(details_view)); + gtk_text_view_set_editable(GTK_TEXT_VIEW(details_view), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(details_view), TRUE); + gtk_text_view_set_monospace(GTK_TEXT_VIEW(details_view), TRUE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(details_view), GTK_WRAP_WORD_CHAR); + details_scroll = gtk_scrolled_window_new(); + gtk_widget_set_hexpand(details_scroll, TRUE); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(details_scroll), details_view); + gtk_box_append(GTK_BOX(content_box), history_scroll); + gtk_box_append(GTK_BOX(content_box), details_scroll); + close_button = gtk_button_new_with_label("Fermer"); + gtk_widget_set_halign(close_button, GTK_ALIGN_END); + g_signal_connect_swapped(close_button, "clicked", + G_CALLBACK(gtk_window_destroy), context->window); + gtk_box_append(GTK_BOX(main_box), content_box); + gtk_box_append(GTK_BOX(main_box), close_button); + gtk_window_set_child(context->window, main_box); + osint_execution_history_dialog_on_record_clicked( + GTK_BUTTON(gtk_widget_get_first_child(history_box)), context); + gtk_window_present(context->window); +} diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 5d943d4..a7a96d8 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -183,7 +183,9 @@ static void workspace_on_osint_action_clicked( gtk_label_set_text( GTK_LABEL(workspace->osint_tools_status_label), - "Exécution lancée ; suivez sa progression dans les tâches." + g_str_has_prefix(action_identifier, "history-") + ? "Ouverture de l'historique enregistré." + : "Exécution lancée ; suivez sa progression dans les tâches." ); gtk_popover_popdown( @@ -290,6 +292,31 @@ static void workspace_update_osint_tools_menu( context ); + { + GtkWidget *history_button = gtk_button_new_with_label( + "Historique OSINT" + ); + const char *history_action = + osint_selection_context_get_kind(context) == + OSINT_SELECTION_CONTEXT_KIND_RELATION + ? "history-relation" : "history-entity"; + gtk_widget_set_tooltip_text( + history_button, + "Consulter les exécutions OSINT enregistrées pour cette sélection" + ); + g_object_set_data_full( + G_OBJECT(history_button), "osint-action-identifier", + g_strdup(history_action), g_free + ); + g_signal_connect( + history_button, "clicked", + G_CALLBACK(workspace_on_osint_action_clicked), workspace + ); + gtk_box_append( + GTK_BOX(workspace->osint_tools_actions_box), history_button + ); + } + for (guint action_index = 0; compatible_actions != NULL && action_index < compatible_actions->len; diff --git a/tests/test_osint_execution_dao.c b/tests/test_osint_execution_dao.c index e2f1d27..00b296a 100644 --- a/tests/test_osint_execution_dao.c +++ b/tests/test_osint_execution_dao.c @@ -5,6 +5,7 @@ #include "dao/osint_execution_dao.h" #include "database/database.h" +#include "database/statement.h" #include #include @@ -22,6 +23,9 @@ static void test_insert_and_read_raw_execution(void) GBytes *stdout_raw = g_bytes_new_static(raw_data, sizeof(raw_data)); GBytes *stderr_raw = g_bytes_new_static("", 0U); GBytes *loaded_stdout = NULL; + GPtrArray *history = NULL; + GPtrArray *linked_objects = NULL; + DatabaseStatement *statement = NULL; g_assert_true(database_initialize(path, "OSINT", directory)); database = database_open(path); @@ -37,6 +41,18 @@ static void test_insert_and_read_raw_execution(void) ); g_assert_nonnull(record); g_assert_true(osint_execution_dao_insert(dao, record, &error)); + statement = database_statement_prepare(database, + "INSERT INTO entites(id,type_id,valeur,confiance,created_at,updated_at,status) " + "VALUES('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'," + "(SELECT id FROM types_entite LIMIT 1),'linked.example',50," + "'2026-01-01T00:00:00Z','2026-01-01T00:00:00Z','active');"); + g_assert_nonnull(statement); + g_assert_cmpint(database_statement_step(statement), ==, + DATABASE_STATEMENT_STEP_DONE); + database_statement_finalize(statement); + g_assert_true(osint_execution_dao_link_entity( + dao, "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", + "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", "created", &error)); loaded = osint_execution_dao_find_by_identifier( dao, "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", &error ); @@ -47,7 +63,24 @@ static void test_insert_and_read_raw_execution(void) g_assert_true(osint_execution_record_has_exit_code(loaded)); loaded_stdout = osint_execution_record_ref_stdout(loaded); g_assert_true(g_bytes_equal(stdout_raw, loaded_stdout)); + history = osint_execution_dao_list_by_selection( + dao, "entity", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", &error); + g_assert_nonnull(history); + g_assert_cmpuint(history->len, ==, 1U); + g_ptr_array_unref(history); + history = osint_execution_dao_list_by_selection( + dao, "entity", "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", &error); + g_assert_nonnull(history); + g_assert_cmpuint(history->len, ==, 1U); + linked_objects = osint_execution_dao_list_linked_objects( + dao, "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", &error); + g_assert_nonnull(linked_objects); + g_assert_cmpuint(linked_objects->len, ==, 1U); + g_assert_nonnull(g_strstr_len( + g_ptr_array_index(linked_objects, 0U), -1, "créée")); + g_ptr_array_unref(linked_objects); + g_ptr_array_unref(history); g_bytes_unref(loaded_stdout); osint_execution_record_free(loaded); osint_execution_record_free(record);