From 947f9550dcc4283bc773b54c6f029751b44d305c Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Wed, 22 Jul 2026 11:28:13 +0200 Subject: [PATCH] feat(osint): add compatible action catalog --- Makefile | 16 ++- README.md | 3 +- include/models/osint_action_catalog.h | 73 ++++++++++++ src/models/osint_action_catalog.c | 155 ++++++++++++++++++++++++++ src/widgets/workspace.c | 128 ++++++++++++++++++++- tests/test_osint_action_catalog.c | 69 ++++++++++++ 6 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 include/models/osint_action_catalog.h create mode 100644 src/models/osint_action_catalog.c create mode 100644 tests/test_osint_action_catalog.c diff --git a/Makefile b/Makefile index c305cf7..b364a03 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ 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 +TEST_OSINT_ACTION_CATALOG := tests/test_osint_action_catalog all: $(TARGET) @@ -536,6 +537,14 @@ $(TEST_OSINT_SELECTION_CONTEXT): \ src/models/relation_record.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_OSINT_ACTION_CATALOG): \ + tests/test_osint_action_catalog.c \ + src/models/osint_action_catalog.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 \ @@ -604,7 +613,8 @@ test: \ $(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ $(TEST_GRAPH_NODE_POSITION_DAO) \ - $(TEST_OSINT_SELECTION_CONTEXT) + $(TEST_OSINT_SELECTION_CONTEXT) \ + $(TEST_OSINT_ACTION_CATALOG) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -653,6 +663,7 @@ test: \ @$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) @$(TEST_GRAPH_NODE_POSITION_DAO) @$(TEST_OSINT_SELECTION_CONTEXT) + @$(TEST_OSINT_ACTION_CATALOG) @echo "Tous les tests sont valides." %.o: %.c @@ -707,7 +718,8 @@ clean: $(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ $(TEST_GRAPH_NODE_POSITION_DAO) \ - $(TEST_OSINT_SELECTION_CONTEXT) + $(TEST_OSINT_SELECTION_CONTEXT) \ + $(TEST_OSINT_ACTION_CATALOG) -include $(DEP) diff --git a/README.md b/README.md index 6abca51..e5738c2 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,8 @@ Le socle actuel comprend notamment : - 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 ; -- contexte de sélection OSINT indépendant de GTK et validé par des tests. +- contexte de sélection OSINT indépendant de GTK et validé par des tests ; +- catalogue déterministe des actions compatibles avec la sélection OSINT. Les outils actuellement présents dans le catalogue initial sont : diff --git a/include/models/osint_action_catalog.h b/include/models/osint_action_catalog.h new file mode 100644 index 0000000..41f646b --- /dev/null +++ b/include/models/osint_action_catalog.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * @file osint_action_catalog.h + * @brief Catalogue des actions compatibles avec une sélection OSINT. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_OSINT_ACTION_CATALOG_H +#define LABFY_INVESTIGATION_OSINT_ACTION_CATALOG_H + +#include "models/osint_selection_context.h" + +#include + +G_BEGIN_DECLS + +/** @brief Description opaque d'une action OSINT. */ +typedef struct OsintAction OsintAction; + +/** @brief Catalogue opaque possédant ses actions. */ +typedef struct OsintActionCatalog OsintActionCatalog; + +/** + * @brief Crée le catalogue initial déterministe. + * + * @return Nouveau catalogue, ou NULL en cas d'échec. + */ +OsintActionCatalog *osint_action_catalog_new_defaults(void); + +/** + * @brief Libère un catalogue et toutes ses actions. + * + * @param catalog Catalogue à libérer, ou NULL. + */ +void osint_action_catalog_free(OsintActionCatalog *catalog); + +/** + * @brief Liste les actions compatibles avec un contexte. + * + * Le tableau appartient à l'appelant. Ses actions sont empruntées au catalogue + * et triées par libellé croissant. + * + * @param catalog Catalogue à consulter. + * @param context Contexte de sélection à comparer. + * + * @return Nouveau tableau, éventuellement vide, ou NULL si un argument est invalide. + */ +GPtrArray *osint_action_catalog_list_compatible( + const OsintActionCatalog *catalog, + const OsintSelectionContext *context +); + +/** @brief Retourne l'identifiant stable emprunté d'une action. */ +const char *osint_action_get_identifier(const OsintAction *action); + +/** @brief Retourne le libellé emprunté d'une action. */ +const char *osint_action_get_label(const OsintAction *action); + +/** @brief Retourne la description empruntée d'une action. */ +const char *osint_action_get_description(const OsintAction *action); + +/** @brief Retourne l'identifiant de l'outil externe requis, ou NULL. */ +const char *osint_action_get_required_tool_identifier( + const OsintAction *action +); + +/** @brief Indique si l'action peut actuellement être déclenchée. */ +gboolean osint_action_is_available(const OsintAction *action); + +/** @brief Retourne l'explication d'indisponibilité, ou NULL. */ +const char *osint_action_get_unavailable_reason(const OsintAction *action); + +G_END_DECLS + +#endif diff --git a/src/models/osint_action_catalog.c b/src/models/osint_action_catalog.c new file mode 100644 index 0000000..5d9d29e --- /dev/null +++ b/src/models/osint_action_catalog.c @@ -0,0 +1,155 @@ +/****************************************************************************** + * @file osint_action_catalog.c + * @brief Implémentation du catalogue des actions OSINT. + ******************************************************************************/ + +#include "models/osint_action_catalog.h" + +struct OsintAction +{ + char *identifier; + char *label; + char *description; + OsintSelectionContextKind target_kind; + char *compatible_type; + char *required_tool_identifier; + gboolean available; + char *unavailable_reason; +}; + +struct OsintActionCatalog +{ + GPtrArray *actions; +}; + +/** @brief Libère une action possédée par le catalogue. */ +static void osint_action_free(gpointer data) +{ + OsintAction *action = data; + if (action == NULL) return; + g_free(action->unavailable_reason); + g_free(action->compatible_type); + g_free(action->required_tool_identifier); + g_free(action->description); + g_free(action->label); + g_free(action->identifier); + g_free(action); +} + +/** @brief Crée une action en copiant toutes ses chaînes. */ +static OsintAction *osint_action_new( + const char *identifier, + const char *label, + const char *description, + OsintSelectionContextKind target_kind, + const char *compatible_type, + const char *required_tool_identifier, + gboolean available, + const char *unavailable_reason +) +{ + OsintAction *action = g_try_new0(OsintAction, 1); + if (action == NULL) return NULL; + action->identifier = g_strdup(identifier); + action->label = g_strdup(label); + action->description = g_strdup(description); + action->target_kind = target_kind; + action->compatible_type = g_strdup(compatible_type); + action->required_tool_identifier = g_strdup(required_tool_identifier); + action->available = available; + action->unavailable_reason = g_strdup(unavailable_reason); + if (action->identifier == NULL || action->label == NULL || + action->description == NULL || + (!available && action->unavailable_reason == NULL)) + { + osint_action_free(action); + return NULL; + } + return action; +} + +OsintActionCatalog *osint_action_catalog_new_defaults(void) +{ + OsintActionCatalog *catalog = g_try_new0(OsintActionCatalog, 1); + OsintAction *demonstration_action = NULL; + OsintAction *dns_action = NULL; + if (catalog == NULL) return NULL; + catalog->actions = g_ptr_array_new_with_free_func(osint_action_free); + demonstration_action = osint_action_new( + "selection-preview", "Aperçu de la sélection", + "Action locale de démonstration sans exécution externe.", + OSINT_SELECTION_CONTEXT_KIND_UNKNOWN, NULL, NULL, TRUE, NULL + ); + dns_action = osint_action_new( + "dns-preview", "Résolution DNS", + "Préfiguration de l'adaptateur DNS.", + OSINT_SELECTION_CONTEXT_KIND_ENTITY, "domain", "dig", FALSE, + "L'adaptateur DNS n'est pas encore intégré." + ); + if (catalog->actions == NULL || demonstration_action == NULL || + dns_action == NULL) + { + osint_action_free(demonstration_action); + osint_action_free(dns_action); + osint_action_catalog_free(catalog); + return NULL; + } + g_ptr_array_add(catalog->actions, demonstration_action); + g_ptr_array_add(catalog->actions, dns_action); + return catalog; +} + +/** @brief Compare deux actions par libellé. */ +static gint osint_action_compare(gconstpointer first, gconstpointer second) +{ + const OsintAction *const *a = first; + const OsintAction *const *b = second; + return g_strcmp0((*a)->label, (*b)->label); +} + +GPtrArray *osint_action_catalog_list_compatible( + const OsintActionCatalog *catalog, + const OsintSelectionContext *context +) +{ + GPtrArray *compatible_actions = NULL; + guint index = 0; + if (catalog == NULL || context == NULL || catalog->actions == NULL) + return NULL; + compatible_actions = g_ptr_array_new(); + if (compatible_actions == NULL) return NULL; + for (index = 0; index < catalog->actions->len; index++) + { + OsintAction *action = g_ptr_array_index(catalog->actions, index); + gboolean kind_matches = action->target_kind == + OSINT_SELECTION_CONTEXT_KIND_UNKNOWN || + action->target_kind == osint_selection_context_get_kind(context); + gboolean type_matches = action->compatible_type == NULL || + g_strcmp0(action->compatible_type, + osint_selection_context_get_type(context)) == 0; + if (kind_matches && type_matches) + g_ptr_array_add(compatible_actions, action); + } + g_ptr_array_sort(compatible_actions, osint_action_compare); + return compatible_actions; +} + +void osint_action_catalog_free(OsintActionCatalog *catalog) +{ + if (catalog == NULL) return; + g_clear_pointer(&catalog->actions, g_ptr_array_unref); + g_free(catalog); +} + +const char *osint_action_get_identifier(const OsintAction *action) +{ return action != NULL ? action->identifier : NULL; } +const char *osint_action_get_label(const OsintAction *action) +{ return action != NULL ? action->label : NULL; } +const char *osint_action_get_description(const OsintAction *action) +{ return action != NULL ? action->description : NULL; } +const char *osint_action_get_required_tool_identifier(const OsintAction *action) +{ return action != NULL ? action->required_tool_identifier : NULL; } +gboolean osint_action_is_available(const OsintAction *action) +{ return action != NULL && action->available; } +const char *osint_action_get_unavailable_reason(const OsintAction *action) +{ return action != NULL ? action->unavailable_reason : NULL; } diff --git a/src/widgets/workspace.c b/src/widgets/workspace.c index 52f2cbe..0870e80 100644 --- a/src/widgets/workspace.c +++ b/src/widgets/workspace.c @@ -8,6 +8,7 @@ #include "models/entity_record.h" #include "models/investigation_graph_model.h" #include "models/osint_selection_context.h" +#include "models/osint_action_catalog.h" #include "models/relation_record.h" #include "widgets/entity_details_panel.h" #include "widgets/investigation_graph_view.h" @@ -57,6 +58,7 @@ struct Workspace GtkWidget *osint_tools_popover; GtkWidget *osint_tools_context_label; GtkWidget *osint_tools_status_label; + GtkWidget *osint_tools_actions_box; GtkWidget *graph_help_button; GtkWidget *graph_help_popover; @@ -120,10 +122,42 @@ struct Workspace const InvestigationGraphLayout *graph_layout; OsintSelectionContext *osint_selection_context; + OsintActionCatalog *osint_action_catalog; WorkspaceGraphState graph_state; }; +/** + * @brief Confirme localement le déclenchement de l'action de démonstration. + */ +static void workspace_on_osint_demo_action_clicked( + GtkButton *button, + gpointer user_data +) +{ + Workspace *workspace = user_data; + const char *action_label = g_object_get_data( + G_OBJECT(button), + "osint-action-label" + ); + char *status_text = NULL; + + if (workspace == NULL || workspace->osint_tools_status_label == NULL) + { + return; + } + + status_text = g_strdup_printf( + "%s : démonstration locale, aucun outil exécuté.", + action_label != NULL ? action_label : "Action OSINT" + ); + gtk_label_set_text( + GTK_LABEL(workspace->osint_tools_status_label), + status_text != NULL ? status_text : "Aucun outil exécuté." + ); + g_free(status_text); +} + /** * @brief Actualise le menu OSINT selon la sélection du graphe. * @@ -144,14 +178,28 @@ static void workspace_update_osint_tools_menu( char *context_text = NULL; + GPtrArray *compatible_actions = + NULL; + + GtkWidget *child = + NULL; + if (workspace == NULL || workspace->osint_tools_button == NULL || workspace->osint_tools_context_label == NULL || - workspace->osint_tools_status_label == NULL) + workspace->osint_tools_status_label == NULL || + workspace->osint_tools_actions_box == NULL) { return; } + while ((child = gtk_widget_get_first_child( + workspace->osint_tools_actions_box + )) != NULL) + { + gtk_box_remove(GTK_BOX(workspace->osint_tools_actions_box), child); + } + if (osint_selection_context_get_kind(context) == OSINT_SELECTION_CONTEXT_KIND_ENTITY) { @@ -197,6 +245,56 @@ static void workspace_update_osint_tools_menu( return; } + compatible_actions = osint_action_catalog_list_compatible( + workspace->osint_action_catalog, + context + ); + + for (guint action_index = 0; + compatible_actions != NULL && + action_index < compatible_actions->len; + action_index++) + { + const OsintAction *action = g_ptr_array_index( + compatible_actions, + action_index + ); + GtkWidget *action_button = gtk_button_new_with_label( + osint_action_get_label(action) + ); + + if (action_button == NULL) + { + continue; + } + + gtk_widget_set_sensitive( + action_button, + osint_action_is_available(action) + ); + gtk_widget_set_tooltip_text( + action_button, + osint_action_is_available(action) + ? osint_action_get_description(action) + : osint_action_get_unavailable_reason(action) + ); + g_object_set_data( + G_OBJECT(action_button), + "osint-action-label", + (gpointer) osint_action_get_label(action) + ); + g_signal_connect( + action_button, + "clicked", + G_CALLBACK(workspace_on_osint_demo_action_clicked), + workspace + ); + gtk_box_append( + GTK_BOX(workspace->osint_tools_actions_box), + action_button + ); + } + context_text = g_strdup_printf( "%s sélectionnée\n%s", @@ -217,12 +315,16 @@ static void workspace_update_osint_tools_menu( GTK_LABEL( workspace->osint_tools_status_label ), - "Aucun outil OSINT intégré pour le moment." + compatible_actions != NULL && compatible_actions->len > 0U + ? "Actions compatibles avec la sélection." + : "Aucune action OSINT compatible." ); g_free( context_text ); + + g_clear_pointer(&compatible_actions, g_ptr_array_unref); } /** @@ -1399,6 +1501,14 @@ Workspace *workspace_new(void) "Les outils compatibles apparaîtront ici." ); + workspace->osint_tools_actions_box = gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 6 + ); + + workspace->osint_action_catalog = + osint_action_catalog_new_defaults(); + if (workspace->graph_toolbar == NULL || workspace->reset_graph_layout_button == NULL || workspace->osint_tools_button == NULL || @@ -1406,7 +1516,9 @@ Workspace *workspace_new(void) osint_tools_content == NULL || osint_tools_title == NULL || workspace->osint_tools_context_label == NULL || - workspace->osint_tools_status_label == NULL) + workspace->osint_tools_status_label == NULL || + workspace->osint_tools_actions_box == NULL || + workspace->osint_action_catalog == NULL) { workspace_free( workspace @@ -1549,6 +1661,11 @@ Workspace *workspace_new(void) workspace->osint_tools_context_label ); + gtk_box_append( + GTK_BOX(osint_tools_content), + workspace->osint_tools_actions_box + ); + gtk_box_append( GTK_BOX( osint_tools_content @@ -2637,6 +2754,11 @@ void workspace_free(Workspace *workspace) osint_selection_context_free ); + g_clear_pointer( + &workspace->osint_action_catalog, + osint_action_catalog_free + ); + investigation_graph_view_set_selection_callback( workspace->graph_view, NULL, diff --git a/tests/test_osint_action_catalog.c b/tests/test_osint_action_catalog.c new file mode 100644 index 0000000..e816634 --- /dev/null +++ b/tests/test_osint_action_catalog.c @@ -0,0 +1,69 @@ +#include "models/osint_action_catalog.h" + +#include + +static OsintSelectionContext *create_entity_context(const char *type) +{ + EntityRecord *entity = entity_record_new( + "11111111-1111-4111-8111-111111111111", type, "example.org", + NULL, NULL, 80, "2026-01-01T00:00:00Z", + "2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, NULL + ); + OsintSelectionContext *context = + osint_selection_context_new_entity(entity, NULL); + entity_record_free(entity); + return context; +} + +static void test_domain_actions(void) +{ + OsintActionCatalog *catalog = osint_action_catalog_new_defaults(); + OsintSelectionContext *context = create_entity_context("domain"); + GPtrArray *actions = osint_action_catalog_list_compatible(catalog, context); + + g_assert_nonnull(actions); + g_assert_cmpuint(actions->len, ==, 2U); + g_assert_cmpstr(osint_action_get_identifier(g_ptr_array_index(actions, 0)), + ==, "selection-preview"); + g_assert_true(osint_action_is_available(g_ptr_array_index(actions, 0))); + g_assert_false(osint_action_is_available(g_ptr_array_index(actions, 1))); + g_assert_cmpstr(osint_action_get_required_tool_identifier( + g_ptr_array_index(actions, 1)), ==, "dig"); + g_assert_nonnull(osint_action_get_unavailable_reason( + g_ptr_array_index(actions, 1))); + + g_ptr_array_unref(actions); + osint_selection_context_free(context); + osint_action_catalog_free(catalog); +} + +static void test_non_domain_actions(void) +{ + OsintActionCatalog *catalog = osint_action_catalog_new_defaults(); + OsintSelectionContext *context = create_entity_context("person"); + GPtrArray *actions = osint_action_catalog_list_compatible(catalog, context); + g_assert_cmpuint(actions->len, ==, 1U); + g_assert_cmpstr(osint_action_get_identifier(g_ptr_array_index(actions, 0)), + ==, "selection-preview"); + g_ptr_array_unref(actions); + osint_selection_context_free(context); + osint_action_catalog_free(catalog); +} + +static void test_invalid_arguments(void) +{ + OsintActionCatalog *catalog = osint_action_catalog_new_defaults(); + g_assert_null(osint_action_catalog_list_compatible(catalog, NULL)); + g_assert_null(osint_action_catalog_list_compatible(NULL, NULL)); + osint_action_catalog_free(catalog); + osint_action_catalog_free(NULL); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/osint-action-catalog/domain", test_domain_actions); + g_test_add_func("/osint-action-catalog/non-domain", test_non_domain_actions); + g_test_add_func("/osint-action-catalog/invalid", test_invalid_arguments); + return g_test_run(); +}