From bc70257e0a5c1d18a8e5205ccde6e4d89eb410c7 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 11:20:06 +0200 Subject: [PATCH] feat(osint): add selection context --- Makefile | 15 ++- README.md | 3 +- include/models/osint_selection_context.h | 104 ++++++++++++++++ src/models/osint_selection_context.c | 152 +++++++++++++++++++++++ src/widgets/workspace.c | 47 +++++-- tests/test_osint_selection_context.c | 84 +++++++++++++ 6 files changed, 390 insertions(+), 15 deletions(-) create mode 100644 include/models/osint_selection_context.h create mode 100644 src/models/osint_selection_context.c create mode 100644 tests/test_osint_selection_context.c diff --git a/Makefile b/Makefile index 7869f24..c305cf7 100644 --- a/Makefile +++ b/Makefile @@ -113,6 +113,7 @@ TEST_INVESTIGATION_GRAPH_MODEL := tests/test_investigation_graph_model TEST_INVESTIGATION_GRAPH_LOADER := tests/test_investigation_graph_loader TEST_INVESTIGATION_GRAPH_LOAD_TASK := tests/test_investigation_graph_load_task TEST_GRAPH_NODE_POSITION_DAO := tests/test_graph_node_position_dao +TEST_OSINT_SELECTION_CONTEXT := tests/test_osint_selection_context all: $(TARGET) @@ -528,6 +529,13 @@ $(TEST_GRAPH_NODE_POSITION_DAO): \ $(CC) $(GRAPH_NODE_POSITION_DAO_TEST_CFLAGS) $^ -o $@ \ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_OSINT_SELECTION_CONTEXT): \ + tests/test_osint_selection_context.c \ + src/models/osint_selection_context.c \ + src/models/entity_record.c \ + src/models/relation_record.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) + $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \ tests/test_investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \ @@ -595,7 +603,8 @@ test: \ $(TEST_INVESTIGATION_GRAPH_MODEL) \ $(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ - $(TEST_GRAPH_NODE_POSITION_DAO) + $(TEST_GRAPH_NODE_POSITION_DAO) \ + $(TEST_OSINT_SELECTION_CONTEXT) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -643,6 +652,7 @@ test: \ @$(TEST_INVESTIGATION_GRAPH_LOADER) @$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) @$(TEST_GRAPH_NODE_POSITION_DAO) + @$(TEST_OSINT_SELECTION_CONTEXT) @echo "Tous les tests sont valides." %.o: %.c @@ -696,7 +706,8 @@ clean: $(TEST_INVESTIGATION_GRAPH_MODEL) \ $(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ - $(TEST_GRAPH_NODE_POSITION_DAO) + $(TEST_GRAPH_NODE_POSITION_DAO) \ + $(TEST_OSINT_SELECTION_CONTEXT) -include $(DEP) diff --git a/README.md b/README.md index dff85b1..6abca51 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,8 @@ Le socle actuel comprend notamment : - exécution d’outils en tâche de fond ; - catalogue initial d’outils ; - détection de présence et de version ; -- menu OSINT contextuel préparé pour les entités et relations du graphe. +- menu OSINT contextuel préparé pour les entités et relations du graphe ; +- contexte de sélection OSINT indépendant de GTK et validé par des tests. Les outils actuellement présents dans le catalogue initial sont : diff --git a/include/models/osint_selection_context.h b/include/models/osint_selection_context.h new file mode 100644 index 0000000..0d6fc15 --- /dev/null +++ b/include/models/osint_selection_context.h @@ -0,0 +1,104 @@ +/****************************************************************************** + * @file osint_selection_context.h + * @brief Contexte métier transmis aux futurs adaptateurs OSINT. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_OSINT_SELECTION_CONTEXT_H +#define LABFY_INVESTIGATION_OSINT_SELECTION_CONTEXT_H + +#include "models/entity_record.h" +#include "models/relation_record.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Nature de la cible sélectionnée pour un outil OSINT. + */ +typedef enum +{ + OSINT_SELECTION_CONTEXT_KIND_UNKNOWN, + OSINT_SELECTION_CONTEXT_KIND_ENTITY, + OSINT_SELECTION_CONTEXT_KIND_RELATION +} OsintSelectionContextKind; + +/** + * @brief Contexte OSINT opaque et indépendant de GTK. + */ +typedef struct OsintSelectionContext OsintSelectionContext; + +/** + * @brief Crée un contexte depuis une entité. + * + * Toutes les données utiles sont copiées. + * + * @param entity_record Entité empruntée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau contexte, ou NULL en cas d'échec. + */ +OsintSelectionContext *osint_selection_context_new_entity( + const EntityRecord *entity_record, + GError **error +); + +/** + * @brief Crée un contexte depuis une relation et ses extrémités. + * + * @param relation_record Relation empruntée. + * @param source_entity Entité source empruntée. + * @param target_entity Entité cible empruntée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau contexte, ou NULL en cas d'échec. + */ +OsintSelectionContext *osint_selection_context_new_relation( + const RelationRecord *relation_record, + const EntityRecord *source_entity, + const EntityRecord *target_entity, + GError **error +); + +/** + * @brief Libère un contexte OSINT. + * + * Cette fonction accepte NULL. + * + * @param context Contexte à libérer. + */ +void osint_selection_context_free(OsintSelectionContext *context); + +/** @brief Retourne la nature du contexte. */ +OsintSelectionContextKind osint_selection_context_get_kind( + const OsintSelectionContext *context +); + +/** @brief Retourne l'UUID copié de la cible. */ +const char *osint_selection_context_get_identifier( + const OsintSelectionContext *context +); + +/** @brief Retourne le type métier copié de la cible. */ +const char *osint_selection_context_get_type( + const OsintSelectionContext *context +); + +/** @brief Retourne la valeur principale copiée de la cible. */ +const char *osint_selection_context_get_value( + const OsintSelectionContext *context +); + +/** @brief Retourne la valeur source copiée d'une relation, ou NULL. */ +const char *osint_selection_context_get_source_value( + const OsintSelectionContext *context +); + +/** @brief Retourne la valeur cible copiée d'une relation, ou NULL. */ +const char *osint_selection_context_get_target_value( + const OsintSelectionContext *context +); + +G_END_DECLS + +#endif diff --git a/src/models/osint_selection_context.c b/src/models/osint_selection_context.c new file mode 100644 index 0000000..886b0e2 --- /dev/null +++ b/src/models/osint_selection_context.c @@ -0,0 +1,152 @@ +/****************************************************************************** + * @file osint_selection_context.c + * @brief Implémentation du contexte métier OSINT. + ******************************************************************************/ + +#include "models/osint_selection_context.h" + +#include + +struct OsintSelectionContext +{ + OsintSelectionContextKind kind; + char *identifier; + char *type; + char *value; + char *source_value; + char *target_value; +}; + +/** + * @brief Alloue et copie les champs communs d'un contexte. + */ +static OsintSelectionContext *osint_selection_context_new( + OsintSelectionContextKind kind, + const char *identifier, + const char *type, + const char *value, + GError **error +) +{ + OsintSelectionContext *context = NULL; + + if ((error != NULL && *error != NULL) || + (kind != OSINT_SELECTION_CONTEXT_KIND_ENTITY && + kind != OSINT_SELECTION_CONTEXT_KIND_RELATION) || + identifier == NULL || !g_uuid_string_is_valid(identifier) || + type == NULL || type[0] == '\0' || + value == NULL || value[0] == '\0') + { + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "Le contexte de sélection OSINT est invalide." + ); + return NULL; + } + + context = g_try_new0(OsintSelectionContext, 1); + if (context != NULL) + { + context->kind = kind; + context->identifier = g_strdup(identifier); + context->type = g_strdup(type); + context->value = g_strdup(value); + } + + if (context == NULL || context->identifier == NULL || + context->type == NULL || context->value == NULL) + { + osint_selection_context_free(context); + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_NO_SPACE, + "Impossible d'allouer le contexte de sélection OSINT." + ); + return NULL; + } + + return context; +} + +OsintSelectionContext *osint_selection_context_new_entity( + const EntityRecord *entity_record, + GError **error +) +{ + return osint_selection_context_new( + OSINT_SELECTION_CONTEXT_KIND_ENTITY, + entity_record_get_identifier(entity_record), + entity_record_get_type_identifier(entity_record), + entity_record_get_value(entity_record), + error + ); +} + +OsintSelectionContext *osint_selection_context_new_relation( + const RelationRecord *relation_record, + const EntityRecord *source_entity, + const EntityRecord *target_entity, + GError **error +) +{ + const char *label = relation_record_get_label(relation_record); + OsintSelectionContext *context = osint_selection_context_new( + OSINT_SELECTION_CONTEXT_KIND_RELATION, + relation_record_get_identifier(relation_record), + relation_record_get_relation_type(relation_record), + label != NULL && label[0] != '\0' + ? label + : relation_record_get_relation_type(relation_record), + error + ); + + if (context == NULL || source_entity == NULL || target_entity == NULL) + { + osint_selection_context_free(context); + if (context != NULL) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + "Les extrémités de la relation OSINT sont invalides."); + } + return NULL; + } + + context->source_value = g_strdup(entity_record_get_value(source_entity)); + context->target_value = g_strdup(entity_record_get_value(target_entity)); + if (context->source_value == NULL || context->target_value == NULL) + { + osint_selection_context_free(context); + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, + "Impossible de copier les extrémités de la relation OSINT."); + return NULL; + } + + return context; +} + +void osint_selection_context_free(OsintSelectionContext *context) +{ + if (context == NULL) return; + g_free(context->target_value); + g_free(context->source_value); + g_free(context->value); + g_free(context->type); + g_free(context->identifier); + g_free(context); +} + +OsintSelectionContextKind osint_selection_context_get_kind(const OsintSelectionContext *context) +{ return context != NULL ? context->kind : OSINT_SELECTION_CONTEXT_KIND_UNKNOWN; } +const char *osint_selection_context_get_identifier(const OsintSelectionContext *context) +{ return context != NULL ? context->identifier : NULL; } +const char *osint_selection_context_get_type(const OsintSelectionContext *context) +{ return context != NULL ? context->type : NULL; } +const char *osint_selection_context_get_value(const OsintSelectionContext *context) +{ return context != NULL ? context->value : NULL; } +const char *osint_selection_context_get_source_value(const OsintSelectionContext *context) +{ return context != NULL ? context->source_value : NULL; } +const char *osint_selection_context_get_target_value(const OsintSelectionContext *context) +{ return context != NULL ? context->target_value : NULL; } diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index d4b0f17..52f2cbe 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -7,6 +7,7 @@ #include "models/entity_record.h" #include "models/investigation_graph_model.h" +#include "models/osint_selection_context.h" #include "models/relation_record.h" #include "widgets/entity_details_panel.h" #include "widgets/investigation_graph_view.h" @@ -118,6 +119,8 @@ struct Workspace const InvestigationGraphModel *graph_model; const InvestigationGraphLayout *graph_layout; + OsintSelectionContext *osint_selection_context; + WorkspaceGraphState graph_state; }; @@ -129,8 +132,7 @@ struct Workspace */ static void workspace_update_osint_tools_menu( Workspace *workspace, - const EntityRecord *entity_record, - const RelationRecord *relation_record + const OsintSelectionContext *context ) { const char *selection_kind = @@ -150,25 +152,23 @@ static void workspace_update_osint_tools_menu( return; } - if (entity_record != NULL) + if (osint_selection_context_get_kind(context) == + OSINT_SELECTION_CONTEXT_KIND_ENTITY) { selection_kind = "Entité"; selection_identifier = - entity_record_get_identifier( - entity_record - ); + osint_selection_context_get_identifier(context); } - else if (relation_record != NULL) + else if (osint_selection_context_get_kind(context) == + OSINT_SELECTION_CONTEXT_KIND_RELATION) { selection_kind = "Relation"; selection_identifier = - relation_record_get_identifier( - relation_record - ); + osint_selection_context_get_identifier(context); } gtk_widget_set_sensitive( @@ -438,6 +438,17 @@ static void workspace_on_graph_node_selected( return; } + g_clear_pointer( + &workspace->osint_selection_context, + osint_selection_context_free + ); + + if (entity_record != NULL) + { + workspace->osint_selection_context = + osint_selection_context_new_entity(entity_record, NULL); + } + if (workspace->entity_details_panel != NULL) { entity_details_panel_set_entity( @@ -468,6 +479,14 @@ static void workspace_on_graph_node_selected( workspace->graph_model, relation_record_get_target_entity_identifier(relation_record) ); + + workspace->osint_selection_context = + osint_selection_context_new_relation( + relation_record, + source_entity, + target_entity, + NULL + ); const char *label = relation_record_get_label(relation_record); char *summary = g_strdup_printf( "%s → %s\n\nType : %s\nConfiance : %d %%\nUUID : %s", @@ -491,8 +510,7 @@ static void workspace_on_graph_node_selected( workspace_update_osint_tools_menu( workspace, - entity_record, - relation_record + workspace->osint_selection_context ); } @@ -2614,6 +2632,11 @@ void workspace_free(Workspace *workspace) return; } + g_clear_pointer( + &workspace->osint_selection_context, + osint_selection_context_free + ); + investigation_graph_view_set_selection_callback( workspace->graph_view, NULL, diff --git a/tests/test_osint_selection_context.c b/tests/test_osint_selection_context.c new file mode 100644 index 0000000..2d0e9cf --- /dev/null +++ b/tests/test_osint_selection_context.c @@ -0,0 +1,84 @@ +#include "models/osint_selection_context.h" + +#include + +static EntityRecord *create_entity(const char *identifier, const char *value) +{ + return entity_record_new( + identifier, "domain", value, NULL, NULL, 80, + "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z", + ENTITY_STATUS_ACTIVE, NULL + ); +} + +static void test_entity_context(void) +{ + EntityRecord *entity = create_entity( + "11111111-1111-4111-8111-111111111111", "example.org" + ); + OsintSelectionContext *context = + osint_selection_context_new_entity(entity, NULL); + + g_assert_nonnull(context); + g_assert_cmpint(osint_selection_context_get_kind(context), ==, + OSINT_SELECTION_CONTEXT_KIND_ENTITY); + g_assert_cmpstr(osint_selection_context_get_type(context), ==, "domain"); + g_assert_cmpstr(osint_selection_context_get_value(context), ==, "example.org"); + g_assert_null(osint_selection_context_get_source_value(context)); + + entity_record_free(entity); + g_assert_cmpstr(osint_selection_context_get_value(context), ==, "example.org"); + osint_selection_context_free(context); +} + +static void test_relation_context(void) +{ + EntityRecord *source = create_entity( + "11111111-1111-4111-8111-111111111111", "source.example" + ); + EntityRecord *target = create_entity( + "22222222-2222-4222-8222-222222222222", "target.example" + ); + RelationRecord *relation = relation_record_new( + "33333333-3333-4333-8333-333333333333", + entity_record_get_identifier(source), entity_record_get_identifier(target), + "redirects_to", "Redirection", NULL, 90, + "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z", + RELATION_STATUS_ACTIVE, NULL + ); + OsintSelectionContext *context = osint_selection_context_new_relation( + relation, source, target, NULL + ); + + g_assert_nonnull(context); + g_assert_cmpint(osint_selection_context_get_kind(context), ==, + OSINT_SELECTION_CONTEXT_KIND_RELATION); + g_assert_cmpstr(osint_selection_context_get_value(context), ==, "Redirection"); + g_assert_cmpstr(osint_selection_context_get_source_value(context), ==, + "source.example"); + g_assert_cmpstr(osint_selection_context_get_target_value(context), ==, + "target.example"); + + osint_selection_context_free(context); + relation_record_free(relation); + entity_record_free(target); + entity_record_free(source); +} + +static void test_invalid_context(void) +{ + GError *error = NULL; + g_assert_null(osint_selection_context_new_entity(NULL, &error)); + g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT); + g_clear_error(&error); + osint_selection_context_free(NULL); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/osint-selection-context/entity", test_entity_context); + g_test_add_func("/osint-selection-context/relation", test_relation_context); + g_test_add_func("/osint-selection-context/invalid", test_invalid_context); + return g_test_run(); +}