feat(osint): display execution history

This commit is contained in:
grayTerminal-sh 2026-07-22 12:54:24 +02:00
parent 1cbbc6acb1
commit bb8295af85
9 changed files with 473 additions and 13 deletions

View file

@ -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 :

View file

@ -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

View file

@ -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,

View file

@ -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 <gtk/gtk.h>
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

View file

@ -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(

View file

@ -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,

View file

@ -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);
}

View file

@ -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;

View file

@ -5,6 +5,7 @@
#include "dao/osint_execution_dao.h"
#include "database/database.h"
#include "database/statement.h"
#include <glib.h>
#include <glib/gstdio.h>
@ -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);