feat(osint): integrate public username and email research tools

This commit is contained in:
grayTerminal-sh 2026-07-23 11:53:48 +02:00
parent 832e3eca32
commit 4e97d0979a
8 changed files with 402 additions and 3 deletions

View file

@ -69,6 +69,9 @@ typedef void (*EntityDetailsPanelPersonNameCallback)(
/** @brief Callback appelé pour gérer les preuves d'une personne. */
typedef void (*EntityDetailsPanelPersonEvidenceCallback)(
const char *entity_identifier, gpointer user_data);
/** @brief Callback appelé pour ouvrir les recherches OSINT compatibles. */
typedef void (*EntityDetailsPanelOsintCallback)(
const char *entity_identifier, gpointer user_data);
/**
* @brief Crée un volet de détails fermé.
@ -175,6 +178,9 @@ void entity_details_panel_set_person_name_callback(
void entity_details_panel_set_person_evidence_callback(
EntityDetailsPanel *details_panel,
EntityDetailsPanelPersonEvidenceCallback callback, gpointer user_data);
/** @brief Définit le callback du bouton de recherche OSINT. */
void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
EntityDetailsPanelOsintCallback callback, gpointer user_data);
/**
* @brief Affiche les preuves associées à la personne courante.
* @param details_panel Volet à mettre à jour.

View file

@ -218,6 +218,7 @@ typedef struct
typedef struct
{
Application *application;
char *action_identifier;
char *target_identifier;
char *target_value;
char *started_at;
@ -231,6 +232,8 @@ typedef struct
char *execution_identifier;
GPtrArray *proposals;
} ApplicationOsintReviewContext;
static char *application_osint_output_to_utf8(GBytes *bytes);
static char *application_osint_create_timestamp(void);
/** @brief Contexte possédé pendant la révision d'une analyse EML. */
typedef struct { Application *application; char *evidence_identifier; }
ApplicationEmlReviewContext;
@ -380,10 +383,153 @@ static void application_osint_action_context_free(
g_free(context->target_value);
g_free(context->target_identifier);
g_free(context->action_identifier);
g_free(context->started_at);
g_free(context);
}
/** @brief Extrait un pseudo final depuis une URL de profil social. */
static char *application_osint_search_target(const char *action_identifier,
const char *target_value)
{
char *target = g_strdup(target_value != NULL ? target_value : "");
char *query = NULL;
char *slash = NULL;
if (target == NULL) return NULL;
if (g_str_has_prefix(action_identifier, "social-"))
{
query = strchr(target, '?');
if (query != NULL) *query = '\0';
slash = strrchr(target, '/');
if (slash != NULL && slash[1] != '\0')
{
char *username = g_strdup(slash + 1);
g_free(target);
target = username;
}
}
return target;
}
/** @brief Présente et archive la sortie d'une recherche sociale/email. */
static void application_on_osint_search_completed(BackgroundTask *task,
gpointer user_data)
{
ApplicationOsintActionContext *context = user_data;
const ToolTaskResult *task_result = NULL;
const ToolProcessResult *process_result = NULL;
GBytes *stdout_bytes = NULL;
GBytes *stderr_bytes = NULL;
char *stdout_text = NULL;
char *stderr_text = NULL;
char *details = NULL;
char *output_directory = NULL;
char *output_name = NULL;
char *output_path = NULL;
const InvestigationProject *project = NULL;
GError *error = NULL;
if (context == NULL || context->application == NULL) return;
if (background_task_get_state(task) == BACKGROUND_TASK_STATE_CANCELLED)
{
main_window_set_status(context->application->main_window,
"Recherche OSINT annulée.");
return;
}
if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED)
{
error = background_task_dup_error(task);
application_present_error(context->application, "Recherche OSINT impossible",
error != NULL ? error->message : "L'outil OSINT a échoué.");
g_clear_error(&error);
return;
}
task_result = tool_task_result_from_background_task(task);
process_result = tool_task_result_get_process_result(task_result);
stdout_bytes = tool_process_result_ref_stdout(process_result);
stderr_bytes = tool_process_result_ref_stderr(process_result);
stdout_text = application_osint_output_to_utf8(stdout_bytes);
stderr_text = application_osint_output_to_utf8(stderr_bytes);
project = investigation_session_get_project(context->application->session);
output_directory = project != NULL ? g_build_filename(
investigation_project_get_root_path(project), "02_Preuves_Traitees",
"Extractions", "OSINT", NULL) : NULL;
if (output_directory == NULL || g_mkdir_with_parents(output_directory, 0750) != 0)
goto failure;
output_name = g_strdup_printf("%s-%s.txt", context->target_identifier,
context->action_identifier);
output_path = g_build_filename(output_directory, output_name, NULL);
details = g_strdup_printf("Cible : %s\nAction : %s\n\n%s%s%s",
context->target_value, context->action_identifier,
stdout_text != NULL ? stdout_text : "",
stderr_text != NULL && stderr_text[0] != '\0' ? "\n\nErreur :\n" : "",
stderr_text != NULL ? stderr_text : "");
if (!g_file_set_contents(output_path, details, -1, &error)) goto failure;
application_message_dialog_present_details_action(
main_window_get_window(context->application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Résultat de la recherche OSINT",
"La sortie publique a été archivée dans les preuves traitées.",
details, NULL, NULL, NULL, NULL);
goto cleanup;
failure:
application_present_error(context->application, "Archivage OSINT impossible",
error != NULL ? error->message : "La sortie n'a pas pu être conservée.");
cleanup:
g_clear_error(&error); g_clear_pointer(&stdout_bytes, g_bytes_unref);
g_clear_pointer(&stderr_bytes, g_bytes_unref); g_free(stdout_text);
g_free(stderr_text); g_free(details); g_free(output_directory);
g_free(output_name); g_free(output_path);
}
/** @brief Lance Sherlock, Maigret ou Holehe selon l'action choisie. */
static void application_start_osint_search(Application *application,
const char *action_identifier, const char *target_identifier,
const char *target_value)
{
const char *tool_identifier = NULL;
const char *arguments[4] = { NULL, NULL, NULL, NULL };
char *search_target = NULL;
ToolTask *tool_task = NULL;
BackgroundTask *background_task = NULL;
ApplicationOsintActionContext *context = NULL;
GError *error = NULL;
if (g_strcmp0(action_identifier, "social-username-sherlock") == 0)
{ tool_identifier = "osint.sherlock"; arguments[0] = "--print-found";
arguments[1] = "--no-color"; }
else if (g_strcmp0(action_identifier, "social-username-maigret") == 0)
{ tool_identifier = "osint.maigret"; arguments[0] = "--json";
arguments[1] = "simple"; }
else if (g_strcmp0(action_identifier, "email-account-holehe") == 0)
{ tool_identifier = "osint.holehe"; arguments[0] = "--no-color"; }
else return;
search_target = application_osint_search_target(action_identifier, target_value);
arguments[2] = search_target;
tool_task = tool_task_new(tool_initializer_get_registry(application->tool_initializer),
tool_identifier, "Recherche OSINT", arguments, NULL, &error);
if (tool_task == NULL) goto failure;
context = g_new0(ApplicationOsintActionContext, 1);
context->application = application;
context->action_identifier = g_strdup(action_identifier);
context->target_identifier = g_strdup(target_identifier);
context->target_value = g_strdup(search_target);
context->started_at = application_osint_create_timestamp();
background_task = tool_task_get_background_task(tool_task);
if (context->action_identifier == NULL || context->target_identifier == NULL ||
context->target_value == NULL || context->started_at == NULL ||
!task_manager_add(application->task_manager, background_task, &error) ||
!tool_task_start(tool_task, application_on_osint_search_completed, context,
application_osint_action_context_free, &error)) goto failure;
context = NULL;
main_window_set_status(application->main_window,
"Recherche OSINT lancée ; progression disponible dans les tâches.");
goto cleanup;
failure:
application_osint_action_context_free(context);
application_present_error(application, "Recherche OSINT impossible",
error != NULL ? error->message : "Impossible de démarrer l'outil.");
cleanup:
g_clear_error(&error); tool_task_free(tool_task); g_free(search_target);
}
/**
* @brief Libère le contexte du dialogue de métadonnées.
*/
@ -1678,6 +1824,15 @@ static void application_on_osint_action_requested(
return;
}
if (g_strcmp0(action_identifier, "social-username-sherlock") == 0 ||
g_strcmp0(action_identifier, "social-username-maigret") == 0 ||
g_strcmp0(action_identifier, "email-account-holehe") == 0)
{
application_start_osint_search(application, action_identifier,
target_identifier, target_value);
return;
}
application_present_error(
application,
"Action OSINT inconnue",
@ -7555,6 +7710,36 @@ static void application_on_activate(
);
}
/** @brief Ajoute le répertoire privé des outils OSINT au PATH du processus. */
static void application_prepare_osint_tool_path(void)
{
const char *current_path = g_getenv("PATH");
const char *home_directory = g_get_home_dir();
char *osint_directory = NULL;
char *new_path = NULL;
if (home_directory == NULL || home_directory[0] == '\0') return;
osint_directory = g_build_filename(home_directory, ".local", "share",
"labfy-osint", "bin", NULL);
if (!g_file_test(osint_directory, G_FILE_TEST_IS_DIR))
{
g_free(osint_directory);
return;
}
if (current_path != NULL && g_strstr_len(current_path, -1,
osint_directory) != NULL)
{
g_free(osint_directory);
return;
}
new_path = g_strdup_printf("%s%s%s", osint_directory,
current_path != NULL && current_path[0] != '\0' ? G_SEARCHPATH_SEPARATOR_S :
"", current_path != NULL ? current_path : "");
g_setenv("PATH", new_path, TRUE);
g_free(new_path);
g_free(osint_directory);
}
Application *application_new(void)
{
Application *application = NULL;
@ -7565,6 +7750,8 @@ Application *application_new(void)
1
);
application_prepare_osint_tool_path();
application->task_manager =
task_manager_new();

View file

@ -84,6 +84,21 @@ static const char *const tool_catalog_qpdf_version_arguments[] =
"--version",
NULL
};
static const char *const tool_catalog_sherlock_version_arguments[] =
{
"--version",
NULL
};
static const char *const tool_catalog_maigret_version_arguments[] =
{
"--version",
NULL
};
static const char *const tool_catalog_holehe_version_arguments[] =
{
"--version",
NULL
};
/**
* @brief Catalogue statique initial.
@ -174,6 +189,30 @@ static const ToolCatalogEntry tool_catalog_entries[] =
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments = tool_catalog_qpdf_version_arguments,
.version_argument_count = 1
},
{
.identifier = "osint.sherlock",
.display_name = "Sherlock",
.executable_name = "sherlock",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments = tool_catalog_sherlock_version_arguments,
.version_argument_count = 1
},
{
.identifier = "osint.maigret",
.display_name = "Maigret",
.executable_name = "maigret",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments = tool_catalog_maigret_version_arguments,
.version_argument_count = 1
},
{
.identifier = "osint.holehe",
.display_name = "Holehe (vérification email)",
.executable_name = "holehe",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments = tool_catalog_holehe_version_arguments,
.version_argument_count = 1
}
};

View file

@ -75,6 +75,9 @@ OsintActionCatalog *osint_action_catalog_new_defaults(void)
OsintActionCatalog *catalog = g_try_new0(OsintActionCatalog, 1);
OsintAction *demonstration_action = NULL;
OsintAction *dns_action = NULL;
OsintAction *sherlock_action = NULL;
OsintAction *maigret_action = NULL;
OsintAction *holehe_action = NULL;
if (catalog == NULL) return NULL;
catalog->actions = g_ptr_array_new_with_free_func(osint_action_free);
demonstration_action = osint_action_new(
@ -88,16 +91,41 @@ OsintActionCatalog *osint_action_catalog_new_defaults(void)
OSINT_SELECTION_CONTEXT_KIND_ENTITY, "domain_name", "dns.dig", FALSE,
"L'outil requis n'a pas encore été vérifié."
);
sherlock_action = osint_action_new(
"social-username-sherlock", "Rechercher le pseudo (Sherlock)",
"Recherche des profils publics correspondant au pseudo sélectionné.",
OSINT_SELECTION_CONTEXT_KIND_ENTITY, "social_account", "osint.sherlock",
FALSE, "L'outil requis n'a pas encore été vérifié."
);
maigret_action = osint_action_new(
"social-username-maigret", "Rechercher le pseudo (Maigret)",
"Recherche complémentaire de profils publics par pseudo.",
OSINT_SELECTION_CONTEXT_KIND_ENTITY, "social_account", "osint.maigret",
FALSE, "L'outil requis n'a pas encore été vérifié."
);
holehe_action = osint_action_new(
"email-account-holehe", "Rechercher l'email (Holehe)",
"Vérification publique optionnelle des services associés à l'email.",
OSINT_SELECTION_CONTEXT_KIND_ENTITY, "email_address", "osint.holehe",
FALSE, "L'outil requis n'a pas encore été vérifié."
);
if (catalog->actions == NULL || demonstration_action == NULL ||
dns_action == NULL)
dns_action == NULL || sherlock_action == NULL || maigret_action == NULL ||
holehe_action == NULL)
{
osint_action_free(demonstration_action);
osint_action_free(dns_action);
osint_action_free(sherlock_action);
osint_action_free(maigret_action);
osint_action_free(holehe_action);
osint_action_catalog_free(catalog);
return NULL;
}
g_ptr_array_add(catalog->actions, demonstration_action);
g_ptr_array_add(catalog->actions, dns_action);
g_ptr_array_add(catalog->actions, sherlock_action);
g_ptr_array_add(catalog->actions, maigret_action);
g_ptr_array_add(catalog->actions, holehe_action);
return catalog;
}
@ -109,6 +137,21 @@ static gint osint_action_compare(gconstpointer first, gconstpointer second)
return g_strcmp0((*a)->label, (*b)->label);
}
/** @brief Regroupe les types de comptes sociaux pour les recherches publiques. */
static gboolean osint_action_social_type_matches(const char *expected,
const char *actual)
{
gboolean expected_social = g_strcmp0(expected, "social_account") == 0;
gboolean actual_social = actual != NULL &&
(g_strcmp0(actual, "social_account") == 0 ||
g_strcmp0(actual, "instagram_account") == 0 ||
g_strcmp0(actual, "tiktok_account") == 0 ||
g_strcmp0(actual, "facebook_account") == 0 ||
g_strcmp0(actual, "telegram_account") == 0 ||
g_strcmp0(actual, "x_account") == 0);
return expected_social && actual_social;
}
GPtrArray *osint_action_catalog_list_compatible(
const OsintActionCatalog *catalog,
const OsintSelectionContext *context
@ -126,9 +169,11 @@ GPtrArray *osint_action_catalog_list_compatible(
gboolean kind_matches = action->target_kind ==
OSINT_SELECTION_CONTEXT_KIND_UNKNOWN ||
action->target_kind == osint_selection_context_get_kind(context);
const char *context_type = osint_selection_context_get_type(context);
gboolean type_matches = action->compatible_type == NULL ||
g_strcmp0(action->compatible_type,
osint_selection_context_get_type(context)) == 0;
g_strcmp0(action->compatible_type, context_type) == 0 ||
osint_action_social_type_matches(action->compatible_type,
context_type);
if (kind_matches && type_matches)
g_ptr_array_add(compatible_actions, action);
}

View file

@ -19,6 +19,7 @@ struct EntityDetailsPanel
GtkWidget *root_revealer;
GtkWidget *frame;
GtkWidget *add_relation_button;
GtkWidget *osint_button;
GtkWidget *close_button;
GtkWidget *entity_title_label;
@ -40,6 +41,7 @@ struct EntityDetailsPanel
GtkWidget *person_evidence_summary_label;
char *selected_entity_identifier;
char *selected_entity_type;
EntityDetailsPanelCloseCallback close_callback;
gpointer close_user_data;
@ -59,7 +61,32 @@ struct EntityDetailsPanel
gpointer person_name_user_data;
EntityDetailsPanelPersonEvidenceCallback person_evidence_callback;
gpointer person_evidence_user_data;
EntityDetailsPanelOsintCallback osint_callback;
gpointer osint_user_data;
};
/** @brief Indique si un type représente un compte social. */
static gboolean entity_details_panel_is_social_type(const char *type)
{
return g_strcmp0(type, "social_account") == 0 ||
g_strcmp0(type, "instagram_account") == 0 ||
g_strcmp0(type, "tiktok_account") == 0 ||
g_strcmp0(type, "facebook_account") == 0 ||
g_strcmp0(type, "telegram_account") == 0 ||
g_strcmp0(type, "x_account") == 0;
}
/** @brief Transmet la demande de recherche OSINT depuis la fiche. */
static void entity_details_panel_on_osint_clicked(GtkButton *button,
gpointer user_data)
{
EntityDetailsPanel *panel = user_data;
(void) button;
if (panel != NULL && panel->osint_callback != NULL &&
panel->selected_entity_identifier != NULL)
panel->osint_callback(panel->selected_entity_identifier,
panel->osint_user_data);
}
/** @brief Relaie la demande de gestion des preuves d'une personne. */
static void entity_details_panel_on_person_evidence_clicked(GtkButton *button,
gpointer user_data)
@ -418,6 +445,8 @@ EntityDetailsPanel *entity_details_panel_new(void)
gtk_button_new_from_icon_name(
"list-add-symbolic"
);
details_panel->osint_button = gtk_button_new_from_icon_name(
"system-search-symbolic");
details_panel->close_button =
gtk_button_new_from_icon_name(
@ -461,6 +490,7 @@ EntityDetailsPanel *entity_details_panel_new(void)
if (details_panel->root_revealer == NULL ||
details_panel->frame == NULL ||
details_panel->add_relation_button == NULL ||
details_panel->osint_button == NULL ||
details_panel->close_button == NULL ||
content_box == NULL ||
header_box == NULL ||
@ -588,6 +618,10 @@ EntityDetailsPanel *entity_details_panel_new(void)
details_panel->add_relation_button,
"flat"
);
gtk_widget_set_tooltip_text(details_panel->osint_button,
"Recherches OSINT compatibles");
gtk_widget_add_css_class(details_panel->osint_button, "flat");
gtk_widget_set_sensitive(details_panel->osint_button, FALSE);
gtk_widget_set_sensitive(
details_panel->add_relation_button,
@ -609,6 +643,8 @@ EntityDetailsPanel *entity_details_panel_new(void)
header_label
);
gtk_box_append(GTK_BOX(header_box), details_panel->osint_button);
gtk_box_append(
GTK_BOX(header_box),
details_panel->add_relation_button
@ -880,6 +916,9 @@ EntityDetailsPanel *entity_details_panel_new(void)
details_panel->frame
);
g_signal_connect(
details_panel->osint_button, "clicked",
G_CALLBACK(entity_details_panel_on_osint_clicked), details_panel);
g_signal_connect(
details_panel->add_relation_button,
"clicked",
@ -937,6 +976,7 @@ void entity_details_panel_set_entity(
&details_panel->selected_entity_identifier,
g_free
);
g_clear_pointer(&details_panel->selected_entity_type, g_free);
if (entity_record == NULL)
{
@ -953,6 +993,12 @@ void entity_details_panel_set_entity(
entity_record
)
);
details_panel->selected_entity_type = g_strdup(
entity_record_get_type_identifier(entity_record));
gtk_widget_set_sensitive(details_panel->osint_button,
details_panel->osint_callback != NULL &&
(g_strcmp0(details_panel->selected_entity_type, "email_address") == 0 ||
entity_details_panel_is_social_type(details_panel->selected_entity_type)));
gboolean is_person = g_strcmp0(entity_record_get_type_identifier(
entity_record), "person") == 0;
@ -1098,6 +1144,9 @@ void entity_details_panel_clear(
&details_panel->selected_entity_identifier,
g_free
);
g_clear_pointer(&details_panel->selected_entity_type, g_free);
if (details_panel->osint_button != NULL)
gtk_widget_set_sensitive(details_panel->osint_button, FALSE);
entity_details_panel_update_add_relation_button(
details_panel
@ -1246,6 +1295,18 @@ void entity_details_panel_set_person_evidence_callback(
details_panel->person_evidence_callback = callback;
details_panel->person_evidence_user_data = user_data;
}
void entity_details_panel_set_osint_callback(EntityDetailsPanel *details_panel,
EntityDetailsPanelOsintCallback callback, gpointer user_data)
{
if (details_panel == NULL) return;
details_panel->osint_callback = callback;
details_panel->osint_user_data = user_data;
if (details_panel->osint_button != NULL)
gtk_widget_set_sensitive(details_panel->osint_button,
callback != NULL && details_panel->selected_entity_type != NULL &&
(g_strcmp0(details_panel->selected_entity_type, "email_address") == 0 ||
entity_details_panel_is_social_type(details_panel->selected_entity_type)));
}
void entity_details_panel_set_person_evidences(EntityDetailsPanel *panel,
const GPtrArray *records)
{

View file

@ -444,6 +444,18 @@ static void workspace_update_osint_tools_menu(
g_clear_pointer(&compatible_actions, g_ptr_array_unref);
}
/** @brief Ouvre le menu OSINT depuis le bouton de la fiche entité. */
static void workspace_on_entity_osint_requested(const char *entity_identifier,
gpointer user_data)
{
Workspace *workspace = user_data;
(void) entity_identifier;
if (workspace == NULL || workspace->osint_tools_popover == NULL) return;
workspace_update_osint_tools_menu(workspace,
workspace->osint_selection_context);
gtk_popover_popup(GTK_POPOVER(workspace->osint_tools_popover));
}
/**
* @brief Ajoute une ligne de métadonnée dans la grille.
*
@ -1945,6 +1957,8 @@ Workspace *workspace_new(void)
entity_details_panel_set_person_evidence_callback(
workspace->entity_details_panel,
workspace_on_person_evidence_requested, workspace);
entity_details_panel_set_osint_callback(workspace->entity_details_panel,
workspace_on_entity_osint_requested, workspace);
gtk_overlay_set_child(
GTK_OVERLAY(

View file

@ -50,6 +50,34 @@ static void test_non_domain_actions(void)
osint_action_catalog_free(catalog);
}
static void test_social_and_email_actions(void)
{
OsintActionCatalog *catalog = osint_action_catalog_new_defaults();
OsintSelectionContext *social = create_entity_context("social_account");
OsintSelectionContext *email = create_entity_context("email_address");
OsintSelectionContext *instagram = create_entity_context("instagram_account");
OsintSelectionContext *facebook = create_entity_context("facebook_account");
GPtrArray *actions = osint_action_catalog_list_compatible(catalog, social);
GPtrArray *email_actions = osint_action_catalog_list_compatible(catalog, email);
GPtrArray *instagram_actions = osint_action_catalog_list_compatible(catalog, instagram);
GPtrArray *facebook_actions = osint_action_catalog_list_compatible(catalog, facebook);
g_assert_cmpuint(actions->len, ==, 3U);
g_assert_cmpuint(email_actions->len, ==, 2U);
g_assert_cmpuint(instagram_actions->len, ==, 3U);
g_assert_cmpuint(facebook_actions->len, ==, 3U);
g_assert_cmpstr(osint_action_get_identifier(g_ptr_array_index(email_actions, 1)),
==, "email-account-holehe");
g_ptr_array_unref(actions);
g_ptr_array_unref(email_actions);
g_ptr_array_unref(instagram_actions);
g_ptr_array_unref(facebook_actions);
osint_selection_context_free(social);
osint_selection_context_free(email);
osint_selection_context_free(instagram);
osint_selection_context_free(facebook);
osint_action_catalog_free(catalog);
}
static void test_invalid_arguments(void)
{
OsintActionCatalog *catalog = osint_action_catalog_new_defaults();
@ -92,6 +120,7 @@ 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/social-email", test_social_and_email_actions);
g_test_add_func("/osint-action-catalog/invalid", test_invalid_arguments);
g_test_add_func("/osint-action-catalog/tool-states", test_tool_states);
return g_test_run();

View file

@ -86,6 +86,24 @@ static const ExpectedToolCatalogEntry expected_catalog_entries[] =
.display_name = "qpdf",
.executable_name = "qpdf",
.version_argument = "--version"
},
{
.identifier = "osint.sherlock",
.display_name = "Sherlock",
.executable_name = "sherlock",
.version_argument = "--version"
},
{
.identifier = "osint.maigret",
.display_name = "Maigret",
.executable_name = "maigret",
.version_argument = "--version"
},
{
.identifier = "osint.holehe",
.display_name = "Holehe (vérification email)",
.executable_name = "holehe",
.version_argument = "--version"
}
};