feat(osint): integrate selected DNS proposals

This commit is contained in:
grayTerminal-sh 2026-07-22 12:18:18 +02:00
parent 5a196f6ac6
commit f6f3e71002
12 changed files with 975 additions and 51 deletions

View file

@ -117,6 +117,7 @@ TEST_OSINT_SELECTION_CONTEXT := tests/test_osint_selection_context
TEST_OSINT_ACTION_CATALOG := tests/test_osint_action_catalog
TEST_OSINT_DNS_QUERY := tests/test_osint_dns_query
TEST_OSINT_DNS_PROPOSAL := tests/test_osint_dns_proposal
TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration
all: $(TARGET)
@ -554,9 +555,24 @@ $(TEST_OSINT_DNS_QUERY): \
$(TEST_OSINT_DNS_PROPOSAL): \
tests/test_osint_dns_proposal.c \
src/models/osint_dns_proposal.c
src/models/osint_dns_proposal.c \
src/models/osint_dns_query.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_OSINT_DNS_INTEGRATION): \
tests/test_osint_dns_integration.c \
src/core/osint_dns_integration.c \
src/models/osint_dns_proposal.c \
src/models/osint_dns_query.c \
src/models/entity_record.c \
src/dao/entity_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_INVESTIGATION_GRAPH_LOAD_TASK): \
tests/test_investigation_graph_load_task.c \
src/core/investigation_graph_load_task.c \
@ -628,7 +644,8 @@ test: \
$(TEST_OSINT_SELECTION_CONTEXT) \
$(TEST_OSINT_ACTION_CATALOG) \
$(TEST_OSINT_DNS_QUERY) \
$(TEST_OSINT_DNS_PROPOSAL)
$(TEST_OSINT_DNS_PROPOSAL) \
$(TEST_OSINT_DNS_INTEGRATION)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -680,6 +697,7 @@ test: \
@$(TEST_OSINT_ACTION_CATALOG)
@$(TEST_OSINT_DNS_QUERY)
@$(TEST_OSINT_DNS_PROPOSAL)
@$(TEST_OSINT_DNS_INTEGRATION)
@echo "Tous les tests sont valides."
%.o: %.c
@ -737,7 +755,8 @@ clean:
$(TEST_OSINT_SELECTION_CONTEXT) \
$(TEST_OSINT_ACTION_CATALOG) \
$(TEST_OSINT_DNS_QUERY) \
$(TEST_OSINT_DNS_PROPOSAL)
$(TEST_OSINT_DNS_PROPOSAL) \
$(TEST_OSINT_DNS_INTEGRATION)
-include $(DEP)

View file

@ -147,7 +147,9 @@ Le socle actuel comprend notamment :
- disponibilité des actions OSINT synchronisée avec le registre doutils ;
- résolution DNS asynchrone avec `dig` depuis une entité domaine ;
- affichage sélectionnable des sorties standard et derreur, sans persistance ;
- révision des réponses DNS sous forme de propositions avant intégration.
- révision des réponses DNS sous forme de propositions avant intégration ;
- sélection explicite et intégration transactionnelle des propositions DNS
compatibles, avec normalisation et détection des doublons.
Les outils actuellement présents dans le catalogue initial sont :

View file

@ -1106,6 +1106,27 @@ 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à
enregistrées.
### Limite actuelle de la traçabilité OSINT
L'intégration DNS initiale peut créer des entités `domain_name` et
`ip_address`. La description de l'entité conserve 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 ;
- la cible et les arguments de l'exécution ;
- la date d'exécution ;
- la sortie brute et son empreinte ;
- les propositions extraites ;
- 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
comme des faits vérifiés et ne doit pas supprimer la sortie brute affichée à
l'utilisateur.
---
# 6. Tables de liaison

View file

@ -0,0 +1,59 @@
/******************************************************************************
* @file osint_dns_integration.h
* @brief Intégration transactionnelle de propositions DNS validées.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_OSINT_DNS_INTEGRATION_H
#define LABFY_INVESTIGATION_OSINT_DNS_INTEGRATION_H
#include "database/database.h"
#include <glib.h>
G_BEGIN_DECLS
/** @brief Codes d'erreur de l'intégration DNS. */
typedef enum
{
OSINT_DNS_INTEGRATION_ERROR_INVALID_ARGUMENT,
OSINT_DNS_INTEGRATION_ERROR_DATABASE,
OSINT_DNS_INTEGRATION_ERROR_MODEL,
OSINT_DNS_INTEGRATION_ERROR_INSERT
} OsintDnsIntegrationError;
/** @brief Domaine d'erreur de l'intégration DNS. */
#define OSINT_DNS_INTEGRATION_ERROR osint_dns_integration_error_quark()
/**
* @brief Retourne le domaine d'erreur de l'intégration DNS.
*
* @return Quark GLib du domaine d'erreur.
*/
GQuark osint_dns_integration_error_quark(void);
/**
* @brief Intègre une sélection de propositions dans une transaction unique.
*
* Les propositions doivent être des OsintDnsProposal empruntées. Les valeurs
* incompatibles sont ignorées. Les couples type/valeur déjà présents sont
* comptés comme doublons et ne sont pas réinsérés.
*
* @param database Connexion SQLite active.
* @param selected_proposals Propositions explicitement sélectionnées.
* @param out_inserted_count Nombre d'entités créées.
* @param out_skipped_count Nombre de propositions ignorées ou déjà présentes.
* @param error Emplacement facultatif recevant une erreur.
*
* @return TRUE lorsque la transaction est validée, sinon FALSE.
*/
gboolean osint_dns_integration_apply(
Database *database,
GPtrArray *selected_proposals,
guint *out_inserted_count,
guint *out_skipped_count,
GError **error
);
G_END_DECLS
#endif

View file

@ -75,6 +75,35 @@ const char *osint_dns_proposal_get_target(
const OsintDnsProposal *proposal
);
/**
* @brief Retourne le type d'entité compatible avec la proposition.
*
* Les types actuellement pris en charge sont `ip_address` pour A et AAAA,
* ainsi que `domain_name` pour CNAME, NS et PTR.
*
* @param proposal Proposition consultée.
*
* @return Identifiant statique du type d'entité, ou NULL.
*/
const char *osint_dns_proposal_get_entity_type(
const OsintDnsProposal *proposal
);
/**
* @brief Normalise la valeur avant recherche ou insertion.
*
* Les adresses IP sont converties dans leur représentation canonique. Les
* noms de domaine sont passés en minuscules et leur point final est retiré.
*
* @param proposal Proposition consultée.
*
* @return Nouvelle chaîne à libérer avec g_free(), ou NULL si la proposition
* n'est pas compatible ou si sa valeur est invalide.
*/
char *osint_dns_proposal_dup_normalized_value(
const OsintDnsProposal *proposal
);
G_END_DECLS
#endif

View file

@ -0,0 +1,47 @@
/******************************************************************************
* @file osint_dns_review_dialog.h
* @brief Dialogue de sélection des propositions issues d'une réponse DNS.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_OSINT_DNS_REVIEW_DIALOG_H
#define LABFY_INVESTIGATION_OSINT_DNS_REVIEW_DIALOG_H
#include <gtk/gtk.h>
G_BEGIN_DECLS
/**
* @brief Callback appelé après confirmation de la sélection DNS.
*
* Le tableau et les propositions sont empruntés et uniquement valides pendant
* l'appel. Le tableau contient seulement les propositions compatibles cochées.
*
* @param selected_proposals Propositions sélectionnées.
* @param user_data Données empruntées fournies par l'appelant.
*/
typedef void (*OsintDnsReviewDialogConfirmCallback)(
GPtrArray *selected_proposals,
gpointer user_data
);
/**
* @brief Affiche le dialogue de sélection avant intégration.
*
* Le dialogue conserve une référence sur proposals jusqu'à sa fermeture.
* Fermer ou annuler ne déclenche pas le callback.
*
* @param parent_window Fenêtre parente, ou NULL.
* @param proposals Tableau de OsintDnsProposal à réviser.
* @param callback Callback de confirmation facultatif.
* @param user_data Données empruntées transmises au callback.
*/
void osint_dns_review_dialog_present(
GtkWindow *parent_window,
GPtrArray *proposals,
OsintDnsReviewDialogConfirmCallback callback,
gpointer user_data
);
G_END_DECLS
#endif

View file

@ -24,6 +24,7 @@
#include "core/background_task.h"
#include "core/tool_initializer.h"
#include "core/tool_task.h"
#include "core/osint_dns_integration.h"
#include "views/file_dialog.h"
#include "core/evidence_import_task.h"
#include "models/evidence_record.h"
@ -31,6 +32,7 @@
#include "dao/graph_node_position_dao.h"
#include "views/evidence_import_dialog.h"
#include "views/create_relation_dialog.h"
#include "views/osint_dns_review_dialog.h"
#include "dao/evidence_dao.h"
#include "dao/entity_dao.h"
#include "models/evidence_type.h"
@ -167,70 +169,99 @@ typedef struct
typedef struct
{
Application *application;
char *proposal_summary;
GPtrArray *proposals;
} ApplicationOsintReviewContext;
static void application_start_graph_loading(
Application *application,
const char *database_path
);
static void application_present_error(
Application *application,
const char *title,
const char *message
);
/** @brief Libère les données de révision possédées par le dialogue. */
static void application_osint_review_context_free(gpointer user_data)
{
ApplicationOsintReviewContext *context = user_data;
if (context == NULL) return;
g_free(context->proposal_summary);
g_clear_pointer(&context->proposals, g_ptr_array_unref);
g_free(context);
}
/** @brief Affiche les propositions sans les intégrer à l'enquête. */
static void application_on_osint_prepare_integration(gpointer user_data)
/** @brief Intègre les propositions confirmées puis recharge le graphe. */
static void application_on_osint_integration_confirmed(
GPtrArray *selected_proposals,
gpointer user_data
)
{
ApplicationOsintReviewContext *context = user_data;
if (context == NULL || context->application == NULL ||
context->application->main_window == NULL)
Application *application = user_data;
const InvestigationProject *project = NULL;
Database *database = NULL;
const char *database_path = NULL;
guint inserted_count = 0U;
guint skipped_count = 0U;
char *message = NULL;
GError *error = NULL;
if (application == NULL || application->session == NULL ||
application->main_window == NULL)
{
return;
}
application_message_dialog_present_details(
main_window_get_window(context->application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION,
"Propositions DNS à réviser",
"Aucune donnée n'a été enregistrée dans l'enquête.",
context->proposal_summary
);
}
/** @brief Construit le résumé lisible des propositions DNS. */
static char *application_build_dns_proposal_summary(GPtrArray *proposals)
{
GString *summary = g_string_new(NULL);
if (summary == NULL) return NULL;
g_string_append_printf(
summary,
"Propositions détectées : %u\n\n",
proposals != NULL ? proposals->len : 0U
);
for (guint index = 0; proposals != NULL && index < proposals->len; index++)
database = investigation_session_get_database(application->session);
project = investigation_session_get_project(application->session);
database_path = project != NULL
? investigation_project_get_database_path(project) : NULL;
if (!osint_dns_integration_apply(
database, selected_proposals, &inserted_count, &skipped_count,
&error
))
{
const OsintDnsProposal *proposal = g_ptr_array_index(proposals, index);
g_string_append_printf(
summary,
"%u. Cible : %s\n Propriétaire : %s\n Type : %s\n"
" Valeur : %s\n\n",
index + 1U,
osint_dns_proposal_get_target(proposal),
osint_dns_proposal_get_owner(proposal),
osint_dns_proposal_get_record_type(proposal),
osint_dns_proposal_get_value(proposal)
application_present_error(
application,
"Intégration DNS impossible",
error != NULL ? error->message
: "La transaction DNS a échoué."
);
g_clear_error(&error);
return;
}
g_string_append(
summary,
"Ces propositions sont uniquement préparées pour révision. "
"Elles ne sont pas enregistrées dans SQLite."
message = g_strdup_printf(
"%u entité(s) créée(s), %u proposition(s) ignorée(s).\n\n"
"La provenance complète devra être ajoutée par une future migration "
"du schéma OSINT.",
inserted_count, skipped_count
);
application_message_dialog_present(
main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION,
"Intégration DNS terminée",
message
);
g_free(message);
if (database_path != NULL && database_path[0] != '\0')
application_start_graph_loading(application, database_path);
}
/** @brief Affiche les propositions et demande une sélection explicite. */
static void application_on_osint_prepare_integration(gpointer user_data)
{
ApplicationOsintReviewContext *context = user_data;
if (context == NULL || context->application == NULL ||
context->application->main_window == NULL || context->proposals == NULL)
return;
osint_dns_review_dialog_present(
main_window_get_window(context->application->main_window),
context->proposals,
application_on_osint_integration_confirmed,
context->application
);
return g_string_free(summary, FALSE);
}
/**
@ -1147,10 +1178,9 @@ static void application_on_dns_lookup_completed(
if (review_context != NULL)
{
review_context->application = application;
review_context->proposal_summary =
application_build_dns_proposal_summary(proposals);
review_context->proposals = g_ptr_array_ref(proposals);
}
if (review_context == NULL || review_context->proposal_summary == NULL)
if (review_context == NULL || review_context->proposals == NULL)
{
application_osint_review_context_free(review_context);
review_context = NULL;

View file

@ -0,0 +1,228 @@
/******************************************************************************
* @file osint_dns_integration.c
* @brief Intégration transactionnelle de propositions DNS validées.
******************************************************************************/
#include "core/osint_dns_integration.h"
#include "dao/entity_dao.h"
#include "database/error.h"
#include "database/transaction.h"
#include "models/entity_record.h"
#include "models/osint_dns_proposal.h"
#include <gio/gio.h>
#include <string.h>
/** @brief Normalise une valeur déjà persistée pour la comparaison. */
static char *osint_dns_integration_normalize_existing_value(
const char *entity_type,
const char *value
)
{
if (g_strcmp0(entity_type, "ip_address") == 0)
{
GInetAddress *address = g_inet_address_new_from_string(value);
char *normalized_value = NULL;
if (address == NULL) return g_strdup(value);
normalized_value = g_inet_address_to_string(address);
g_object_unref(address);
return normalized_value;
}
if (g_strcmp0(entity_type, "domain_name") == 0)
{
char *normalized_value = g_ascii_strdown(value, -1);
gsize value_length = normalized_value != NULL
? strlen(normalized_value) : 0U;
if (value_length > 0U && normalized_value[value_length - 1U] == '.')
normalized_value[value_length - 1U] = '\0';
return normalized_value;
}
return g_strdup(value);
}
/** @brief Construit la clé anti-doublon d'une entité. */
static char *osint_dns_integration_build_key(
const char *entity_type,
const char *value
)
{
return g_strdup_printf("%s\n%s", entity_type, value);
}
/** @brief Renseigne une erreur littérale si possible. */
static void osint_dns_integration_set_error(
GError **error,
OsintDnsIntegrationError code,
const char *message
)
{
if (error != NULL && *error == NULL)
g_set_error_literal(error, OSINT_DNS_INTEGRATION_ERROR, code, message);
}
GQuark osint_dns_integration_error_quark(void)
{
return g_quark_from_static_string("osint-dns-integration-error-quark");
}
gboolean osint_dns_integration_apply(
Database *database,
GPtrArray *selected_proposals,
guint *out_inserted_count,
guint *out_skipped_count,
GError **error
)
{
EntityDao *entity_dao = NULL;
GPtrArray *existing_entities = NULL;
GHashTable *known_entities = NULL;
GDateTime *now = NULL;
char *timestamp = NULL;
guint inserted_count = 0U;
guint skipped_count = 0U;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_inserted_count != NULL) *out_inserted_count = 0U;
if (out_skipped_count != NULL) *out_skipped_count = 0U;
if (database == NULL || selected_proposals == NULL ||
selected_proposals->len == 0U)
{
osint_dns_integration_set_error(
error, OSINT_DNS_INTEGRATION_ERROR_INVALID_ARGUMENT,
"La sélection DNS à intégrer est invalide."
);
return FALSE;
}
entity_dao = entity_dao_new(database, error);
if (entity_dao == NULL) return FALSE;
existing_entities = entity_dao_list_all(entity_dao, error);
if (existing_entities == NULL) goto cleanup;
known_entities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
for (guint index = 0; index < existing_entities->len; index++)
{
const EntityRecord *entity = g_ptr_array_index(existing_entities, index);
const char *entity_type = entity_record_get_type_identifier(entity);
char *normalized_value = osint_dns_integration_normalize_existing_value(
entity_type, entity_record_get_value(entity)
);
g_hash_table_add(
known_entities,
osint_dns_integration_build_key(
entity_type,
normalized_value
)
);
g_free(normalized_value);
}
now = g_date_time_new_now_utc();
timestamp = now != NULL
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
if (timestamp == NULL)
{
osint_dns_integration_set_error(
error, OSINT_DNS_INTEGRATION_ERROR_MODEL,
"Impossible de dater les entités DNS."
);
goto cleanup;
}
if (!database_transaction_begin(database))
{
osint_dns_integration_set_error(
error, OSINT_DNS_INTEGRATION_ERROR_DATABASE,
database_error_get_message(database) != NULL
? database_error_get_message(database)
: "Impossible de démarrer la transaction DNS."
);
goto cleanup;
}
for (guint index = 0; index < selected_proposals->len; index++)
{
const OsintDnsProposal *proposal = g_ptr_array_index(
selected_proposals, index
);
const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal);
char *entity_key = NULL;
char *identifier = NULL;
char *description = NULL;
EntityRecord *entity = NULL;
GError *model_error = NULL;
if (entity_type == NULL || normalized_value == NULL)
{
skipped_count++;
g_free(normalized_value);
continue;
}
entity_key = osint_dns_integration_build_key(entity_type, normalized_value);
if (g_hash_table_contains(known_entities, entity_key))
{
skipped_count++;
g_free(entity_key);
g_free(normalized_value);
continue;
}
identifier = g_uuid_string_random();
description = g_strdup_printf(
"Proposition DNS %s obtenue avec dns.dig depuis %s. "
"Résultat OSINT à vérifier.",
osint_dns_proposal_get_record_type(proposal),
osint_dns_proposal_get_target(proposal)
);
entity = entity_record_new(
identifier, entity_type, normalized_value, normalized_value,
description, 50, timestamp, timestamp, ENTITY_STATUS_ACTIVE,
&model_error
);
if (entity == NULL || !entity_dao_insert(entity_dao, entity, error))
{
if (error != NULL && *error == NULL && model_error != NULL)
g_propagate_error(error, g_steal_pointer(&model_error));
entity_record_free(entity);
g_clear_error(&model_error);
g_free(description);
g_free(identifier);
g_free(entity_key);
g_free(normalized_value);
database_transaction_rollback(database);
goto cleanup;
}
g_hash_table_add(known_entities, entity_key);
inserted_count++;
entity_record_free(entity);
g_clear_error(&model_error);
g_free(description);
g_free(identifier);
g_free(normalized_value);
}
if (!database_transaction_commit(database))
{
osint_dns_integration_set_error(
error, OSINT_DNS_INTEGRATION_ERROR_DATABASE,
database_error_get_message(database) != NULL
? database_error_get_message(database)
: "Impossible de valider la transaction DNS."
);
database_transaction_rollback(database);
goto cleanup;
}
success = TRUE;
if (out_inserted_count != NULL) *out_inserted_count = inserted_count;
if (out_skipped_count != NULL) *out_skipped_count = skipped_count;
cleanup:
g_free(timestamp);
g_clear_pointer(&now, g_date_time_unref);
g_clear_pointer(&known_entities, g_hash_table_unref);
g_clear_pointer(&existing_entities, g_ptr_array_unref);
entity_dao_free(entity_dao);
return success;
}

View file

@ -4,7 +4,9 @@
******************************************************************************/
#include "models/osint_dns_proposal.h"
#include "models/osint_dns_query.h"
#include <gio/gio.h>
#include <string.h>
struct OsintDnsProposal
@ -155,3 +157,56 @@ const char *osint_dns_proposal_get_value(const OsintDnsProposal *proposal)
{ return proposal != NULL ? proposal->value : NULL; }
const char *osint_dns_proposal_get_target(const OsintDnsProposal *proposal)
{ return proposal != NULL ? proposal->target : NULL; }
const char *osint_dns_proposal_get_entity_type(
const OsintDnsProposal *proposal
)
{
const char *record_type = osint_dns_proposal_get_record_type(proposal);
if (g_strcmp0(record_type, "A") == 0 ||
g_strcmp0(record_type, "AAAA") == 0)
{
return "ip_address";
}
if (g_strcmp0(record_type, "CNAME") == 0 ||
g_strcmp0(record_type, "NS") == 0 ||
g_strcmp0(record_type, "PTR") == 0)
{
return "domain_name";
}
return NULL;
}
char *osint_dns_proposal_dup_normalized_value(
const OsintDnsProposal *proposal
)
{
const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
const char *value = osint_dns_proposal_get_value(proposal);
if (entity_type == NULL || value == NULL) return NULL;
if (g_strcmp0(entity_type, "ip_address") == 0)
{
GInetAddress *address = g_inet_address_new_from_string(value);
char *normalized_value = NULL;
if (address == NULL) return NULL;
normalized_value = g_inet_address_to_string(address);
g_object_unref(address);
return normalized_value;
}
if (g_strcmp0(entity_type, "domain_name") == 0)
{
char *normalized_value = g_ascii_strdown(value, -1);
gsize value_length = normalized_value != NULL
? strlen(normalized_value) : 0U;
if (value_length > 0U && normalized_value[value_length - 1U] == '.')
normalized_value[value_length - 1U] = '\0';
if (!osint_dns_query_is_valid_target(normalized_value))
{
g_free(normalized_value);
return NULL;
}
return normalized_value;
}
return NULL;
}

View file

@ -0,0 +1,276 @@
/******************************************************************************
* @file osint_dns_review_dialog.c
* @brief Dialogue de sélection des propositions issues d'une réponse DNS.
******************************************************************************/
#include "views/osint_dns_review_dialog.h"
#include "models/osint_dns_proposal.h"
typedef struct
{
GtkWindow *window;
GtkWidget *select_all_button;
GtkWidget *confirm_button;
GPtrArray *proposals;
GPtrArray *selection_buttons;
OsintDnsReviewDialogConfirmCallback callback;
gpointer user_data;
} OsintDnsReviewDialogContext;
/** @brief Libère le contexte possédé par la fenêtre. */
static void osint_dns_review_dialog_context_free(gpointer data)
{
OsintDnsReviewDialogContext *context = data;
if (context == NULL) return;
g_clear_pointer(&context->selection_buttons, g_ptr_array_unref);
g_clear_pointer(&context->proposals, g_ptr_array_unref);
g_free(context);
}
/** @brief Ferme le dialogue sans intégrer de proposition. */
static void osint_dns_review_dialog_on_cancel_clicked(
GtkButton *button,
gpointer user_data
)
{
OsintDnsReviewDialogContext *context = user_data;
(void) button;
if (context != NULL) gtk_window_destroy(context->window);
}
/** @brief Actualise l'état du bouton de confirmation. */
static void osint_dns_review_dialog_update_confirmation(
OsintDnsReviewDialogContext *context
)
{
gboolean has_selection = FALSE;
for (guint index = 0;
context != NULL && index < context->selection_buttons->len;
index++)
{
GtkCheckButton *button = g_ptr_array_index(
context->selection_buttons, index
);
if (gtk_widget_get_sensitive(GTK_WIDGET(button)) &&
gtk_check_button_get_active(button))
{
has_selection = TRUE;
break;
}
}
if (context != NULL)
gtk_widget_set_sensitive(context->confirm_button, has_selection);
}
/** @brief Réagit à la sélection individuelle d'une proposition. */
static void osint_dns_review_dialog_on_proposal_toggled(
GtkCheckButton *button,
gpointer user_data
)
{
(void) button;
osint_dns_review_dialog_update_confirmation(user_data);
}
/** @brief Sélectionne ou désélectionne toutes les propositions compatibles. */
static void osint_dns_review_dialog_on_select_all_toggled(
GtkCheckButton *button,
gpointer user_data
)
{
OsintDnsReviewDialogContext *context = user_data;
const gboolean selected = gtk_check_button_get_active(button);
for (guint index = 0;
context != NULL && index < context->selection_buttons->len;
index++)
{
GtkWidget *selection_button = g_ptr_array_index(
context->selection_buttons, index
);
if (gtk_widget_get_sensitive(selection_button))
gtk_check_button_set_active(
GTK_CHECK_BUTTON(selection_button), selected
);
}
osint_dns_review_dialog_update_confirmation(context);
}
/** @brief Transmet les propositions cochées puis ferme le dialogue. */
static void osint_dns_review_dialog_on_confirm_clicked(
GtkButton *button,
gpointer user_data
)
{
OsintDnsReviewDialogContext *context = user_data;
GPtrArray *selected_proposals = g_ptr_array_new();
(void) button;
for (guint index = 0;
context != NULL && index < context->selection_buttons->len;
index++)
{
GtkCheckButton *selection_button = g_ptr_array_index(
context->selection_buttons, index
);
if (gtk_widget_get_sensitive(GTK_WIDGET(selection_button)) &&
gtk_check_button_get_active(selection_button))
{
g_ptr_array_add(
selected_proposals,
g_object_get_data(
G_OBJECT(selection_button), "osint-dns-proposal"
)
);
}
}
if (context != NULL && context->callback != NULL &&
selected_proposals->len > 0U)
{
context->callback(selected_proposals, context->user_data);
}
g_ptr_array_unref(selected_proposals);
if (context != NULL) gtk_window_destroy(context->window);
}
/** @brief Crée une ligne décrivant une proposition DNS. */
static GtkWidget *osint_dns_review_dialog_create_row(
OsintDnsReviewDialogContext *context,
const OsintDnsProposal *proposal
)
{
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal);
const gboolean compatible = entity_type != NULL && normalized_value != NULL;
char *title = g_strdup_printf(
"%s → %s",
osint_dns_proposal_get_record_type(proposal),
osint_dns_proposal_get_value(proposal)
);
char *details = g_strdup_printf(
"Cible : %s\nPropriétaire : %s\nType Labfy : %s",
osint_dns_proposal_get_target(proposal),
osint_dns_proposal_get_owner(proposal),
compatible ? entity_type : "non pris en charge"
);
GtkWidget *selection_button = gtk_check_button_new_with_label(title);
GtkWidget *details_label = gtk_label_new(details);
gtk_widget_set_sensitive(selection_button, compatible);
gtk_widget_set_margin_start(details_label, 28);
gtk_widget_set_halign(details_label, GTK_ALIGN_START);
gtk_label_set_xalign(GTK_LABEL(details_label), 0.0F);
gtk_label_set_selectable(GTK_LABEL(details_label), TRUE);
g_object_set_data(
G_OBJECT(selection_button), "osint-dns-proposal", (gpointer) proposal
);
g_signal_connect(
selection_button, "toggled",
G_CALLBACK(osint_dns_review_dialog_on_proposal_toggled), context
);
g_ptr_array_add(context->selection_buttons, selection_button);
gtk_box_append(GTK_BOX(row), selection_button);
gtk_box_append(GTK_BOX(row), details_label);
g_free(normalized_value);
g_free(title);
g_free(details);
return row;
}
void osint_dns_review_dialog_present(
GtkWindow *parent_window,
GPtrArray *proposals,
OsintDnsReviewDialogConfirmCallback callback,
gpointer user_data
)
{
OsintDnsReviewDialogContext *context = NULL;
GtkWidget *main_box = NULL;
GtkWidget *title_label = NULL;
GtkWidget *notice_label = NULL;
GtkWidget *scrolled_window = NULL;
GtkWidget *proposal_box = NULL;
GtkWidget *button_box = NULL;
GtkWidget *cancel_button = NULL;
if (proposals == NULL || proposals->len == 0U) return;
context = g_new0(OsintDnsReviewDialogContext, 1);
context->window = GTK_WINDOW(gtk_window_new());
context->proposals = g_ptr_array_ref(proposals);
context->selection_buttons = g_ptr_array_new();
context->callback = callback;
context->user_data = user_data;
gtk_window_set_title(context->window, "Intégrer les propositions DNS");
gtk_window_set_default_size(context->window, 720, 560);
gtk_window_set_modal(context->window, TRUE);
gtk_window_set_destroy_with_parent(context->window, TRUE);
if (parent_window != NULL)
gtk_window_set_transient_for(context->window, parent_window);
g_object_set_data_full(
G_OBJECT(context->window), "osint-dns-review-context", context,
osint_dns_review_dialog_context_free
);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_start(main_box, 20);
gtk_widget_set_margin_end(main_box, 20);
gtk_widget_set_margin_top(main_box, 20);
gtk_widget_set_margin_bottom(main_box, 20);
title_label = gtk_label_new("Sélectionner les propositions à intégrer");
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
gtk_widget_add_css_class(title_label, "title-2");
notice_label = gtk_label_new(
"Aucune écriture ne sera effectuée avant confirmation. "
"Les types non pris en charge restent visibles mais désactivés."
);
gtk_label_set_wrap(GTK_LABEL(notice_label), TRUE);
gtk_label_set_xalign(GTK_LABEL(notice_label), 0.0F);
context->select_all_button = gtk_check_button_new_with_label(
"Sélectionner toutes les propositions compatibles"
);
g_signal_connect(
context->select_all_button, "toggled",
G_CALLBACK(osint_dns_review_dialog_on_select_all_toggled), context
);
proposal_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
for (guint index = 0; index < proposals->len; index++)
gtk_box_append(
GTK_BOX(proposal_box),
osint_dns_review_dialog_create_row(
context, g_ptr_array_index(proposals, index)
)
);
scrolled_window = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scrolled_window, TRUE);
gtk_scrolled_window_set_child(
GTK_SCROLLED_WINDOW(scrolled_window), proposal_box
);
button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(button_box, GTK_ALIGN_END);
cancel_button = gtk_button_new_with_label("Annuler");
context->confirm_button = gtk_button_new_with_label("Intégrer la sélection");
gtk_widget_add_css_class(context->confirm_button, "suggested-action");
gtk_widget_set_sensitive(context->confirm_button, FALSE);
g_signal_connect(
cancel_button, "clicked",
G_CALLBACK(osint_dns_review_dialog_on_cancel_clicked), context
);
g_signal_connect(
context->confirm_button, "clicked",
G_CALLBACK(osint_dns_review_dialog_on_confirm_clicked), context
);
gtk_box_append(GTK_BOX(button_box), cancel_button);
gtk_box_append(GTK_BOX(button_box), context->confirm_button);
gtk_box_append(GTK_BOX(main_box), title_label);
gtk_box_append(GTK_BOX(main_box), notice_label);
gtk_box_append(GTK_BOX(main_box), context->select_all_button);
gtk_box_append(GTK_BOX(main_box), scrolled_window);
gtk_box_append(GTK_BOX(main_box), button_box);
gtk_window_set_child(context->window, main_box);
gtk_window_present(context->window);
}

View file

@ -0,0 +1,125 @@
/******************************************************************************
* @file test_osint_dns_integration.c
* @brief Tests de l'intégration transactionnelle des propositions DNS.
******************************************************************************/
#include "core/osint_dns_integration.h"
#include "dao/entity_dao.h"
#include "database/database.h"
#include "models/osint_dns_proposal.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_integration_and_duplicates(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-dns-integration-XXXXXX", &error);
char *database_path = g_build_filename(directory, "Enquete.sqlite", NULL);
Database *database = NULL;
EntityDao *entity_dao = NULL;
GPtrArray *proposals = NULL;
GPtrArray *entities = NULL;
guint inserted = 0U;
guint skipped = 0U;
g_assert_no_error(error);
g_assert_true(database_initialize(database_path, "DNS", directory));
database = database_open(database_path);
proposals = osint_dns_proposal_parse(
"example.org",
"example.org. 60 IN A 192.0.2.10\n"
"example.org. 60 IN CNAME WWW.Example.ORG.\n"
"example.org. 60 IN MX 10 mail.example.org.\n"
);
g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error
));
g_assert_no_error(error);
g_assert_cmpuint(inserted, ==, 2);
g_assert_cmpuint(skipped, ==, 1);
g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error
));
g_assert_cmpuint(inserted, ==, 0);
g_assert_cmpuint(skipped, ==, 3);
entity_dao = entity_dao_new(database, &error);
entities = entity_dao_list_all(entity_dao, &error);
g_assert_cmpuint(entities->len, ==, 2);
g_ptr_array_unref(entities);
entity_dao_free(entity_dao);
g_ptr_array_unref(proposals);
database_close(database);
g_assert_cmpint(g_remove(database_path), ==, 0);
g_assert_cmpint(g_rmdir(directory), ==, 0);
g_free(database_path);
g_free(directory);
}
static void test_invalid_arguments(void)
{
GError *error = NULL;
g_assert_false(osint_dns_integration_apply(
NULL, NULL, NULL, NULL, &error
));
g_assert_error(
error, OSINT_DNS_INTEGRATION_ERROR,
OSINT_DNS_INTEGRATION_ERROR_INVALID_ARGUMENT
);
g_clear_error(&error);
}
static void test_normalized_existing_domain_is_skipped(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-dns-existing-XXXXXX", &error);
char *database_path = g_build_filename(directory, "Enquete.sqlite", NULL);
Database *database = NULL;
EntityDao *entity_dao = NULL;
EntityRecord *existing = NULL;
GPtrArray *proposals = NULL;
guint inserted = 0U;
guint skipped = 0U;
g_assert_true(database_initialize(database_path, "DNS", directory));
database = database_open(database_path);
entity_dao = entity_dao_new(database, &error);
existing = entity_record_new(
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "domain_name",
"WWW.Example.ORG.", NULL, NULL, 50, "2026-01-01T00:00:00Z",
"2026-01-01T00:00:00Z", ENTITY_STATUS_ACTIVE, &error
);
g_assert_true(entity_dao_insert(entity_dao, existing, &error));
proposals = osint_dns_proposal_parse(
"example.org", "example.org. 60 IN CNAME www.example.org.\n"
);
g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error
));
g_assert_cmpuint(inserted, ==, 0);
g_assert_cmpuint(skipped, ==, 1);
g_ptr_array_unref(proposals);
entity_record_free(existing);
entity_dao_free(entity_dao);
database_close(database);
g_assert_cmpint(g_remove(database_path), ==, 0);
g_assert_cmpint(g_rmdir(directory), ==, 0);
g_free(database_path);
g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/osint-dns-integration/apply", test_integration_and_duplicates);
g_test_add_func("/osint-dns-integration/invalid", test_invalid_arguments);
g_test_add_func(
"/osint-dns-integration/normalized-duplicate",
test_normalized_existing_domain_is_skipped
);
return g_test_run();
}

View file

@ -49,11 +49,44 @@ static void test_invalid_arguments(void)
g_assert_null(osint_dns_proposal_parse("example.org", NULL));
}
static void test_entity_mapping_and_normalization(void)
{
const char *output =
"example.org. 60 IN A 192.0.2.10\n"
"example.org. 60 IN AAAA 2001:0db8:0:0:0:0:0:1\n"
"example.org. 60 IN CNAME WWW.Example.ORG.\n"
"example.org. 60 IN MX 10 mail.example.org.\n";
GPtrArray *proposals = osint_dns_proposal_parse("example.org", output);
const OsintDnsProposal *ipv4 = g_ptr_array_index(proposals, 0);
const OsintDnsProposal *ipv6 = g_ptr_array_index(proposals, 1);
const OsintDnsProposal *domain = g_ptr_array_index(proposals, 2);
const OsintDnsProposal *unsupported = g_ptr_array_index(proposals, 3);
char *ipv4_value = osint_dns_proposal_dup_normalized_value(ipv4);
char *ipv6_value = osint_dns_proposal_dup_normalized_value(ipv6);
char *domain_value = osint_dns_proposal_dup_normalized_value(domain);
g_assert_cmpstr(osint_dns_proposal_get_entity_type(ipv4), ==, "ip_address");
g_assert_cmpstr(ipv4_value, ==, "192.0.2.10");
g_assert_cmpstr(ipv6_value, ==, "2001:db8::1");
g_assert_cmpstr(osint_dns_proposal_get_entity_type(domain), ==, "domain_name");
g_assert_cmpstr(domain_value, ==, "www.example.org");
g_assert_null(osint_dns_proposal_get_entity_type(unsupported));
g_assert_null(osint_dns_proposal_dup_normalized_value(unsupported));
g_free(ipv4_value);
g_free(ipv6_value);
g_free(domain_value);
g_ptr_array_unref(proposals);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/osint-dns-proposal/recognized", test_parse_recognized_records);
g_test_add_func("/osint-dns-proposal/ignored", test_ignore_unrecognized_lines);
g_test_add_func("/osint-dns-proposal/invalid", test_invalid_arguments);
g_test_add_func(
"/osint-dns-proposal/mapping", test_entity_mapping_and_normalization
);
return g_test_run();
}