feat(osint): add compatible action catalog

This commit is contained in:
grayTerminal-sh 2026-07-22 11:28:13 +02:00
parent bc70257e0a
commit 947f9550dc
6 changed files with 438 additions and 6 deletions

View file

@ -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_INVESTIGATION_GRAPH_LOAD_TASK := tests/test_investigation_graph_load_task
TEST_GRAPH_NODE_POSITION_DAO := tests/test_graph_node_position_dao TEST_GRAPH_NODE_POSITION_DAO := tests/test_graph_node_position_dao
TEST_OSINT_SELECTION_CONTEXT := tests/test_osint_selection_context TEST_OSINT_SELECTION_CONTEXT := tests/test_osint_selection_context
TEST_OSINT_ACTION_CATALOG := tests/test_osint_action_catalog
all: $(TARGET) all: $(TARGET)
@ -536,6 +537,14 @@ $(TEST_OSINT_SELECTION_CONTEXT): \
src/models/relation_record.c src/models/relation_record.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(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): \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
tests/test_investigation_graph_load_task.c \ tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \ src/core/investigation_graph_load_task.c \
@ -604,7 +613,8 @@ test: \
$(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOADER) \
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \
$(TEST_GRAPH_NODE_POSITION_DAO) \ $(TEST_GRAPH_NODE_POSITION_DAO) \
$(TEST_OSINT_SELECTION_CONTEXT) $(TEST_OSINT_SELECTION_CONTEXT) \
$(TEST_OSINT_ACTION_CATALOG)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -653,6 +663,7 @@ test: \
@$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) @$(TEST_INVESTIGATION_GRAPH_LOAD_TASK)
@$(TEST_GRAPH_NODE_POSITION_DAO) @$(TEST_GRAPH_NODE_POSITION_DAO)
@$(TEST_OSINT_SELECTION_CONTEXT) @$(TEST_OSINT_SELECTION_CONTEXT)
@$(TEST_OSINT_ACTION_CATALOG)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -707,7 +718,8 @@ clean:
$(TEST_INVESTIGATION_GRAPH_LOADER) \ $(TEST_INVESTIGATION_GRAPH_LOADER) \
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \ $(TEST_INVESTIGATION_GRAPH_LOAD_TASK) \
$(TEST_GRAPH_NODE_POSITION_DAO) \ $(TEST_GRAPH_NODE_POSITION_DAO) \
$(TEST_OSINT_SELECTION_CONTEXT) $(TEST_OSINT_SELECTION_CONTEXT) \
$(TEST_OSINT_ACTION_CATALOG)
-include $(DEP) -include $(DEP)

View file

@ -142,7 +142,8 @@ Le socle actuel comprend notamment :
- catalogue initial doutils ; - catalogue initial doutils ;
- détection de présence et de version ; - 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. - 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 : Les outils actuellement présents dans le catalogue initial sont :

View file

@ -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 <glib.h>
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

View file

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

View file

@ -8,6 +8,7 @@
#include "models/entity_record.h" #include "models/entity_record.h"
#include "models/investigation_graph_model.h" #include "models/investigation_graph_model.h"
#include "models/osint_selection_context.h" #include "models/osint_selection_context.h"
#include "models/osint_action_catalog.h"
#include "models/relation_record.h" #include "models/relation_record.h"
#include "widgets/entity_details_panel.h" #include "widgets/entity_details_panel.h"
#include "widgets/investigation_graph_view.h" #include "widgets/investigation_graph_view.h"
@ -57,6 +58,7 @@ struct Workspace
GtkWidget *osint_tools_popover; GtkWidget *osint_tools_popover;
GtkWidget *osint_tools_context_label; GtkWidget *osint_tools_context_label;
GtkWidget *osint_tools_status_label; GtkWidget *osint_tools_status_label;
GtkWidget *osint_tools_actions_box;
GtkWidget *graph_help_button; GtkWidget *graph_help_button;
GtkWidget *graph_help_popover; GtkWidget *graph_help_popover;
@ -120,10 +122,42 @@ struct Workspace
const InvestigationGraphLayout *graph_layout; const InvestigationGraphLayout *graph_layout;
OsintSelectionContext *osint_selection_context; OsintSelectionContext *osint_selection_context;
OsintActionCatalog *osint_action_catalog;
WorkspaceGraphState graph_state; 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. * @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 = char *context_text =
NULL; NULL;
GPtrArray *compatible_actions =
NULL;
GtkWidget *child =
NULL;
if (workspace == NULL || if (workspace == NULL ||
workspace->osint_tools_button == NULL || workspace->osint_tools_button == NULL ||
workspace->osint_tools_context_label == 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; 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) == if (osint_selection_context_get_kind(context) ==
OSINT_SELECTION_CONTEXT_KIND_ENTITY) OSINT_SELECTION_CONTEXT_KIND_ENTITY)
{ {
@ -197,6 +245,56 @@ static void workspace_update_osint_tools_menu(
return; 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 = context_text =
g_strdup_printf( g_strdup_printf(
"%s sélectionnée\n%s", "%s sélectionnée\n%s",
@ -217,12 +315,16 @@ static void workspace_update_osint_tools_menu(
GTK_LABEL( GTK_LABEL(
workspace->osint_tools_status_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( g_free(
context_text 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." "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 || if (workspace->graph_toolbar == NULL ||
workspace->reset_graph_layout_button == NULL || workspace->reset_graph_layout_button == NULL ||
workspace->osint_tools_button == NULL || workspace->osint_tools_button == NULL ||
@ -1406,7 +1516,9 @@ Workspace *workspace_new(void)
osint_tools_content == NULL || osint_tools_content == NULL ||
osint_tools_title == NULL || osint_tools_title == NULL ||
workspace->osint_tools_context_label == 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_free(
workspace workspace
@ -1549,6 +1661,11 @@ Workspace *workspace_new(void)
workspace->osint_tools_context_label workspace->osint_tools_context_label
); );
gtk_box_append(
GTK_BOX(osint_tools_content),
workspace->osint_tools_actions_box
);
gtk_box_append( gtk_box_append(
GTK_BOX( GTK_BOX(
osint_tools_content osint_tools_content
@ -2637,6 +2754,11 @@ void workspace_free(Workspace *workspace)
osint_selection_context_free osint_selection_context_free
); );
g_clear_pointer(
&workspace->osint_action_catalog,
osint_action_catalog_free
);
investigation_graph_view_set_selection_callback( investigation_graph_view_set_selection_callback(
workspace->graph_view, workspace->graph_view,
NULL, NULL,

View file

@ -0,0 +1,69 @@
#include "models/osint_action_catalog.h"
#include <glib.h>
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();
}