feat(osint): persist structured execution provenance

This commit is contained in:
grayTerminal-sh 2026-07-22 12:44:17 +02:00
parent 16a5d81e95
commit 1cbbc6acb1
21 changed files with 1089 additions and 46 deletions

View file

@ -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_QUERY := tests/test_osint_dns_query
TEST_OSINT_DNS_PROPOSAL := tests/test_osint_dns_proposal TEST_OSINT_DNS_PROPOSAL := tests/test_osint_dns_proposal
TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration
TEST_OSINT_EXECUTION_DAO := tests/test_osint_execution_dao
all: $(TARGET) all: $(TARGET)
@ -566,8 +567,21 @@ $(TEST_OSINT_DNS_INTEGRATION): \
src/models/osint_dns_query.c \ src/models/osint_dns_query.c \
src/models/entity_record.c \ src/models/entity_record.c \
src/models/relation_record.c \ src/models/relation_record.c \
src/models/osint_execution_record.c \
src/dao/entity_dao.c \ src/dao/entity_dao.c \
src/dao/relation_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/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/statement.c \ src/database/statement.c \
@ -647,7 +661,8 @@ test: \
$(TEST_OSINT_ACTION_CATALOG) \ $(TEST_OSINT_ACTION_CATALOG) \
$(TEST_OSINT_DNS_QUERY) \ $(TEST_OSINT_DNS_QUERY) \
$(TEST_OSINT_DNS_PROPOSAL) \ $(TEST_OSINT_DNS_PROPOSAL) \
$(TEST_OSINT_DNS_INTEGRATION) $(TEST_OSINT_DNS_INTEGRATION) \
$(TEST_OSINT_EXECUTION_DAO)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -700,6 +715,7 @@ test: \
@$(TEST_OSINT_DNS_QUERY) @$(TEST_OSINT_DNS_QUERY)
@$(TEST_OSINT_DNS_PROPOSAL) @$(TEST_OSINT_DNS_PROPOSAL)
@$(TEST_OSINT_DNS_INTEGRATION) @$(TEST_OSINT_DNS_INTEGRATION)
@$(TEST_OSINT_EXECUTION_DAO)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -758,7 +774,8 @@ clean:
$(TEST_OSINT_ACTION_CATALOG) \ $(TEST_OSINT_ACTION_CATALOG) \
$(TEST_OSINT_DNS_QUERY) \ $(TEST_OSINT_DNS_QUERY) \
$(TEST_OSINT_DNS_PROPOSAL) \ $(TEST_OSINT_DNS_PROPOSAL) \
$(TEST_OSINT_DNS_INTEGRATION) $(TEST_OSINT_DNS_INTEGRATION) \
$(TEST_OSINT_EXECUTION_DAO)
-include $(DEP) -include $(DEP)

View file

@ -151,7 +151,9 @@ Le socle actuel comprend notamment :
- sélection explicite et intégration transactionnelle des propositions DNS - sélection explicite et intégration transactionnelle des propositions DNS
compatibles, avec normalisation et détection des doublons ; compatibles, avec normalisation et détection des doublons ;
- création transactionnelle des relations DNS `resolves_to`, `aliases_to` et - 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 : Les outils actuellement présents dans le catalogue initial sont :

View file

@ -1,7 +1,7 @@
/****************************************************************************** /******************************************************************************
* Labfy Investigation * Labfy Investigation
* *
* Extensions idempotentes du schéma SQLite courant V2 * Extensions idempotentes du schéma SQLite courant V3
******************************************************************************/ ******************************************************************************/
/* /*

90
database/schema_v3.sql Normal file
View file

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

View file

@ -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à réalisée avec précaution afin de préserver la cohérence des données déjà
enregistrées. 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 La table `osint_executions` conserve chaque exécution terminée, y compris
`ip_address`, ainsi que les relations `resolves_to`, `aliases_to` et lorsque la révision est annulée. Elle contient notamment :
`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 :
- l'outil et sa version ; - l'outil et sa version ;
- la cible et les arguments de l'exécution ; - la cible et les arguments de l'exécution ;
- la date d'exécution ; - la date d'exécution ;
- la sortie brute et son empreinte ; - les sorties standard et d'erreur sous forme de BLOB ;
- les propositions extraites ; - leur empreinte SHA-256.
- les entités effectivement créées ou réutilisées.
Jusqu'à cette migration, l'intégration ne doit pas présenter les entités DNS Les tables `osint_execution_entities` et `osint_execution_relations` relient
comme des faits vérifiés et ne doit pas supprimer la sortie brute affichée à l'exécution aux objets intégrés. Le champ `disposition` distingue les objets
l'utilisateur. 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.
--- ---

View file

@ -40,6 +40,7 @@ GQuark osint_dns_integration_error_quark(void);
* *
* @param database Connexion SQLite active. * @param database Connexion SQLite active.
* @param source_entity_identifier UUID de l'entité interrogée. * @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 selected_proposals Propositions explicitement sélectionnées.
* @param out_inserted_entity_count Nombre d'entités créé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. * @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( gboolean osint_dns_integration_apply(
Database *database, Database *database,
const char *source_entity_identifier, const char *source_entity_identifier,
const char *execution_identifier,
GPtrArray *selected_proposals, GPtrArray *selected_proposals,
guint *out_inserted_entity_count, guint *out_inserted_entity_count,
guint *out_skipped_entity_count, guint *out_skipped_entity_count,

View file

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

View file

@ -54,6 +54,20 @@ bool schema_install_v2(
Database *database 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 é appliquée, sinon false.
*/
bool schema_install_v3(
Database *database
);
/** /**
* @brief Garantit la présence des extensions du schéma courant V2. * @brief Garantit la présence des extensions du schéma courant V2.
* *

View file

@ -10,6 +10,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <glib.h>
/** /**
* @brief Résultat de l'exécution d'une requête préparée. * @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 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. * @brief Lie une chaîne de caractères à un paramètre SQL.
* *
@ -178,6 +197,23 @@ bool database_statement_bind_text(
const char *value 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. * @brief Lie un entier signé sur 64 bits à un paramètre SQL.
* *

View file

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

View file

@ -15,6 +15,7 @@
#include "models/investigation_record.h" #include "models/investigation_record.h"
#include "models/osint_dns_query.h" #include "models/osint_dns_query.h"
#include "models/osint_dns_proposal.h" #include "models/osint_dns_proposal.h"
#include "models/osint_execution_record.h"
#include "core/investigation_tree_builder.h" #include "core/investigation_tree_builder.h"
#include "views/create_investigation_dialog.h" #include "views/create_investigation_dialog.h"
#include "views/folder_dialog.h" #include "views/folder_dialog.h"
@ -35,6 +36,7 @@
#include "views/osint_dns_review_dialog.h" #include "views/osint_dns_review_dialog.h"
#include "dao/evidence_dao.h" #include "dao/evidence_dao.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/osint_execution_dao.h"
#include "models/evidence_type.h" #include "models/evidence_type.h"
#include "widgets/evidence_list_model.h" #include "widgets/evidence_list_model.h"
#include "widgets/evidence_category_model.h" #include "widgets/evidence_category_model.h"
@ -164,6 +166,7 @@ typedef struct
Application *application; Application *application;
char *target_identifier; char *target_identifier;
char *target_value; char *target_value;
char *started_at;
} ApplicationOsintActionContext; } ApplicationOsintActionContext;
/** @brief Données possédées par l'étape de révision OSINT. */ /** @brief Données possédées par l'étape de révision OSINT. */
@ -171,6 +174,7 @@ typedef struct
{ {
Application *application; Application *application;
char *source_entity_identifier; char *source_entity_identifier;
char *execution_identifier;
GPtrArray *proposals; GPtrArray *proposals;
} ApplicationOsintReviewContext; } ApplicationOsintReviewContext;
@ -191,6 +195,7 @@ static void application_osint_review_context_free(gpointer user_data)
ApplicationOsintReviewContext *context = user_data; ApplicationOsintReviewContext *context = user_data;
if (context == NULL) return; if (context == NULL) return;
g_clear_pointer(&context->proposals, g_ptr_array_unref); g_clear_pointer(&context->proposals, g_ptr_array_unref);
g_free(context->execution_identifier);
g_free(context->source_entity_identifier); g_free(context->source_entity_identifier);
g_free(context); g_free(context);
} }
@ -227,6 +232,7 @@ static void application_on_osint_integration_confirmed(
? investigation_project_get_database_path(project) : NULL; ? investigation_project_get_database_path(project) : NULL;
if (!osint_dns_integration_apply( if (!osint_dns_integration_apply(
database, review_context->source_entity_identifier, database, review_context->source_entity_identifier,
review_context->execution_identifier,
selected_proposals, &inserted_count, &skipped_count, selected_proposals, &inserted_count, &skipped_count,
&inserted_relation_count, &skipped_relation_count, &error &inserted_relation_count, &skipped_relation_count, &error
)) ))
@ -244,10 +250,10 @@ static void application_on_osint_integration_confirmed(
message = g_strdup_printf( message = g_strdup_printf(
"%u entité(s) créée(s), %u entité(s) réutilisée(s) ou ignorée(s).\n" "%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" "%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 " "Les objets créés ou réutilisés sont liés à l'exécution OSINT %s.",
"du schéma OSINT.",
inserted_count, skipped_count, inserted_count, skipped_count,
inserted_relation_count, skipped_relation_count inserted_relation_count, skipped_relation_count,
review_context->execution_identifier
); );
application_message_dialog_present( application_message_dialog_present(
main_window_get_window(application->main_window), 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_value);
g_free(context->target_identifier); g_free(context->target_identifier);
g_free(context->started_at);
g_free(context); g_free(context);
} }
@ -1085,6 +1092,89 @@ static char *application_osint_output_to_utf8(
return g_utf8_make_valid(data, (gssize) data_size); 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. * @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 *stderr_text = NULL;
char *details = NULL; char *details = NULL;
char *message = NULL; char *message = NULL;
char *execution_identifier = NULL;
GPtrArray *proposals = NULL; GPtrArray *proposals = NULL;
ApplicationOsintReviewContext *review_context = NULL; ApplicationOsintReviewContext *review_context = NULL;
GError *error = NULL; GError *error = NULL;
const ToolInfo *dns_tool_info = NULL;
if (task == NULL || context == NULL || context->application == NULL) if (task == NULL || context == NULL || context->application == NULL)
{ {
@ -1118,19 +1210,38 @@ static void application_on_dns_lookup_completed(
return; 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) 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( application_message_dialog_present(
main_window_get_window(application->main_window), main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION, APPLICATION_MESSAGE_DIALOG_INFORMATION,
"Résolution DNS annulée", "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; return;
} }
if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) 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); error = background_task_dup_error(task);
application_present_error( application_present_error(
application, application,
@ -1139,6 +1250,7 @@ static void application_on_dns_lookup_completed(
? error->message ? error->message
: "L'exécution de dig a été annulée ou a échoué." : "L'exécution de dig a été annulée ou a échoué."
); );
g_free(execution_identifier);
g_clear_error(&error); g_clear_error(&error);
return; return;
} }
@ -1157,6 +1269,23 @@ static void application_on_dns_lookup_completed(
stdout_bytes = tool_process_result_ref_stdout(process_result); stdout_bytes = tool_process_result_ref_stdout(process_result);
stderr_bytes = tool_process_result_ref_stderr(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); stdout_text = application_osint_output_to_utf8(stdout_bytes);
stderr_text = application_osint_output_to_utf8(stderr_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( review_context->source_entity_identifier = g_strdup(
context->target_identifier context->target_identifier
); );
review_context->execution_identifier = g_strdup(
execution_identifier
);
review_context->proposals = g_ptr_array_ref(proposals); review_context->proposals = g_ptr_array_ref(proposals);
} }
if (review_context == NULL || review_context->proposals == NULL || 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); application_osint_review_context_free(review_context);
review_context = NULL; review_context = NULL;
@ -1204,10 +1337,17 @@ static void application_on_dns_lookup_completed(
} }
message = g_strdup_printf( message = g_strdup_printf(
"Cible : %s\nOutil : dns.dig\nCode de sortie : %d\n" "Cible : %s\nOutil : dns.dig%s%s\nDébut : %s\nCode de sortie : %d\n"
"Résultat brut non enregistré dans l'enquête.", "Exécution : %s\nSortie brute enregistrée avec son empreinte SHA-256.",
context->target_value, 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( application_message_dialog_present_details_action(
@ -1232,6 +1372,7 @@ static void application_on_dns_lookup_completed(
g_free(stderr_text); g_free(stderr_text);
g_free(details); g_free(details);
g_free(message); g_free(message);
g_free(execution_identifier);
g_clear_pointer(&proposals, g_ptr_array_unref); g_clear_pointer(&proposals, g_ptr_array_unref);
} }
@ -1294,10 +1435,11 @@ static void application_start_dns_lookup(
context->application = application; context->application = application;
context->target_identifier = g_strdup(target_identifier); context->target_identifier = g_strdup(target_identifier);
context->target_value = g_strdup(target_value); context->target_value = g_strdup(target_value);
context->started_at = application_osint_create_timestamp();
} }
if (context == NULL || context->target_identifier == NULL || 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); application_osint_action_context_free(context);
tool_task_free(tool_task); tool_task_free(tool_task);

View file

@ -7,6 +7,7 @@
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/relation_dao.h" #include "dao/relation_dao.h"
#include "dao/osint_execution_dao.h"
#include "database/error.h" #include "database/error.h"
#include "database/transaction.h" #include "database/transaction.h"
#include "models/entity_record.h" #include "models/entity_record.h"
@ -83,6 +84,7 @@ GQuark osint_dns_integration_error_quark(void)
gboolean osint_dns_integration_apply( gboolean osint_dns_integration_apply(
Database *database, Database *database,
const char *source_entity_identifier, const char *source_entity_identifier,
const char *execution_identifier,
GPtrArray *selected_proposals, GPtrArray *selected_proposals,
guint *out_inserted_entity_count, guint *out_inserted_entity_count,
guint *out_skipped_entity_count, guint *out_skipped_entity_count,
@ -93,6 +95,7 @@ gboolean osint_dns_integration_apply(
{ {
EntityDao *entity_dao = NULL; EntityDao *entity_dao = NULL;
RelationDao *relation_dao = NULL; RelationDao *relation_dao = NULL;
OsintExecutionDao *execution_dao = NULL;
GPtrArray *existing_entities = NULL; GPtrArray *existing_entities = NULL;
GPtrArray *existing_relations = NULL; GPtrArray *existing_relations = NULL;
GHashTable *known_entities = 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 (out_skipped_relation_count != NULL) *out_skipped_relation_count = 0U;
if (database == NULL || source_entity_identifier == NULL || if (database == NULL || source_entity_identifier == NULL ||
!g_uuid_string_is_valid(source_entity_identifier) || !g_uuid_string_is_valid(source_entity_identifier) ||
execution_identifier == NULL ||
!g_uuid_string_is_valid(execution_identifier) ||
selected_proposals == NULL || selected_proposals == NULL ||
selected_proposals->len == 0U) selected_proposals->len == 0U)
{ {
@ -127,6 +132,8 @@ gboolean osint_dns_integration_apply(
if (entity_dao == NULL) return FALSE; if (entity_dao == NULL) return FALSE;
relation_dao = relation_dao_new(database, error); relation_dao = relation_dao_new(database, error);
if (relation_dao == NULL) goto cleanup; 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( source_entity = entity_dao_find_by_identifier(
entity_dao, source_entity_identifier, error entity_dao, source_entity_identifier, error
); );
@ -164,20 +171,21 @@ gboolean osint_dns_integration_apply(
g_free(normalized_value); g_free(normalized_value);
} }
known_relations = g_hash_table_new_full( 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++) for (guint index = 0; index < existing_relations->len; index++)
{ {
const RelationRecord *relation = g_ptr_array_index( const RelationRecord *relation = g_ptr_array_index(
existing_relations, index existing_relations, index
); );
g_hash_table_add( g_hash_table_insert(
known_relations, known_relations,
osint_dns_integration_build_relation_key( osint_dns_integration_build_relation_key(
relation_record_get_source_entity_identifier(relation), relation_record_get_source_entity_identifier(relation),
relation_record_get_target_entity_identifier(relation), relation_record_get_target_entity_identifier(relation),
relation_record_get_relation_type(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_label = NULL;
char *relation_justification = NULL; char *relation_justification = NULL;
GError *model_error = NULL; GError *model_error = NULL;
gboolean entity_was_created = FALSE;
gboolean relation_was_created = FALSE;
if (entity_type == NULL || relation_type == NULL || if (entity_type == NULL || relation_type == NULL ||
normalized_value == NULL) normalized_value == NULL)
@ -271,6 +281,7 @@ gboolean osint_dns_integration_apply(
); );
entity_key = NULL; entity_key = NULL;
inserted_entity_count++; inserted_entity_count++;
entity_was_created = TRUE;
entity_record_free(entity); entity_record_free(entity);
g_clear_error(&model_error); g_clear_error(&model_error);
g_free(description); g_free(description);
@ -279,8 +290,11 @@ gboolean osint_dns_integration_apply(
relation_key = osint_dns_integration_build_relation_key( relation_key = osint_dns_integration_build_relation_key(
source_entity_identifier, target_identifier, relation_type 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 || if (g_strcmp0(source_entity_identifier, target_identifier) == 0 ||
g_hash_table_contains(known_relations, relation_key)) relation_identifier != NULL)
{ {
skipped_relation_count++; skipped_relation_count++;
} }
@ -321,16 +335,36 @@ gboolean osint_dns_integration_apply(
database_transaction_rollback(database); database_transaction_rollback(database);
goto cleanup; 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; relation_key = NULL;
inserted_relation_count++; inserted_relation_count++;
relation_was_created = TRUE;
relation_record_free(relation); relation_record_free(relation);
g_clear_error(&model_error); g_clear_error(&model_error);
g_free(relation_identifier);
g_free(relation_label); g_free(relation_label);
g_free(relation_justification); 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(relation_key);
g_free(target_identifier); g_free(target_identifier);
g_free(entity_key); g_free(entity_key);
@ -367,6 +401,7 @@ cleanup:
g_clear_pointer(&existing_relations, g_ptr_array_unref); g_clear_pointer(&existing_relations, g_ptr_array_unref);
entity_record_free(source_entity); entity_record_free(source_entity);
relation_dao_free(relation_dao); relation_dao_free(relation_dao);
osint_execution_dao_free(execution_dao);
entity_dao_free(entity_dao); entity_dao_free(entity_dao);
return success; return success;
} }

View file

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

View file

@ -16,12 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @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. * @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. * @brief Nom de l'application enregistré dans les métadonnées.
@ -668,6 +668,30 @@ rollback:
return false; 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 dannuler la migration SQLite V2 vers V3.");
return false;
}
/** /**
* @brief Garantit atomiquement la présence des extensions du schéma courant. * @brief Garantit atomiquement la présence des extensions du schéma courant.
*/ */
@ -854,6 +878,12 @@ bool database_migrate_to_latest(
schema_version = 2; schema_version = 2;
break; break;
case 2:
if (!database_migrate_v2_to_v3(database))
return false;
schema_version = 3;
break;
default: default:
database_set_error( database_set_error(
database, database,
@ -963,6 +993,13 @@ bool database_initialize(
goto rollback; goto rollback;
} }
if (!schema_install_v3(
database
))
{
goto rollback;
}
if (!schema_ensure_current( if (!schema_ensure_current(
database database
)) ))

View file

@ -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( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -172,6 +172,49 @@ bool database_statement_bind_text(
return true; 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( bool database_statement_bind_int64(
DatabaseStatement *statement, DatabaseStatement *statement,
int index, int index,
@ -666,6 +709,32 @@ bool database_statement_column_text(
return true; 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( bool database_statement_column_int64(
DatabaseStatement *statement, DatabaseStatement *statement,
int column_index, int column_index,

View file

@ -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 <string.h>
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; }

View file

@ -525,7 +525,10 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "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(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\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) 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); 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( assert(
database_migrate_to_latest( 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é. * Aucun ALTER TABLE, index ou trigger ne doit être rejoué.
*/ */
@ -984,7 +987,7 @@ static void test_database_migrate_v1_to_v2(void)
assert( assert(
strcmp( strcmp(
schema_version, schema_version,
"2" "3"
) == 0 ) == 0
); );

View file

@ -7,13 +7,49 @@
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/relation_dao.h" #include "dao/relation_dao.h"
#include "dao/osint_execution_dao.h"
#include "database/database.h" #include "database/database.h"
#include "database/statement.h"
#include "models/osint_dns_proposal.h" #include "models/osint_dns_proposal.h"
#include "models/relation_record.h" #include "models/relation_record.h"
#include "models/osint_execution_record.h"
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
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) static void test_integration_and_duplicates(void)
{ {
GError *error = NULL; GError *error = NULL;
@ -41,6 +77,7 @@ static void test_integration_and_duplicates(void)
"2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error "2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error
); );
g_assert_true(entity_dao_insert(entity_dao, source, &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( proposals = osint_dns_proposal_parse(
"example.org", "example.org",
"example.org. 60 IN A 192.0.2.10\n" "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" "example.org. 60 IN MX 10 mail.example.org.\n"
); );
g_assert_true(osint_dns_integration_apply( 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 &inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_no_error(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_cmpuint(skipped_relations, ==, 1);
g_assert_true(osint_dns_integration_apply( 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 &inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_cmpuint(inserted, ==, 0); g_assert_cmpuint(inserted, ==, 0);
@ -70,6 +109,12 @@ static void test_integration_and_duplicates(void)
relation_dao = relation_dao_new(database, &error); relation_dao = relation_dao_new(database, &error);
relations = relation_dao_list_all(relation_dao, &error); relations = relation_dao_list_all(relation_dao, &error);
g_assert_cmpuint(relations->len, ==, 2); 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); g_ptr_array_unref(relations);
relation_dao_free(relation_dao); relation_dao_free(relation_dao);
@ -88,7 +133,7 @@ static void test_invalid_arguments(void)
{ {
GError *error = NULL; GError *error = NULL;
g_assert_false(osint_dns_integration_apply( 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( g_assert_error(
error, OSINT_DNS_INTEGRATION_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 "2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error
); );
g_assert_true(entity_dao_insert(entity_dao, existing, &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( proposals = osint_dns_proposal_parse(
"example.org", "example.org. 60 IN CNAME www.example.org.\n" "example.org", "example.org. 60 IN CNAME www.example.org.\n"
); );
g_assert_true(osint_dns_integration_apply( 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 &inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_cmpuint(inserted, ==, 0); g_assert_cmpuint(inserted, ==, 0);

View file

@ -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 <glib.h>
#include <glib/gstdio.h>
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();
}

View file

@ -8,6 +8,7 @@
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <glib.h> #include <glib.h>
/** /**
@ -663,6 +664,31 @@ static void test_column_int64(void)
database_close(database); 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) int main(void)
{ {
test_prepare_valid_statement(); test_prepare_valid_statement();
@ -680,6 +706,7 @@ int main(void)
test_column_is_null(); test_column_is_null();
test_column_text(); test_column_text();
test_column_int64(); test_column_int64();
test_bind_and_read_blob();
printf( printf(
"DatabaseStatement : tous les tests sont valides.\n" "DatabaseStatement : tous les tests sont valides.\n"