From 1cbbc6acb11a330b6b9a10f9cfb64183ef4bfa8f Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 12:44:17 +0200 Subject: [PATCH] feat(osint): persist structured execution provenance --- Makefile | 21 ++- README.md | 4 +- database/schema_current.sql | 2 +- database/schema_v3.sql | 90 ++++++++++++ docs/database/DATABASE_ARCHITECTURE.md | 28 ++-- include/core/osint_dns_integration.h | 2 + include/dao/osint_execution_dao.h | 41 ++++++ include/database/schema.h | 14 ++ include/database/statement.h | 36 +++++ include/models/osint_execution_record.h | 74 ++++++++++ src/core/application.c | 160 ++++++++++++++++++-- src/core/osint_dns_integration.c | 47 +++++- src/dao/osint_execution_dao.c | 186 ++++++++++++++++++++++++ src/database/database.c | 41 +++++- src/database/schema.c | 11 ++ src/database/statement.c | 69 +++++++++ src/models/osint_execution_record.c | 130 +++++++++++++++++ tests/test_database.c | 13 +- tests/test_osint_dns_integration.c | 55 ++++++- tests/test_osint_execution_dao.c | 84 +++++++++++ tests/test_statement.c | 27 ++++ 21 files changed, 1089 insertions(+), 46 deletions(-) create mode 100644 database/schema_v3.sql create mode 100644 include/dao/osint_execution_dao.h create mode 100644 include/models/osint_execution_record.h create mode 100644 src/dao/osint_execution_dao.c create mode 100644 src/models/osint_execution_record.c create mode 100644 tests/test_osint_execution_dao.c diff --git a/Makefile b/Makefile index f20d3a4..68f7f6f 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,7 @@ TEST_OSINT_ACTION_CATALOG := tests/test_osint_action_catalog TEST_OSINT_DNS_QUERY := tests/test_osint_dns_query TEST_OSINT_DNS_PROPOSAL := tests/test_osint_dns_proposal TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration +TEST_OSINT_EXECUTION_DAO := tests/test_osint_execution_dao all: $(TARGET) @@ -566,8 +567,21 @@ $(TEST_OSINT_DNS_INTEGRATION): \ src/models/osint_dns_query.c \ src/models/entity_record.c \ src/models/relation_record.c \ + src/models/osint_execution_record.c \ src/dao/entity_dao.c \ src/dao/relation_dao.c \ + src/dao/osint_execution_dao.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + +$(TEST_OSINT_EXECUTION_DAO): \ + tests/test_osint_execution_dao.c \ + src/dao/osint_execution_dao.c \ + src/models/osint_execution_record.c \ src/database/database.c \ src/database/schema.c \ src/database/statement.c \ @@ -647,7 +661,8 @@ test: \ $(TEST_OSINT_ACTION_CATALOG) \ $(TEST_OSINT_DNS_QUERY) \ $(TEST_OSINT_DNS_PROPOSAL) \ - $(TEST_OSINT_DNS_INTEGRATION) + $(TEST_OSINT_DNS_INTEGRATION) \ + $(TEST_OSINT_EXECUTION_DAO) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -700,6 +715,7 @@ test: \ @$(TEST_OSINT_DNS_QUERY) @$(TEST_OSINT_DNS_PROPOSAL) @$(TEST_OSINT_DNS_INTEGRATION) + @$(TEST_OSINT_EXECUTION_DAO) @echo "Tous les tests sont valides." %.o: %.c @@ -758,7 +774,8 @@ clean: $(TEST_OSINT_ACTION_CATALOG) \ $(TEST_OSINT_DNS_QUERY) \ $(TEST_OSINT_DNS_PROPOSAL) \ - $(TEST_OSINT_DNS_INTEGRATION) + $(TEST_OSINT_DNS_INTEGRATION) \ + $(TEST_OSINT_EXECUTION_DAO) -include $(DEP) diff --git a/README.md b/README.md index d6844d1..fcd51a1 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,9 @@ Le socle actuel comprend notamment : - sélection explicite et intégration transactionnelle des propositions DNS compatibles, avec normalisation et détection des doublons ; - création transactionnelle des relations DNS `resolves_to`, `aliases_to` et - `uses_name_server` depuis l'entité interrogée. + `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. Les outils actuellement présents dans le catalogue initial sont : diff --git a/database/schema_current.sql b/database/schema_current.sql index f1a1571..ed76ff1 100644 --- a/database/schema_current.sql +++ b/database/schema_current.sql @@ -1,7 +1,7 @@ /****************************************************************************** * Labfy Investigation * - * Extensions idempotentes du schéma SQLite courant V2 + * Extensions idempotentes du schéma SQLite courant V3 ******************************************************************************/ /* diff --git a/database/schema_v3.sql b/database/schema_v3.sql new file mode 100644 index 0000000..f6a4f91 --- /dev/null +++ b/database/schema_v3.sql @@ -0,0 +1,90 @@ +/****************************************************************************** + * Labfy Investigation + * + * Migration du schéma SQLite V2 vers V3 : provenance OSINT structurée + ******************************************************************************/ + +CREATE TABLE osint_executions +( + id TEXT PRIMARY KEY, + tool_identifier TEXT NOT NULL, + tool_version TEXT, + action_identifier TEXT NOT NULL, + selection_id TEXT NOT NULL, + selection_kind TEXT NOT NULL, + target_value TEXT NOT NULL, + arguments TEXT NOT NULL, + started_at TEXT NOT NULL, + finished_at TEXT NOT NULL, + exit_code INTEGER, + final_state TEXT NOT NULL, + stdout_raw BLOB NOT NULL, + stderr_raw BLOB NOT NULL, + output_sha256 TEXT NOT NULL, + + CHECK (length(id) = 36), + CHECK (length(trim(tool_identifier)) > 0), + CHECK (length(trim(action_identifier)) > 0), + CHECK (length(selection_id) = 36), + CHECK (selection_kind IN ('entity', 'relation')), + CHECK (length(trim(target_value)) > 0), + CHECK (length(started_at) = 20), + CHECK (length(finished_at) = 20), + CHECK (final_state IN ('completed', 'failed', 'cancelled')), + CHECK (length(output_sha256) = 64), + CHECK (output_sha256 = lower(output_sha256)) +); + +CREATE INDEX idx_osint_executions_finished_at +ON osint_executions(finished_at); + +CREATE INDEX idx_osint_executions_selection +ON osint_executions(selection_kind, selection_id); + +CREATE TABLE osint_execution_entities +( + execution_id TEXT NOT NULL, + entity_id TEXT NOT NULL, + disposition TEXT NOT NULL, + + PRIMARY KEY (execution_id, entity_id), + + FOREIGN KEY (execution_id) + REFERENCES osint_executions(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (entity_id) + REFERENCES entites(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (disposition IN ('created', 'reused')) +); + +CREATE INDEX idx_osint_execution_entities_entity +ON osint_execution_entities(entity_id); + +CREATE TABLE osint_execution_relations +( + execution_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + disposition TEXT NOT NULL, + + PRIMARY KEY (execution_id, relation_id), + + FOREIGN KEY (execution_id) + REFERENCES osint_executions(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + FOREIGN KEY (relation_id) + REFERENCES relations(id) + ON UPDATE CASCADE + ON DELETE RESTRICT, + + CHECK (disposition IN ('created', 'reused')) +); + +CREATE INDEX idx_osint_execution_relations_relation +ON osint_execution_relations(relation_id); diff --git a/docs/database/DATABASE_ARCHITECTURE.md b/docs/database/DATABASE_ARCHITECTURE.md index dacf3a8..d3d1d9b 100644 --- a/docs/database/DATABASE_ARCHITECTURE.md +++ b/docs/database/DATABASE_ARCHITECTURE.md @@ -1106,28 +1106,24 @@ En revanche, la suppression ou la modification d'un type existant doit être réalisée avec précaution afin de préserver la cohérence des données déjà enregistrées. -### Limite actuelle de la traçabilité OSINT +### Traçabilité OSINT structurée — schéma V3 -L'intégration DNS initiale peut créer des entités `domain_name` et -`ip_address`, ainsi que les relations `resolves_to`, `aliases_to` et -`uses_name_server`. La description de l'entité et la justification de la -relation conservent une indication minimale : outil `dns.dig`, cible -interrogée et type d'enregistrement. - -Cette information ne remplace pas une provenance structurée. Une migration -future devra introduire un objet de résultat OSINT capable de conserver au -minimum : +La table `osint_executions` conserve chaque exécution terminée, y compris +lorsque la révision est annulée. Elle contient notamment : - l'outil et sa version ; - la cible et les arguments de l'exécution ; - la date d'exécution ; -- la sortie brute et son empreinte ; -- les propositions extraites ; -- les entités effectivement créées ou réutilisées. +- les sorties standard et d'erreur sous forme de BLOB ; +- leur empreinte SHA-256. -Jusqu'à cette migration, l'intégration ne doit pas présenter les entités DNS -comme des faits vérifiés et ne doit pas supprimer la sortie brute affichée à -l'utilisateur. +Les tables `osint_execution_entities` et `osint_execution_relations` relient +l'exécution aux objets intégrés. Le champ `disposition` distingue les objets +créés des objets réutilisés. Ces liaisons sont ajoutées dans la même +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. --- diff --git a/include/core/osint_dns_integration.h b/include/core/osint_dns_integration.h index 5485c70..ea669f9 100644 --- a/include/core/osint_dns_integration.h +++ b/include/core/osint_dns_integration.h @@ -40,6 +40,7 @@ GQuark osint_dns_integration_error_quark(void); * * @param database Connexion SQLite active. * @param source_entity_identifier UUID de l'entité interrogée. + * @param execution_identifier UUID de l'exécution OSINT persistée. * @param selected_proposals Propositions explicitement sélectionnées. * @param out_inserted_entity_count Nombre d'entités créées. * @param out_skipped_entity_count Nombre d'entités ignorées ou réutilisées. @@ -52,6 +53,7 @@ GQuark osint_dns_integration_error_quark(void); gboolean osint_dns_integration_apply( Database *database, const char *source_entity_identifier, + const char *execution_identifier, GPtrArray *selected_proposals, guint *out_inserted_entity_count, guint *out_skipped_entity_count, diff --git a/include/dao/osint_execution_dao.h b/include/dao/osint_execution_dao.h new file mode 100644 index 0000000..096fcdf --- /dev/null +++ b/include/dao/osint_execution_dao.h @@ -0,0 +1,41 @@ +/****************************************************************************** + * @file osint_execution_dao.h + * @brief Persistance de la provenance des exécutions OSINT. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_OSINT_EXECUTION_DAO_H +#define LABFY_INVESTIGATION_OSINT_EXECUTION_DAO_H + +#include "database/database.h" +#include "models/osint_execution_record.h" + +G_BEGIN_DECLS + +/** @brief DAO opaque empruntant une connexion Database. */ +typedef struct OsintExecutionDao OsintExecutionDao; + +/** @brief Crée un DAO sur une connexion existante. */ +OsintExecutionDao *osint_execution_dao_new(Database *database, GError **error); +/** @brief Libère le DAO sans fermer la connexion. */ +void osint_execution_dao_free(OsintExecutionDao *dao); +/** @brief Insère une exécution sans remplacer une ligne existante. */ +gboolean osint_execution_dao_insert( + OsintExecutionDao *dao, const OsintExecutionRecord *record, GError **error +); +/** @brief Recherche une exécution par UUID et retourne un modèle possédé. */ +OsintExecutionRecord *osint_execution_dao_find_by_identifier( + OsintExecutionDao *dao, const char *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, + const char *entity_identifier, const char *disposition, GError **error +); +/** @brief Lie une relation créée ou réutilisée à une exécution. */ +gboolean osint_execution_dao_link_relation( + OsintExecutionDao *dao, const char *execution_identifier, + const char *relation_identifier, const char *disposition, GError **error +); + +G_END_DECLS +#endif diff --git a/include/database/schema.h b/include/database/schema.h index ffafd4b..46bab7d 100644 --- a/include/database/schema.h +++ b/include/database/schema.h @@ -54,6 +54,20 @@ bool schema_install_v2( Database *database ); +/** + * @brief Installe la migration de provenance OSINT du schéma V3. + * + * La connexion doit être valide et une transaction doit déjà être active. + * La fonction ne réalise ni COMMIT ni ROLLBACK. + * + * @param database Connexion Database ouverte. + * + * @return true si la migration V3 a été appliquée, sinon false. + */ +bool schema_install_v3( + Database *database +); + /** * @brief Garantit la présence des extensions du schéma courant V2. * diff --git a/include/database/statement.h b/include/database/statement.h index a430a3b..101158c 100644 --- a/include/database/statement.h +++ b/include/database/statement.h @@ -10,6 +10,7 @@ #include #include +#include /** * @brief Résultat de l'exécution d'une requête préparée. @@ -161,6 +162,24 @@ bool database_statement_column_text( char **value ); +/** + * @brief Copie le contenu binaire d'une colonne BLOB. + * + * Si la colonne contient SQL NULL, la fonction réussit et place NULL dans + * value. La référence retournée doit être libérée avec g_bytes_unref(). + * + * @param statement Requête positionnée sur une ligne. + * @param column_index Indice de la colonne. + * @param value Destination du GBytes alloué. + * + * @return true si la colonne a pu être lue, sinon false. + */ +bool database_statement_column_blob( + DatabaseStatement *statement, + int column_index, + GBytes **value +); + /** * @brief Lie une chaîne de caractères à un paramètre SQL. * @@ -178,6 +197,23 @@ bool database_statement_bind_text( const char *value ); +/** + * @brief Lie des octets à un paramètre SQL BLOB. + * + * Les octets sont copiés par SQLite pendant l'appel. + * + * @param statement Requête préparée. + * @param index Indice du paramètre, commençant à 1. + * @param value Octets à lier. + * + * @return true en cas de succès, sinon false. + */ +bool database_statement_bind_blob( + DatabaseStatement *statement, + int index, + GBytes *value +); + /** * @brief Lie un entier signé sur 64 bits à un paramètre SQL. * diff --git a/include/models/osint_execution_record.h b/include/models/osint_execution_record.h new file mode 100644 index 0000000..06c37db --- /dev/null +++ b/include/models/osint_execution_record.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * @file osint_execution_record.h + * @brief Modèle immuable d'une exécution OSINT persistée. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_OSINT_EXECUTION_RECORD_H +#define LABFY_INVESTIGATION_OSINT_EXECUTION_RECORD_H + +#include + +G_BEGIN_DECLS + +/** @brief Exécution OSINT opaque. */ +typedef struct OsintExecutionRecord OsintExecutionRecord; + +/** @brief Crée une exécution OSINT en copiant toutes ses données. */ +OsintExecutionRecord *osint_execution_record_new( + const char *identifier, + const char *tool_identifier, + const char *tool_version, + const char *action_identifier, + const char *selection_identifier, + const char *selection_kind, + const char *target_value, + const char *arguments, + const char *started_at, + const char *finished_at, + gboolean has_exit_code, + gint exit_code, + const char *final_state, + GBytes *stdout_raw, + GBytes *stderr_raw, + const char *output_sha256, + GError **error +); + +/** @brief Libère un modèle, ou accepte NULL. */ +void osint_execution_record_free(OsintExecutionRecord *record); + +/** @brief Retourne l'UUID emprunté. */ +const char *osint_execution_record_get_identifier(const OsintExecutionRecord *record); +/** @brief Retourne l'identifiant d'outil emprunté. */ +const char *osint_execution_record_get_tool_identifier(const OsintExecutionRecord *record); +/** @brief Retourne la version d'outil empruntée, ou NULL. */ +const char *osint_execution_record_get_tool_version(const OsintExecutionRecord *record); +/** @brief Retourne l'identifiant d'action emprunté. */ +const char *osint_execution_record_get_action_identifier(const OsintExecutionRecord *record); +/** @brief Retourne l'UUID de sélection emprunté. */ +const char *osint_execution_record_get_selection_identifier(const OsintExecutionRecord *record); +/** @brief Retourne la nature de sélection empruntée. */ +const char *osint_execution_record_get_selection_kind(const OsintExecutionRecord *record); +/** @brief Retourne la cible empruntée. */ +const char *osint_execution_record_get_target_value(const OsintExecutionRecord *record); +/** @brief Retourne les arguments déterministes empruntés. */ +const char *osint_execution_record_get_arguments(const OsintExecutionRecord *record); +/** @brief Retourne la date UTC de début empruntée. */ +const char *osint_execution_record_get_started_at(const OsintExecutionRecord *record); +/** @brief Retourne la date UTC de fin empruntée. */ +const char *osint_execution_record_get_finished_at(const OsintExecutionRecord *record); +/** @brief Indique si un code de sortie est disponible. */ +gboolean osint_execution_record_has_exit_code(const OsintExecutionRecord *record); +/** @brief Retourne le code de sortie, ou -1 s'il est absent. */ +gint osint_execution_record_get_exit_code(const OsintExecutionRecord *record); +/** @brief Retourne l'état final emprunté. */ +const char *osint_execution_record_get_final_state(const OsintExecutionRecord *record); +/** @brief Retourne une référence sur stdout brut. */ +GBytes *osint_execution_record_ref_stdout(const OsintExecutionRecord *record); +/** @brief Retourne une référence sur stderr brut. */ +GBytes *osint_execution_record_ref_stderr(const OsintExecutionRecord *record); +/** @brief Retourne l'empreinte SHA-256 empruntée. */ +const char *osint_execution_record_get_output_sha256(const OsintExecutionRecord *record); + +G_END_DECLS +#endif diff --git a/src/core/application.c b/src/core/application.c index 366e72c..e52e3cc 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -15,6 +15,7 @@ #include "models/investigation_record.h" #include "models/osint_dns_query.h" #include "models/osint_dns_proposal.h" +#include "models/osint_execution_record.h" #include "core/investigation_tree_builder.h" #include "views/create_investigation_dialog.h" #include "views/folder_dialog.h" @@ -35,6 +36,7 @@ #include "views/osint_dns_review_dialog.h" #include "dao/evidence_dao.h" #include "dao/entity_dao.h" +#include "dao/osint_execution_dao.h" #include "models/evidence_type.h" #include "widgets/evidence_list_model.h" #include "widgets/evidence_category_model.h" @@ -164,6 +166,7 @@ typedef struct Application *application; char *target_identifier; char *target_value; + char *started_at; } ApplicationOsintActionContext; /** @brief Données possédées par l'étape de révision OSINT. */ @@ -171,6 +174,7 @@ typedef struct { Application *application; char *source_entity_identifier; + char *execution_identifier; GPtrArray *proposals; } ApplicationOsintReviewContext; @@ -191,6 +195,7 @@ static void application_osint_review_context_free(gpointer user_data) ApplicationOsintReviewContext *context = user_data; if (context == NULL) return; g_clear_pointer(&context->proposals, g_ptr_array_unref); + g_free(context->execution_identifier); g_free(context->source_entity_identifier); g_free(context); } @@ -227,6 +232,7 @@ static void application_on_osint_integration_confirmed( ? investigation_project_get_database_path(project) : NULL; if (!osint_dns_integration_apply( database, review_context->source_entity_identifier, + review_context->execution_identifier, selected_proposals, &inserted_count, &skipped_count, &inserted_relation_count, &skipped_relation_count, &error )) @@ -244,10 +250,10 @@ static void application_on_osint_integration_confirmed( message = g_strdup_printf( "%u entité(s) créée(s), %u entité(s) réutilisée(s) ou ignorée(s).\n" "%u relation(s) créée(s), %u relation(s) ignorée(s).\n\n" - "La provenance complète devra être ajoutée par une future migration " - "du schéma OSINT.", + "Les objets créés ou réutilisés sont liés à l'exécution OSINT %s.", inserted_count, skipped_count, - inserted_relation_count, skipped_relation_count + inserted_relation_count, skipped_relation_count, + review_context->execution_identifier ); application_message_dialog_present( main_window_get_window(application->main_window), @@ -291,6 +297,7 @@ static void application_osint_action_context_free( g_free(context->target_value); g_free(context->target_identifier); + g_free(context->started_at); g_free(context); } @@ -1085,6 +1092,89 @@ static char *application_osint_output_to_utf8( return g_utf8_make_valid(data, (gssize) data_size); } +/** @brief Crée une date UTC au format persistant du projet. */ +static char *application_osint_create_timestamp(void) +{ + GDateTime *now = g_date_time_new_now_utc(); + char *timestamp = now != NULL + ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL; + g_clear_pointer(&now, g_date_time_unref); + return timestamp; +} + +/** @brief Calcule l'empreinte déterministe de stdout et stderr bruts. */ +static char *application_osint_hash_outputs(GBytes *stdout_raw, GBytes *stderr_raw) +{ + GChecksum *checksum = g_checksum_new(G_CHECKSUM_SHA256); + gconstpointer data = NULL; + gsize data_size = 0U; + const guint8 separator = 0U; + char *digest = NULL; + if (checksum == NULL || stdout_raw == NULL || stderr_raw == NULL) + { + g_clear_pointer(&checksum, g_checksum_free); + return NULL; + } + data = g_bytes_get_data(stdout_raw, &data_size); + if (data_size > 0U) g_checksum_update(checksum, data, data_size); + g_checksum_update(checksum, &separator, 1U); + data = g_bytes_get_data(stderr_raw, &data_size); + if (data_size > 0U) g_checksum_update(checksum, data, data_size); + digest = g_strdup(g_checksum_get_string(checksum)); + g_checksum_free(checksum); + return digest; +} + +/** @brief Persiste une exécution DNS terminée et retourne son UUID. */ +static char *application_persist_dns_execution( + ApplicationOsintActionContext *context, + const char *final_state, + gboolean has_exit_code, + gint exit_code, + GBytes *stdout_raw, + GBytes *stderr_raw, + GError **error +) +{ + Application *application = context != NULL ? context->application : NULL; + ToolRegistry *registry = NULL; + const ToolInfo *tool_info = NULL; + Database *database = NULL; + OsintExecutionDao *dao = NULL; + OsintExecutionRecord *record = NULL; + char *identifier = NULL; + char *finished_at = NULL; + char *arguments = NULL; + char *sha256 = NULL; + gboolean inserted = FALSE; + if (application == NULL || application->session == NULL) return NULL; + database = investigation_session_get_database(application->session); + registry = tool_initializer_get_registry(application->tool_initializer); + tool_info = tool_registry_find(registry, "dns.dig"); + identifier = g_uuid_string_random(); + finished_at = application_osint_create_timestamp(); + arguments = g_strdup_printf( + "[\"+noall\",\"+answer\",\"%s\"]", context->target_value + ); + sha256 = application_osint_hash_outputs(stdout_raw, stderr_raw); + record = osint_execution_record_new( + identifier, "dns.dig", + tool_info != NULL ? tool_info_get_detected_version(tool_info) : NULL, + "dns-preview", context->target_identifier, "entity", + context->target_value, arguments, context->started_at, finished_at, + has_exit_code, exit_code, final_state, stdout_raw, stderr_raw, sha256, + error + ); + dao = osint_execution_dao_new(database, error); + if (record != NULL && dao != NULL) + inserted = osint_execution_dao_insert(dao, record, error); + osint_execution_dao_free(dao); + osint_execution_record_free(record); + g_free(finished_at); g_free(arguments); g_free(sha256); + if (!inserted) g_clear_pointer(&identifier, g_free); + return identifier; +} + /** * @brief Présente le résultat final d'une résolution DNS. */ @@ -1103,9 +1193,11 @@ static void application_on_dns_lookup_completed( char *stderr_text = NULL; char *details = NULL; char *message = NULL; + char *execution_identifier = NULL; GPtrArray *proposals = NULL; ApplicationOsintReviewContext *review_context = NULL; GError *error = NULL; + const ToolInfo *dns_tool_info = NULL; if (task == NULL || context == NULL || context->application == NULL) { @@ -1118,19 +1210,38 @@ static void application_on_dns_lookup_completed( return; } + dns_tool_info = tool_registry_find( + tool_initializer_get_registry(application->tool_initializer), + "dns.dig" + ); + if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED) { + GBytes *empty_output = g_bytes_new_static("", 0U); + execution_identifier = application_persist_dns_execution( + context, "cancelled", FALSE, -1, empty_output, empty_output, &error + ); + g_bytes_unref(empty_output); application_message_dialog_present( main_window_get_window(application->main_window), APPLICATION_MESSAGE_DIALOG_INFORMATION, "Résolution DNS annulée", - "La tâche a été annulée. Aucun résultat n'a été enregistré." + execution_identifier != NULL + ? "La tâche a été annulée et sa provenance a été enregistrée." + : "La tâche a été annulée, mais sa provenance n'a pas pu être enregistrée." ); + g_free(execution_identifier); + g_clear_error(&error); return; } if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) { + GBytes *empty_output = g_bytes_new_static("", 0U); + execution_identifier = application_persist_dns_execution( + context, "failed", FALSE, -1, empty_output, empty_output, NULL + ); + g_bytes_unref(empty_output); error = background_task_dup_error(task); application_present_error( application, @@ -1139,6 +1250,7 @@ static void application_on_dns_lookup_completed( ? error->message : "L'exécution de dig a été annulée ou a échoué." ); + g_free(execution_identifier); g_clear_error(&error); return; } @@ -1157,6 +1269,23 @@ static void application_on_dns_lookup_completed( stdout_bytes = tool_process_result_ref_stdout(process_result); stderr_bytes = tool_process_result_ref_stderr(process_result); + execution_identifier = application_persist_dns_execution( + context, "completed", TRUE, + tool_process_result_get_exit_status(process_result), + stdout_bytes, stderr_bytes, &error + ); + if (execution_identifier == NULL) + { + application_present_error( + application, + "Conservation OSINT impossible", + error != NULL ? error->message + : "La sortie brute n'a pas pu être enregistrée." + ); + g_clear_error(&error); + g_clear_pointer(&stdout_bytes, g_bytes_unref); + return; + } stdout_text = application_osint_output_to_utf8(stdout_bytes); stderr_text = application_osint_output_to_utf8(stderr_bytes); @@ -1193,10 +1322,14 @@ static void application_on_dns_lookup_completed( review_context->source_entity_identifier = g_strdup( context->target_identifier ); + review_context->execution_identifier = g_strdup( + execution_identifier + ); review_context->proposals = g_ptr_array_ref(proposals); } if (review_context == NULL || review_context->proposals == NULL || - review_context->source_entity_identifier == NULL) + review_context->source_entity_identifier == NULL || + review_context->execution_identifier == NULL) { application_osint_review_context_free(review_context); review_context = NULL; @@ -1204,10 +1337,17 @@ static void application_on_dns_lookup_completed( } message = g_strdup_printf( - "Cible : %s\nOutil : dns.dig\nCode de sortie : %d\n" - "Résultat brut non enregistré dans l'enquête.", + "Cible : %s\nOutil : dns.dig%s%s\nDébut : %s\nCode de sortie : %d\n" + "Exécution : %s\nSortie brute enregistrée avec son empreinte SHA-256.", context->target_value, - tool_process_result_get_exit_status(process_result) + dns_tool_info != NULL && + tool_info_get_detected_version(dns_tool_info) != NULL ? " — " : "", + dns_tool_info != NULL && + tool_info_get_detected_version(dns_tool_info) != NULL + ? tool_info_get_detected_version(dns_tool_info) : "", + context->started_at, + tool_process_result_get_exit_status(process_result), + execution_identifier ); application_message_dialog_present_details_action( @@ -1232,6 +1372,7 @@ static void application_on_dns_lookup_completed( g_free(stderr_text); g_free(details); g_free(message); + g_free(execution_identifier); g_clear_pointer(&proposals, g_ptr_array_unref); } @@ -1294,10 +1435,11 @@ static void application_start_dns_lookup( context->application = application; context->target_identifier = g_strdup(target_identifier); context->target_value = g_strdup(target_value); + context->started_at = application_osint_create_timestamp(); } if (context == NULL || context->target_identifier == NULL || - context->target_value == NULL) + context->target_value == NULL || context->started_at == NULL) { application_osint_action_context_free(context); tool_task_free(tool_task); diff --git a/src/core/osint_dns_integration.c b/src/core/osint_dns_integration.c index 6844b92..c02e2c6 100644 --- a/src/core/osint_dns_integration.c +++ b/src/core/osint_dns_integration.c @@ -7,6 +7,7 @@ #include "dao/entity_dao.h" #include "dao/relation_dao.h" +#include "dao/osint_execution_dao.h" #include "database/error.h" #include "database/transaction.h" #include "models/entity_record.h" @@ -83,6 +84,7 @@ GQuark osint_dns_integration_error_quark(void) gboolean osint_dns_integration_apply( Database *database, const char *source_entity_identifier, + const char *execution_identifier, GPtrArray *selected_proposals, guint *out_inserted_entity_count, guint *out_skipped_entity_count, @@ -93,6 +95,7 @@ gboolean osint_dns_integration_apply( { EntityDao *entity_dao = NULL; RelationDao *relation_dao = NULL; + OsintExecutionDao *execution_dao = NULL; GPtrArray *existing_entities = NULL; GPtrArray *existing_relations = NULL; GHashTable *known_entities = NULL; @@ -113,6 +116,8 @@ gboolean osint_dns_integration_apply( if (out_skipped_relation_count != NULL) *out_skipped_relation_count = 0U; if (database == NULL || source_entity_identifier == NULL || !g_uuid_string_is_valid(source_entity_identifier) || + execution_identifier == NULL || + !g_uuid_string_is_valid(execution_identifier) || selected_proposals == NULL || selected_proposals->len == 0U) { @@ -127,6 +132,8 @@ gboolean osint_dns_integration_apply( if (entity_dao == NULL) return FALSE; relation_dao = relation_dao_new(database, error); if (relation_dao == NULL) goto cleanup; + execution_dao = osint_execution_dao_new(database, error); + if (execution_dao == NULL) goto cleanup; source_entity = entity_dao_find_by_identifier( entity_dao, source_entity_identifier, error ); @@ -164,20 +171,21 @@ gboolean osint_dns_integration_apply( g_free(normalized_value); } known_relations = g_hash_table_new_full( - g_str_hash, g_str_equal, g_free, NULL + g_str_hash, g_str_equal, g_free, g_free ); for (guint index = 0; index < existing_relations->len; index++) { const RelationRecord *relation = g_ptr_array_index( existing_relations, index ); - g_hash_table_add( + g_hash_table_insert( known_relations, osint_dns_integration_build_relation_key( relation_record_get_source_entity_identifier(relation), relation_record_get_target_entity_identifier(relation), relation_record_get_relation_type(relation) - ) + ), + g_strdup(relation_record_get_identifier(relation)) ); } @@ -222,6 +230,8 @@ gboolean osint_dns_integration_apply( char *relation_label = NULL; char *relation_justification = NULL; GError *model_error = NULL; + gboolean entity_was_created = FALSE; + gboolean relation_was_created = FALSE; if (entity_type == NULL || relation_type == NULL || normalized_value == NULL) @@ -271,6 +281,7 @@ gboolean osint_dns_integration_apply( ); entity_key = NULL; inserted_entity_count++; + entity_was_created = TRUE; entity_record_free(entity); g_clear_error(&model_error); g_free(description); @@ -279,8 +290,11 @@ gboolean osint_dns_integration_apply( relation_key = osint_dns_integration_build_relation_key( source_entity_identifier, target_identifier, relation_type ); + relation_identifier = g_strdup( + g_hash_table_lookup(known_relations, relation_key) + ); if (g_strcmp0(source_entity_identifier, target_identifier) == 0 || - g_hash_table_contains(known_relations, relation_key)) + relation_identifier != NULL) { skipped_relation_count++; } @@ -321,16 +335,36 @@ gboolean osint_dns_integration_apply( database_transaction_rollback(database); goto cleanup; } - g_hash_table_add(known_relations, relation_key); + g_hash_table_insert( + known_relations, relation_key, g_strdup(relation_identifier) + ); relation_key = NULL; inserted_relation_count++; + relation_was_created = TRUE; relation_record_free(relation); g_clear_error(&model_error); - g_free(relation_identifier); g_free(relation_label); g_free(relation_justification); } + if (!osint_execution_dao_link_entity( + execution_dao, execution_identifier, target_identifier, + entity_was_created ? "created" : "reused", error + ) || + (relation_identifier != NULL && + !osint_execution_dao_link_relation( + execution_dao, execution_identifier, relation_identifier, + relation_was_created ? "created" : "reused", error + ))) + { + g_free(relation_identifier); + g_free(relation_key); g_free(target_identifier); + g_free(entity_key); g_free(normalized_value); + database_transaction_rollback(database); + goto cleanup; + } + + g_free(relation_identifier); g_free(relation_key); g_free(target_identifier); g_free(entity_key); @@ -367,6 +401,7 @@ cleanup: g_clear_pointer(&existing_relations, g_ptr_array_unref); entity_record_free(source_entity); relation_dao_free(relation_dao); + osint_execution_dao_free(execution_dao); entity_dao_free(entity_dao); return success; } diff --git a/src/dao/osint_execution_dao.c b/src/dao/osint_execution_dao.c new file mode 100644 index 0000000..1e29d08 --- /dev/null +++ b/src/dao/osint_execution_dao.c @@ -0,0 +1,186 @@ +/****************************************************************************** + * @file osint_execution_dao.c + * @brief Persistance de la provenance des exécutions OSINT. + ******************************************************************************/ + +#include "dao/osint_execution_dao.h" + +#include "database/error.h" +#include "database/statement.h" + +struct OsintExecutionDao { Database *database; }; + +static const char *const insert_sql = + "INSERT INTO osint_executions(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) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"; +static const char *const find_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 id=?;"; + +static void osint_execution_dao_set_error( + OsintExecutionDao *dao, GError **error, const char *context +) +{ + if (error != NULL && *error == NULL) + g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s : %s", context, + dao != NULL && database_error_get_message(dao->database) != NULL + ? database_error_get_message(dao->database) : "erreur SQLite"); +} + +OsintExecutionDao *osint_execution_dao_new(Database *database, GError **error) +{ + if (database == NULL) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "La connexion de provenance OSINT est absente."); + return NULL; + } + OsintExecutionDao *dao = g_try_new0(OsintExecutionDao, 1); + if (dao != NULL) dao->database = database; + return dao; +} + +void osint_execution_dao_free(OsintExecutionDao *dao) { g_free(dao); } + +gboolean osint_execution_dao_insert( + OsintExecutionDao *dao, const OsintExecutionRecord *record, GError **error +) +{ + DatabaseStatement *statement = NULL; + GBytes *stdout_raw = NULL; + GBytes *stderr_raw = NULL; + gboolean success = FALSE; + if (dao == NULL || record == NULL) return FALSE; + statement = database_statement_prepare(dao->database, insert_sql); + if (statement == NULL) goto cleanup; + stdout_raw = osint_execution_record_ref_stdout(record); + stderr_raw = osint_execution_record_ref_stderr(record); + success = database_statement_bind_text(statement, 1, + osint_execution_record_get_identifier(record)) && + database_statement_bind_text(statement, 2, + osint_execution_record_get_tool_identifier(record)) && + (osint_execution_record_get_tool_version(record) != NULL + ? database_statement_bind_text(statement, 3, + osint_execution_record_get_tool_version(record)) + : database_statement_bind_null(statement, 3)) && + database_statement_bind_text(statement, 4, + osint_execution_record_get_action_identifier(record)) && + database_statement_bind_text(statement, 5, + osint_execution_record_get_selection_identifier(record)) && + database_statement_bind_text(statement, 6, + osint_execution_record_get_selection_kind(record)) && + database_statement_bind_text(statement, 7, + osint_execution_record_get_target_value(record)) && + database_statement_bind_text(statement, 8, + osint_execution_record_get_arguments(record)) && + database_statement_bind_text(statement, 9, + osint_execution_record_get_started_at(record)) && + database_statement_bind_text(statement, 10, + osint_execution_record_get_finished_at(record)) && + (osint_execution_record_has_exit_code(record) + ? database_statement_bind_int64(statement, 11, + osint_execution_record_get_exit_code(record)) + : database_statement_bind_null(statement, 11)) && + database_statement_bind_text(statement, 12, + osint_execution_record_get_final_state(record)) && + database_statement_bind_blob(statement, 13, stdout_raw) && + database_statement_bind_blob(statement, 14, stderr_raw) && + database_statement_bind_text(statement, 15, + osint_execution_record_get_output_sha256(record)) && + database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE; +cleanup: + if (!success) osint_execution_dao_set_error(dao, error, + "Impossible d'insérer l'exécution OSINT"); + g_clear_pointer(&stdout_raw, g_bytes_unref); + g_clear_pointer(&stderr_raw, g_bytes_unref); + database_statement_finalize(statement); + return success; +} + +OsintExecutionRecord *osint_execution_dao_find_by_identifier( + OsintExecutionDao *dao, const char *identifier, 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]) || + !database_statement_column_text(statement, 3, &values[3]) || + !database_statement_column_text(statement, 4, &values[4]) || + !database_statement_column_text(statement, 5, &values[5]) || + !database_statement_column_text(statement, 6, &values[6]) || + !database_statement_column_text(statement, 7, &values[7]) || + !database_statement_column_text(statement, 8, &values[8]) || + !database_statement_column_text(statement, 9, &values[9]) || + !database_statement_column_is_null(statement, 10, &exit_is_null) || + (!exit_is_null && !database_statement_column_int64(statement, 10, &exit_code)) || + !database_statement_column_text(statement, 11, &values[10]) || + !database_statement_column_blob(statement, 12, &stdout_raw) || + !database_statement_column_blob(statement, 13, &stderr_raw) || + !database_statement_column_text(statement, 14, &values[11])) goto cleanup; + record = osint_execution_record_new( + values[0], values[1], values[2], values[3], values[4], values[5], + 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); + database_statement_finalize(statement); + return record; +} + +static gboolean osint_execution_dao_link( + OsintExecutionDao *dao, const char *table, const char *object_column, + const char *execution_identifier, const char *object_identifier, + const char *disposition, GError **error +) +{ + char *sql = NULL; + DatabaseStatement *statement = NULL; + gboolean success = FALSE; + if (dao == NULL || execution_identifier == NULL || object_identifier == NULL || + (g_strcmp0(disposition, "created") != 0 && + g_strcmp0(disposition, "reused") != 0)) return FALSE; + sql = g_strdup_printf( + "INSERT OR IGNORE INTO %s(execution_id,%s,disposition) VALUES(?,?,?);", + table, object_column); + statement = database_statement_prepare(dao->database, sql); + success = statement != NULL && + database_statement_bind_text(statement, 1, execution_identifier) && + database_statement_bind_text(statement, 2, object_identifier) && + database_statement_bind_text(statement, 3, disposition) && + database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE; + if (!success) osint_execution_dao_set_error(dao, error, + "Impossible de lier la provenance OSINT"); + database_statement_finalize(statement); g_free(sql); return success; +} + +gboolean osint_execution_dao_link_entity( + OsintExecutionDao *dao, const char *execution_identifier, + const char *entity_identifier, const char *disposition, GError **error) +{ return osint_execution_dao_link(dao, "osint_execution_entities", "entity_id", + execution_identifier, entity_identifier, disposition, error); } +gboolean osint_execution_dao_link_relation( + OsintExecutionDao *dao, const char *execution_identifier, + const char *relation_identifier, const char *disposition, GError **error) +{ return osint_execution_dao_link(dao, "osint_execution_relations", "relation_id", + execution_identifier, relation_identifier, disposition, error); } diff --git a/src/database/database.c b/src/database/database.c index e05c946..61536e8 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -16,12 +16,12 @@ /** * @brief Version actuelle du schéma SQLite. */ -#define DATABASE_SCHEMA_VERSION_CURRENT 2 +#define DATABASE_SCHEMA_VERSION_CURRENT 3 /** * @brief Version actuelle sous forme textuelle pour metadata. */ -#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "2" +#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "3" /** * @brief Nom de l'application enregistré dans les métadonnées. @@ -668,6 +668,30 @@ rollback: return false; } +/** + * @brief Applique atomiquement la migration du schéma V2 vers V3. + */ +static bool database_migrate_v2_to_v3( + Database *database +) +{ + bool transaction_started = false; + + if (database == NULL || !database_transaction_begin(database)) + return false; + transaction_started = true; + if (!schema_install_v3(database) || + !database_update_schema_version(database, "3") || + !database_transaction_commit(database)) + goto rollback; + return true; + +rollback: + if (transaction_started && !database_transaction_rollback(database)) + g_warning("Impossible d’annuler la migration SQLite V2 vers V3."); + return false; +} + /** * @brief Garantit atomiquement la présence des extensions du schéma courant. */ @@ -854,6 +878,12 @@ bool database_migrate_to_latest( schema_version = 2; break; + case 2: + if (!database_migrate_v2_to_v3(database)) + return false; + schema_version = 3; + break; + default: database_set_error( database, @@ -963,6 +993,13 @@ bool database_initialize( goto rollback; } + if (!schema_install_v3( + database + )) + { + goto rollback; + } + if (!schema_ensure_current( database )) diff --git a/src/database/schema.c b/src/database/schema.c index 0302c22..874e707 100644 --- a/src/database/schema.c +++ b/src/database/schema.c @@ -193,6 +193,17 @@ bool schema_install_v2( ); } +bool schema_install_v3( + Database *database +) +{ + return schema_execute_file( + database, + "database/schema_v3.sql", + "la migration SQLite V3" + ); +} + bool schema_ensure_current( Database *database ) diff --git a/src/database/statement.c b/src/database/statement.c index 6db1b91..6360c52 100644 --- a/src/database/statement.c +++ b/src/database/statement.c @@ -172,6 +172,49 @@ bool database_statement_bind_text( return true; } +bool database_statement_bind_blob( + DatabaseStatement *statement, + int index, + GBytes *value +) +{ + sqlite3 *database_handle = NULL; + gconstpointer data = NULL; + gsize data_size = 0U; + int result = SQLITE_ERROR; + + if (statement == NULL || statement->handle == NULL || + index <= 0 || value == NULL) + { + if (statement != NULL) + database_set_error( + statement->database, DATABASE_ERROR_INVALID_ARGUMENT, + "Paramètres invalides pour la liaison d'un BLOB." + ); + return false; + } + + data = g_bytes_get_data(value, &data_size); + result = data_size == 0U + ? sqlite3_bind_zeroblob(statement->handle, index, 0) + : sqlite3_bind_blob64( + statement->handle, index, data, (sqlite3_uint64) data_size, + SQLITE_TRANSIENT + ); + if (result != SQLITE_OK) + { + database_handle = database_get_handle(statement->database); + database_set_error( + statement->database, DATABASE_ERROR_SQLITE, + database_handle != NULL ? sqlite3_errmsg(database_handle) + : sqlite3_errstr(result) + ); + return false; + } + database_clear_error_internal(statement->database); + return true; +} + bool database_statement_bind_int64( DatabaseStatement *statement, int index, @@ -666,6 +709,32 @@ bool database_statement_column_text( return true; } +bool database_statement_column_blob( + DatabaseStatement *statement, + int column_index, + GBytes **value +) +{ + const void *column_data = NULL; + int column_count = 0; + int data_length = 0; + + if (statement == NULL || statement->handle == NULL || value == NULL) + return false; + *value = NULL; + column_count = sqlite3_column_count(statement->handle); + if (column_index < 0 || column_index >= column_count) return false; + if (sqlite3_column_type(statement->handle, column_index) == SQLITE_NULL) + return true; + if (sqlite3_column_type(statement->handle, column_index) != SQLITE_BLOB) + return false; + column_data = sqlite3_column_blob(statement->handle, column_index); + data_length = sqlite3_column_bytes(statement->handle, column_index); + if (data_length < 0 || (data_length > 0 && column_data == NULL)) return false; + *value = g_bytes_new(column_data, (gsize) data_length); + return *value != NULL; +} + bool database_statement_column_int64( DatabaseStatement *statement, int column_index, diff --git a/src/models/osint_execution_record.c b/src/models/osint_execution_record.c new file mode 100644 index 0000000..2ec7681 --- /dev/null +++ b/src/models/osint_execution_record.c @@ -0,0 +1,130 @@ +/****************************************************************************** + * @file osint_execution_record.c + * @brief Modèle immuable d'une exécution OSINT persistée. + ******************************************************************************/ + +#include "models/osint_execution_record.h" + +#include + +struct OsintExecutionRecord +{ + char *identifier; + char *tool_identifier; + char *tool_version; + char *action_identifier; + char *selection_identifier; + char *selection_kind; + char *target_value; + char *arguments; + char *started_at; + char *finished_at; + gboolean has_exit_code; + gint exit_code; + char *final_state; + GBytes *stdout_raw; + GBytes *stderr_raw; + char *output_sha256; +}; + +static gboolean osint_execution_record_text_valid(const char *text) +{ return text != NULL && text[0] != '\0'; } + +OsintExecutionRecord *osint_execution_record_new( + const char *identifier, const char *tool_identifier, + const char *tool_version, const char *action_identifier, + const char *selection_identifier, const char *selection_kind, + const char *target_value, const char *arguments, + const char *started_at, const char *finished_at, + gboolean has_exit_code, gint exit_code, const char *final_state, + GBytes *stdout_raw, GBytes *stderr_raw, const char *output_sha256, + GError **error +) +{ + OsintExecutionRecord *record = NULL; + const gboolean selection_valid = g_strcmp0(selection_kind, "entity") == 0 || + g_strcmp0(selection_kind, "relation") == 0; + const gboolean state_valid = g_strcmp0(final_state, "completed") == 0 || + g_strcmp0(final_state, "failed") == 0 || + g_strcmp0(final_state, "cancelled") == 0; + if ((error != NULL && *error != NULL) || identifier == NULL || + !g_uuid_string_is_valid(identifier) || selection_identifier == NULL || + !g_uuid_string_is_valid(selection_identifier) || !selection_valid || + !state_valid || !osint_execution_record_text_valid(tool_identifier) || + !osint_execution_record_text_valid(action_identifier) || + !osint_execution_record_text_valid(target_value) || arguments == NULL || + started_at == NULL || strlen(started_at) != 20U || finished_at == NULL || + strlen(finished_at) != 20U || stdout_raw == NULL || stderr_raw == NULL || + output_sha256 == NULL || strlen(output_sha256) != 64U) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "L'exécution OSINT est invalide."); + return NULL; + } + record = g_try_new0(OsintExecutionRecord, 1); + if (record == NULL) return NULL; + record->identifier = g_strdup(identifier); + record->tool_identifier = g_strdup(tool_identifier); + record->tool_version = g_strdup(tool_version); + record->action_identifier = g_strdup(action_identifier); + record->selection_identifier = g_strdup(selection_identifier); + record->selection_kind = g_strdup(selection_kind); + record->target_value = g_strdup(target_value); + record->arguments = g_strdup(arguments); + record->started_at = g_strdup(started_at); + record->finished_at = g_strdup(finished_at); + record->has_exit_code = has_exit_code; + record->exit_code = exit_code; + record->final_state = g_strdup(final_state); + record->stdout_raw = g_bytes_ref(stdout_raw); + record->stderr_raw = g_bytes_ref(stderr_raw); + record->output_sha256 = g_strdup(output_sha256); + if (record->identifier == NULL || record->tool_identifier == NULL || + record->action_identifier == NULL || record->selection_identifier == NULL || + record->selection_kind == NULL || record->target_value == NULL || + record->arguments == NULL || record->started_at == NULL || + record->finished_at == NULL || record->final_state == NULL || + record->output_sha256 == NULL) + { + osint_execution_record_free(record); + return NULL; + } + return record; +} + +void osint_execution_record_free(OsintExecutionRecord *record) +{ + if (record == NULL) return; + g_free(record->output_sha256); g_clear_pointer(&record->stderr_raw, g_bytes_unref); + g_clear_pointer(&record->stdout_raw, g_bytes_unref); g_free(record->final_state); + g_free(record->finished_at); g_free(record->started_at); g_free(record->arguments); + g_free(record->target_value); g_free(record->selection_kind); + g_free(record->selection_identifier); g_free(record->action_identifier); + g_free(record->tool_version); g_free(record->tool_identifier); + g_free(record->identifier); g_free(record); +} + +#define OSINT_GETTER(name, field) \ +const char *name(const OsintExecutionRecord *record) \ +{ return record != NULL ? record->field : NULL; } +OSINT_GETTER(osint_execution_record_get_identifier, identifier) +OSINT_GETTER(osint_execution_record_get_tool_identifier, tool_identifier) +OSINT_GETTER(osint_execution_record_get_tool_version, tool_version) +OSINT_GETTER(osint_execution_record_get_action_identifier, action_identifier) +OSINT_GETTER(osint_execution_record_get_selection_identifier, selection_identifier) +OSINT_GETTER(osint_execution_record_get_selection_kind, selection_kind) +OSINT_GETTER(osint_execution_record_get_target_value, target_value) +OSINT_GETTER(osint_execution_record_get_arguments, arguments) +OSINT_GETTER(osint_execution_record_get_started_at, started_at) +OSINT_GETTER(osint_execution_record_get_finished_at, finished_at) +OSINT_GETTER(osint_execution_record_get_final_state, final_state) +OSINT_GETTER(osint_execution_record_get_output_sha256, output_sha256) +#undef OSINT_GETTER +gboolean osint_execution_record_has_exit_code(const OsintExecutionRecord *record) +{ return record != NULL && record->has_exit_code; } +gint osint_execution_record_get_exit_code(const OsintExecutionRecord *record) +{ return record != NULL && record->has_exit_code ? record->exit_code : -1; } +GBytes *osint_execution_record_ref_stdout(const OsintExecutionRecord *record) +{ return record != NULL && record->stdout_raw != NULL ? g_bytes_ref(record->stdout_raw) : NULL; } +GBytes *osint_execution_record_ref_stderr(const OsintExecutionRecord *record) +{ return record != NULL && record->stderr_raw != NULL ? g_bytes_ref(record->stderr_raw) : NULL; } diff --git a/tests/test_database.c b/tests/test_database.c index edaa7f8..a4e2373 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -525,7 +525,10 @@ static void test_database_initialize_valid_database(void) "FROM investigation;" ); - assert(strcmp(schema_version, "2") == 0); + assert(strcmp(schema_version, "3") == 0); + test_database_assert_table_exists(database, "osint_executions"); + test_database_assert_table_exists(database, "osint_execution_entities"); + test_database_assert_table_exists(database, "osint_execution_relations"); assert(strcmp(application_name, "Labfy Investigation") == 0); assert(created_at[0] != '\0'); @@ -795,7 +798,7 @@ static void test_database_initialize_rollback(void) } /** - * @brief Vérifie la migration V1 vers V2 et sa répétition sans effet. + * @brief Vérifie la migration V1 vers la version courante sans perte. */ static void test_database_migrate_v1_to_v2(void) { @@ -849,7 +852,7 @@ static void test_database_migrate_v1_to_v2(void) assert(database_context != NULL); /* - * Premier appel : migration réelle de V1 vers V2. + * Premier appel : migrations réelles de V1 vers la version courante. */ assert( database_migrate_to_latest( @@ -864,7 +867,7 @@ static void test_database_migrate_v1_to_v2(void) ); /* - * Second appel : la base est déjà en V2. + * Second appel : la base est déjà à jour. * * Aucun ALTER TABLE, index ou trigger ne doit être rejoué. */ @@ -984,7 +987,7 @@ static void test_database_migrate_v1_to_v2(void) assert( strcmp( schema_version, - "2" + "3" ) == 0 ); diff --git a/tests/test_osint_dns_integration.c b/tests/test_osint_dns_integration.c index 6f3a8c0..ac40b33 100644 --- a/tests/test_osint_dns_integration.c +++ b/tests/test_osint_dns_integration.c @@ -7,13 +7,49 @@ #include "dao/entity_dao.h" #include "dao/relation_dao.h" +#include "dao/osint_execution_dao.h" #include "database/database.h" +#include "database/statement.h" #include "models/osint_dns_proposal.h" #include "models/relation_record.h" +#include "models/osint_execution_record.h" #include #include +static void test_insert_execution(Database *database, const char *source_identifier) +{ + GError *error = NULL; + GBytes *empty = g_bytes_new_static("", 0U); + OsintExecutionRecord *record = osint_execution_record_new( + "99999999-9999-4999-8999-999999999999", "dns.dig", "DiG 9", + "dns-preview", source_identifier, "entity", "example.org", + "[\"+noall\",\"+answer\",\"example.org\"]", + "2026-01-01T00:00:00Z", "2026-01-01T00:00:01Z", TRUE, 0, + "completed", empty, empty, + "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + &error + ); + OsintExecutionDao *dao = osint_execution_dao_new(database, &error); + g_assert_true(osint_execution_dao_insert(dao, record, &error)); + osint_execution_dao_free(dao); + osint_execution_record_free(record); + g_bytes_unref(empty); +} + +static gint64 test_count_rows(Database *database, const char *table) +{ + char *sql = g_strdup_printf("SELECT COUNT(*) FROM %s;", table); + DatabaseStatement *statement = database_statement_prepare(database, sql); + int64_t count = -1; + g_assert_cmpint(database_statement_step(statement), ==, + DATABASE_STATEMENT_STEP_ROW); + g_assert_true(database_statement_column_int64(statement, 0, &count)); + database_statement_finalize(statement); + g_free(sql); + return count; +} + static void test_integration_and_duplicates(void) { GError *error = NULL; @@ -41,6 +77,7 @@ static void test_integration_and_duplicates(void) "2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error ); g_assert_true(entity_dao_insert(entity_dao, source, &error)); + test_insert_execution(database, "11111111-1111-4111-8111-111111111111"); proposals = osint_dns_proposal_parse( "example.org", "example.org. 60 IN A 192.0.2.10\n" @@ -48,7 +85,8 @@ static void test_integration_and_duplicates(void) "example.org. 60 IN MX 10 mail.example.org.\n" ); g_assert_true(osint_dns_integration_apply( - database, "11111111-1111-4111-8111-111111111111", proposals, + database, "11111111-1111-4111-8111-111111111111", + "99999999-9999-4999-8999-999999999999", proposals, &inserted, &skipped, &inserted_relations, &skipped_relations, &error )); g_assert_no_error(error); @@ -58,7 +96,8 @@ static void test_integration_and_duplicates(void) g_assert_cmpuint(skipped_relations, ==, 1); g_assert_true(osint_dns_integration_apply( - database, "11111111-1111-4111-8111-111111111111", proposals, + database, "11111111-1111-4111-8111-111111111111", + "99999999-9999-4999-8999-999999999999", proposals, &inserted, &skipped, &inserted_relations, &skipped_relations, &error )); g_assert_cmpuint(inserted, ==, 0); @@ -70,6 +109,12 @@ static void test_integration_and_duplicates(void) relation_dao = relation_dao_new(database, &error); relations = relation_dao_list_all(relation_dao, &error); g_assert_cmpuint(relations->len, ==, 2); + g_assert_cmpint( + test_count_rows(database, "osint_execution_entities"), ==, 2 + ); + g_assert_cmpint( + test_count_rows(database, "osint_execution_relations"), ==, 2 + ); g_ptr_array_unref(relations); relation_dao_free(relation_dao); @@ -88,7 +133,7 @@ static void test_invalid_arguments(void) { GError *error = NULL; g_assert_false(osint_dns_integration_apply( - NULL, NULL, NULL, NULL, NULL, NULL, NULL, &error + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &error )); g_assert_error( error, OSINT_DNS_INTEGRATION_ERROR, @@ -120,11 +165,13 @@ static void test_normalized_existing_domain_is_skipped(void) "2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error ); g_assert_true(entity_dao_insert(entity_dao, existing, &error)); + test_insert_execution(database, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); proposals = osint_dns_proposal_parse( "example.org", "example.org. 60 IN CNAME www.example.org.\n" ); g_assert_true(osint_dns_integration_apply( - database, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", proposals, + database, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "99999999-9999-4999-8999-999999999999", proposals, &inserted, &skipped, &inserted_relations, &skipped_relations, &error )); g_assert_cmpuint(inserted, ==, 0); diff --git a/tests/test_osint_execution_dao.c b/tests/test_osint_execution_dao.c new file mode 100644 index 0000000..e2f1d27 --- /dev/null +++ b/tests/test_osint_execution_dao.c @@ -0,0 +1,84 @@ +/****************************************************************************** + * @file test_osint_execution_dao.c + * @brief Tests du modèle et du DAO de provenance OSINT. + ******************************************************************************/ + +#include "dao/osint_execution_dao.h" +#include "database/database.h" + +#include +#include + +static void test_insert_and_read_raw_execution(void) +{ + const guint8 raw_data[] = {0x41U, 0x00U, 0xFFU}; + GError *error = NULL; + char *directory = g_dir_make_tmp("labfy-osint-execution-XXXXXX", &error); + char *path = g_build_filename(directory, "Enquete.sqlite", NULL); + Database *database = NULL; + OsintExecutionDao *dao = NULL; + OsintExecutionRecord *record = NULL; + OsintExecutionRecord *loaded = NULL; + GBytes *stdout_raw = g_bytes_new_static(raw_data, sizeof(raw_data)); + GBytes *stderr_raw = g_bytes_new_static("", 0U); + GBytes *loaded_stdout = NULL; + + g_assert_true(database_initialize(path, "OSINT", directory)); + database = database_open(path); + dao = osint_execution_dao_new(database, &error); + record = osint_execution_record_new( + "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", "dns.dig", "DiG 9", + "dns-preview", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "entity", + "example.org", "[\"example.org\"]", "2026-01-01T00:00:00Z", + "2026-01-01T00:00:01Z", TRUE, 0, "completed", stdout_raw, + stderr_raw, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + &error + ); + g_assert_nonnull(record); + g_assert_true(osint_execution_dao_insert(dao, record, &error)); + loaded = osint_execution_dao_find_by_identifier( + dao, "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", &error + ); + g_assert_nonnull(loaded); + g_assert_cmpstr( + osint_execution_record_get_target_value(loaded), ==, "example.org" + ); + 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)); + + g_bytes_unref(loaded_stdout); + osint_execution_record_free(loaded); + osint_execution_record_free(record); + osint_execution_dao_free(dao); + database_close(database); + g_bytes_unref(stdout_raw); + g_bytes_unref(stderr_raw); + g_assert_cmpint(g_remove(path), ==, 0); + g_assert_cmpint(g_rmdir(directory), ==, 0); + g_free(path); + g_free(directory); +} + +static void test_invalid_model(void) +{ + GError *error = NULL; + GBytes *empty = g_bytes_new_static("", 0U); + g_assert_null(osint_execution_record_new( + "invalid", "dns.dig", NULL, "dns-preview", "invalid", "entity", + "example.org", "[]", "bad", "bad", FALSE, -1, "completed", + empty, empty, "bad", &error + )); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_clear_error(&error); + g_bytes_unref(empty); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/osint-execution-dao/raw", test_insert_and_read_raw_execution); + g_test_add_func("/osint-execution-record/invalid", test_invalid_model); + return g_test_run(); +} diff --git a/tests/test_statement.c b/tests/test_statement.c index 0436b4f..6eecb33 100644 --- a/tests/test_statement.c +++ b/tests/test_statement.c @@ -8,6 +8,7 @@ #include #include +#include #include /** @@ -663,6 +664,31 @@ static void test_column_int64(void) database_close(database); } +/** + * @brief Vérifie la conservation exacte d'un paramètre BLOB. + */ +static void test_bind_and_read_blob(void) +{ + const guint8 expected_data[] = {0x00U, 0x41U, 0xFFU, 0x0AU}; + Database *database = database_open(":memory:"); + DatabaseStatement *statement = database_statement_prepare(database, "SELECT ?;"); + GBytes *input = g_bytes_new_static(expected_data, sizeof(expected_data)); + GBytes *output = NULL; + gconstpointer output_data = NULL; + gsize output_size = 0U; + + assert(database_statement_bind_blob(statement, 1, input)); + assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW); + assert(database_statement_column_blob(statement, 0, &output)); + output_data = g_bytes_get_data(output, &output_size); + assert(output_size == sizeof(expected_data)); + assert(memcmp(output_data, expected_data, sizeof(expected_data)) == 0); + g_bytes_unref(output); + g_bytes_unref(input); + database_statement_finalize(statement); + database_close(database); +} + int main(void) { test_prepare_valid_statement(); @@ -680,6 +706,7 @@ int main(void) test_column_is_null(); test_column_text(); test_column_int64(); + test_bind_and_read_blob(); printf( "DatabaseStatement : tous les tests sont valides.\n"