feat(osint): create DNS result relations

This commit is contained in:
grayTerminal-sh 2026-07-22 12:29:17 +02:00
parent f6f3e71002
commit 16a5d81e95
15 changed files with 340 additions and 75 deletions

View file

@ -565,7 +565,9 @@ $(TEST_OSINT_DNS_INTEGRATION): \
src/models/osint_dns_proposal.c \ src/models/osint_dns_proposal.c \
src/models/osint_dns_query.c \ src/models/osint_dns_query.c \
src/models/entity_record.c \ src/models/entity_record.c \
src/models/relation_record.c \
src/dao/entity_dao.c \ src/dao/entity_dao.c \
src/dao/relation_dao.c \
src/database/database.c \ src/database/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/statement.c \ src/database/statement.c \

View file

@ -149,7 +149,9 @@ Le socle actuel comprend notamment :
- affichage sélectionnable des sorties standard et derreur, sans persistance ; - 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 - sélection explicite et intégration transactionnelle des propositions DNS
compatibles, avec normalisation et détection des doublons. compatibles, avec normalisation et détection des doublons ;
- création transactionnelle des relations DNS `resolves_to`, `aliases_to` et
`uses_name_server` depuis l'entité interrogée.
Les outils actuellement présents dans le catalogue initial sont : Les outils actuellement présents dans le catalogue initial sont :

View file

@ -1109,8 +1109,10 @@ enregistrées.
### Limite actuelle de la traçabilité OSINT ### Limite actuelle de la traçabilité OSINT
L'intégration DNS initiale peut créer des entités `domain_name` et 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 : `ip_address`, ainsi que les relations `resolves_to`, `aliases_to` et
outil `dns.dig`, cible interrogée et type d'enregistrement. `uses_name_server`. La description de l'entité et la justification de la
relation conservent une indication minimale : outil `dns.dig`, cible
interrogée et type d'enregistrement.
Cette information ne remplace pas une provenance structurée. Une migration Cette information ne remplace pas une provenance structurée. Une migration
future devra introduire un objet de résultat OSINT capable de conserver au future devra introduire un objet de résultat OSINT capable de conserver au

View file

@ -32,25 +32,31 @@ typedef enum
GQuark osint_dns_integration_error_quark(void); GQuark osint_dns_integration_error_quark(void);
/** /**
* @brief Intègre une sélection de propositions dans une transaction unique. * @brief Intègre des propositions et leurs relations dans une transaction.
* *
* Les propositions doivent être des OsintDnsProposal empruntées. Les valeurs * Les propositions doivent être des OsintDnsProposal empruntées. Les valeurs
* incompatibles sont ignorées. Les couples type/valeur déjà présents sont * 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. * comptés comme doublons et ne sont pas réinsérés.
* *
* @param database Connexion SQLite active. * @param database Connexion SQLite active.
* @param source_entity_identifier UUID de l'entité interrogée.
* @param selected_proposals Propositions explicitement sélectionnées. * @param selected_proposals Propositions explicitement sélectionnées.
* @param out_inserted_count Nombre d'entités créées. * @param out_inserted_entity_count Nombre d'entités créées.
* @param out_skipped_count Nombre de propositions ignorées ou déjà présentes. * @param out_skipped_entity_count Nombre d'entités ignorées ou réutilisées.
* @param out_inserted_relation_count Nombre de relations créées.
* @param out_skipped_relation_count Nombre de relations ignorées.
* @param error Emplacement facultatif recevant une erreur. * @param error Emplacement facultatif recevant une erreur.
* *
* @return TRUE lorsque la transaction est validée, sinon FALSE. * @return TRUE lorsque la transaction est validée, sinon FALSE.
*/ */
gboolean osint_dns_integration_apply( gboolean osint_dns_integration_apply(
Database *database, Database *database,
const char *source_entity_identifier,
GPtrArray *selected_proposals, GPtrArray *selected_proposals,
guint *out_inserted_count, guint *out_inserted_entity_count,
guint *out_skipped_count, guint *out_skipped_entity_count,
guint *out_inserted_relation_count,
guint *out_skipped_relation_count,
GError **error GError **error
); );

View file

@ -104,6 +104,20 @@ char *osint_dns_proposal_dup_normalized_value(
const OsintDnsProposal *proposal const OsintDnsProposal *proposal
); );
/**
* @brief Retourne le type de relation correspondant au type DNS.
*
* A, AAAA et PTR utilisent `resolves_to`, CNAME utilise `aliases_to` et NS
* utilise `uses_name_server`.
*
* @param proposal Proposition consultée.
*
* @return Identifiant statique de relation, ou NULL.
*/
const char *osint_dns_proposal_get_relation_type(
const OsintDnsProposal *proposal
);
G_END_DECLS G_END_DECLS
#endif #endif

View file

@ -125,11 +125,13 @@ typedef void (*MainWindowAddRelationCallback)(
* @brief Callback appelé lors du déclenchement d'une action OSINT. * @brief Callback appelé lors du déclenchement d'une action OSINT.
* *
* @param action_identifier Identifiant stable de l'action. * @param action_identifier Identifiant stable de l'action.
* @param target_identifier UUID de la sélection ciblée.
* @param target_value Valeur métier ciblée. * @param target_value Valeur métier ciblée.
* @param user_data Données privées fournies par l'appelant. * @param user_data Données privées fournies par l'appelant.
*/ */
typedef void (*MainWindowOsintActionCallback)( typedef void (*MainWindowOsintActionCallback)(
const char *action_identifier, const char *action_identifier,
const char *target_identifier,
const char *target_value, const char *target_value,
gpointer user_data gpointer user_data
); );

View file

@ -84,11 +84,13 @@ typedef void (*WorkspaceAddRelationCallback)(
* Les chaînes sont empruntées et uniquement valides pendant l'appel. * Les chaînes sont empruntées et uniquement valides pendant l'appel.
* *
* @param action_identifier Identifiant stable de l'action. * @param action_identifier Identifiant stable de l'action.
* @param target_identifier UUID de la sélection ciblée.
* @param target_value Valeur métier ciblée par l'action. * @param target_value Valeur métier ciblée par l'action.
* @param user_data Données empruntées fournies par l'appelant. * @param user_data Données empruntées fournies par l'appelant.
*/ */
typedef void (*WorkspaceOsintActionCallback)( typedef void (*WorkspaceOsintActionCallback)(
const char *action_identifier, const char *action_identifier,
const char *target_identifier,
const char *target_value, const char *target_value,
gpointer user_data gpointer user_data
); );

View file

@ -162,6 +162,7 @@ typedef struct
typedef struct typedef struct
{ {
Application *application; Application *application;
char *target_identifier;
char *target_value; char *target_value;
} ApplicationOsintActionContext; } ApplicationOsintActionContext;
@ -169,6 +170,7 @@ typedef struct
typedef struct typedef struct
{ {
Application *application; Application *application;
char *source_entity_identifier;
GPtrArray *proposals; GPtrArray *proposals;
} ApplicationOsintReviewContext; } ApplicationOsintReviewContext;
@ -189,6 +191,7 @@ static void application_osint_review_context_free(gpointer user_data)
ApplicationOsintReviewContext *context = user_data; ApplicationOsintReviewContext *context = user_data;
if (context == NULL) return; if (context == NULL) return;
g_clear_pointer(&context->proposals, g_ptr_array_unref); g_clear_pointer(&context->proposals, g_ptr_array_unref);
g_free(context->source_entity_identifier);
g_free(context); g_free(context);
} }
@ -198,16 +201,21 @@ static void application_on_osint_integration_confirmed(
gpointer user_data gpointer user_data
) )
{ {
Application *application = user_data; ApplicationOsintReviewContext *review_context = user_data;
Application *application = review_context != NULL
? review_context->application : NULL;
const InvestigationProject *project = NULL; const InvestigationProject *project = NULL;
Database *database = NULL; Database *database = NULL;
const char *database_path = NULL; const char *database_path = NULL;
guint inserted_count = 0U; guint inserted_count = 0U;
guint skipped_count = 0U; guint skipped_count = 0U;
guint inserted_relation_count = 0U;
guint skipped_relation_count = 0U;
char *message = NULL; char *message = NULL;
GError *error = NULL; GError *error = NULL;
if (application == NULL || application->session == NULL || if (application == NULL || review_context->source_entity_identifier == NULL ||
application->session == NULL ||
application->main_window == NULL) application->main_window == NULL)
{ {
return; return;
@ -218,8 +226,9 @@ static void application_on_osint_integration_confirmed(
database_path = project != NULL database_path = project != NULL
? investigation_project_get_database_path(project) : NULL; ? investigation_project_get_database_path(project) : NULL;
if (!osint_dns_integration_apply( if (!osint_dns_integration_apply(
database, selected_proposals, &inserted_count, &skipped_count, database, review_context->source_entity_identifier,
&error selected_proposals, &inserted_count, &skipped_count,
&inserted_relation_count, &skipped_relation_count, &error
)) ))
{ {
application_present_error( application_present_error(
@ -233,10 +242,12 @@ static void application_on_osint_integration_confirmed(
} }
message = g_strdup_printf( message = g_strdup_printf(
"%u entité(s) créée(s), %u proposition(s) ignorée(s).\n\n" "%u entité(s) créée(s), %u entité(s) réutilisée(s) ou ignorée(s).\n"
"%u relation(s) créée(s), %u relation(s) ignorée(s).\n\n"
"La provenance complète devra être ajoutée par une future migration " "La provenance complète devra être ajoutée par une future migration "
"du schéma OSINT.", "du schéma OSINT.",
inserted_count, skipped_count inserted_count, skipped_count,
inserted_relation_count, skipped_relation_count
); );
application_message_dialog_present( application_message_dialog_present(
main_window_get_window(application->main_window), main_window_get_window(application->main_window),
@ -260,7 +271,7 @@ static void application_on_osint_prepare_integration(gpointer user_data)
main_window_get_window(context->application->main_window), main_window_get_window(context->application->main_window),
context->proposals, context->proposals,
application_on_osint_integration_confirmed, application_on_osint_integration_confirmed,
context->application context
); );
} }
@ -279,6 +290,7 @@ static void application_osint_action_context_free(
} }
g_free(context->target_value); g_free(context->target_value);
g_free(context->target_identifier);
g_free(context); g_free(context);
} }
@ -1178,9 +1190,13 @@ static void application_on_dns_lookup_completed(
if (review_context != NULL) if (review_context != NULL)
{ {
review_context->application = application; review_context->application = application;
review_context->source_entity_identifier = g_strdup(
context->target_identifier
);
review_context->proposals = g_ptr_array_ref(proposals); review_context->proposals = g_ptr_array_ref(proposals);
} }
if (review_context == NULL || review_context->proposals == NULL) if (review_context == NULL || review_context->proposals == NULL ||
review_context->source_entity_identifier == NULL)
{ {
application_osint_review_context_free(review_context); application_osint_review_context_free(review_context);
review_context = NULL; review_context = NULL;
@ -1224,6 +1240,7 @@ static void application_on_dns_lookup_completed(
*/ */
static void application_start_dns_lookup( static void application_start_dns_lookup(
Application *application, Application *application,
const char *target_identifier,
const char *target_value const char *target_value
) )
{ {
@ -1236,6 +1253,7 @@ static void application_start_dns_lookup(
if (application == NULL || application->task_manager == NULL || if (application == NULL || application->task_manager == NULL ||
application->tool_initializer == NULL || target_value == NULL || application->tool_initializer == NULL || target_value == NULL ||
target_identifier == NULL || !g_uuid_string_is_valid(target_identifier) ||
!osint_dns_query_is_valid_target(target_value)) !osint_dns_query_is_valid_target(target_value))
{ {
if (application != NULL) if (application != NULL)
@ -1274,10 +1292,12 @@ static void application_start_dns_lookup(
if (context != NULL) if (context != NULL)
{ {
context->application = application; context->application = application;
context->target_identifier = g_strdup(target_identifier);
context->target_value = g_strdup(target_value); context->target_value = g_strdup(target_value);
} }
if (context == NULL || context->target_value == NULL) if (context == NULL || context->target_identifier == NULL ||
context->target_value == NULL)
{ {
application_osint_action_context_free(context); application_osint_action_context_free(context);
tool_task_free(tool_task); tool_task_free(tool_task);
@ -1319,6 +1339,7 @@ static void application_start_dns_lookup(
*/ */
static void application_on_osint_action_requested( static void application_on_osint_action_requested(
const char *action_identifier, const char *action_identifier,
const char *target_identifier,
const char *target_value, const char *target_value,
gpointer user_data gpointer user_data
) )
@ -1327,7 +1348,9 @@ static void application_on_osint_action_requested(
if (g_strcmp0(action_identifier, "dns-preview") == 0) if (g_strcmp0(action_identifier, "dns-preview") == 0)
{ {
application_start_dns_lookup(application, target_value); application_start_dns_lookup(
application, target_identifier, target_value
);
return; return;
} }

View file

@ -6,10 +6,12 @@
#include "core/osint_dns_integration.h" #include "core/osint_dns_integration.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/relation_dao.h"
#include "database/error.h" #include "database/error.h"
#include "database/transaction.h" #include "database/transaction.h"
#include "models/entity_record.h" #include "models/entity_record.h"
#include "models/osint_dns_proposal.h" #include "models/osint_dns_proposal.h"
#include "models/relation_record.h"
#include <gio/gio.h> #include <gio/gio.h>
#include <string.h> #include <string.h>
@ -50,6 +52,18 @@ static char *osint_dns_integration_build_key(
return g_strdup_printf("%s\n%s", entity_type, value); return g_strdup_printf("%s\n%s", entity_type, value);
} }
/** @brief Construit la clé anti-doublon d'une relation orientée. */
static char *osint_dns_integration_build_relation_key(
const char *source_identifier,
const char *target_identifier,
const char *relation_type
)
{
return g_strdup_printf(
"%s\n%s\n%s", source_identifier, target_identifier, relation_type
);
}
/** @brief Renseigne une erreur littérale si possible. */ /** @brief Renseigne une erreur littérale si possible. */
static void osint_dns_integration_set_error( static void osint_dns_integration_set_error(
GError **error, GError **error,
@ -68,25 +82,38 @@ GQuark osint_dns_integration_error_quark(void)
gboolean osint_dns_integration_apply( gboolean osint_dns_integration_apply(
Database *database, Database *database,
const char *source_entity_identifier,
GPtrArray *selected_proposals, GPtrArray *selected_proposals,
guint *out_inserted_count, guint *out_inserted_entity_count,
guint *out_skipped_count, guint *out_skipped_entity_count,
guint *out_inserted_relation_count,
guint *out_skipped_relation_count,
GError **error GError **error
) )
{ {
EntityDao *entity_dao = NULL; EntityDao *entity_dao = NULL;
RelationDao *relation_dao = NULL;
GPtrArray *existing_entities = NULL; GPtrArray *existing_entities = NULL;
GPtrArray *existing_relations = NULL;
GHashTable *known_entities = NULL; GHashTable *known_entities = NULL;
GHashTable *known_relations = NULL;
EntityRecord *source_entity = NULL;
GDateTime *now = NULL; GDateTime *now = NULL;
char *timestamp = NULL; char *timestamp = NULL;
guint inserted_count = 0U; guint inserted_entity_count = 0U;
guint skipped_count = 0U; guint skipped_entity_count = 0U;
guint inserted_relation_count = 0U;
guint skipped_relation_count = 0U;
gboolean success = FALSE; gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_inserted_count != NULL) *out_inserted_count = 0U; if (out_inserted_entity_count != NULL) *out_inserted_entity_count = 0U;
if (out_skipped_count != NULL) *out_skipped_count = 0U; if (out_skipped_entity_count != NULL) *out_skipped_entity_count = 0U;
if (database == NULL || selected_proposals == NULL || if (out_inserted_relation_count != NULL) *out_inserted_relation_count = 0U;
if (out_skipped_relation_count != NULL) *out_skipped_relation_count = 0U;
if (database == NULL || source_entity_identifier == NULL ||
!g_uuid_string_is_valid(source_entity_identifier) ||
selected_proposals == NULL ||
selected_proposals->len == 0U) selected_proposals->len == 0U)
{ {
osint_dns_integration_set_error( osint_dns_integration_set_error(
@ -98,9 +125,27 @@ gboolean osint_dns_integration_apply(
entity_dao = entity_dao_new(database, error); entity_dao = entity_dao_new(database, error);
if (entity_dao == NULL) return FALSE; if (entity_dao == NULL) return FALSE;
relation_dao = relation_dao_new(database, error);
if (relation_dao == NULL) goto cleanup;
source_entity = entity_dao_find_by_identifier(
entity_dao, source_entity_identifier, error
);
if (source_entity == NULL)
{
if (error == NULL || *error == NULL)
osint_dns_integration_set_error(
error, OSINT_DNS_INTEGRATION_ERROR_INVALID_ARGUMENT,
"L'entité source de la résolution DNS est introuvable."
);
goto cleanup;
}
existing_entities = entity_dao_list_all(entity_dao, error); existing_entities = entity_dao_list_all(entity_dao, error);
if (existing_entities == NULL) goto cleanup; if (existing_entities == NULL) goto cleanup;
known_entities = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); existing_relations = relation_dao_list_all(relation_dao, error);
if (existing_relations == NULL) goto cleanup;
known_entities = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, g_free
);
for (guint index = 0; index < existing_entities->len; index++) for (guint index = 0; index < existing_entities->len; index++)
{ {
const EntityRecord *entity = g_ptr_array_index(existing_entities, index); const EntityRecord *entity = g_ptr_array_index(existing_entities, index);
@ -108,15 +153,33 @@ gboolean osint_dns_integration_apply(
char *normalized_value = osint_dns_integration_normalize_existing_value( char *normalized_value = osint_dns_integration_normalize_existing_value(
entity_type, entity_record_get_value(entity) entity_type, entity_record_get_value(entity)
); );
g_hash_table_add( g_hash_table_insert(
known_entities, known_entities,
osint_dns_integration_build_key( osint_dns_integration_build_key(
entity_type, entity_type,
normalized_value normalized_value
) ),
g_strdup(entity_record_get_identifier(entity))
); );
g_free(normalized_value); g_free(normalized_value);
} }
known_relations = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, NULL
);
for (guint index = 0; index < existing_relations->len; index++)
{
const RelationRecord *relation = g_ptr_array_index(
existing_relations, index
);
g_hash_table_add(
known_relations,
osint_dns_integration_build_relation_key(
relation_record_get_source_entity_identifier(relation),
relation_record_get_target_entity_identifier(relation),
relation_record_get_relation_type(relation)
)
);
}
now = g_date_time_new_now_utc(); now = g_date_time_new_now_utc();
timestamp = now != NULL timestamp = now != NULL
@ -146,60 +209,131 @@ gboolean osint_dns_integration_apply(
selected_proposals, index selected_proposals, index
); );
const char *entity_type = osint_dns_proposal_get_entity_type(proposal); const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
const char *relation_type =
osint_dns_proposal_get_relation_type(proposal);
char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal); char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal);
char *entity_key = NULL; char *entity_key = NULL;
char *identifier = NULL; char *target_identifier = NULL;
char *description = NULL; char *description = NULL;
EntityRecord *entity = NULL; EntityRecord *entity = NULL;
RelationRecord *relation = NULL;
char *relation_identifier = NULL;
char *relation_key = NULL;
char *relation_label = NULL;
char *relation_justification = NULL;
GError *model_error = NULL; GError *model_error = NULL;
if (entity_type == NULL || normalized_value == NULL) if (entity_type == NULL || relation_type == NULL ||
normalized_value == NULL)
{ {
skipped_count++; skipped_entity_count++;
skipped_relation_count++;
g_free(normalized_value); g_free(normalized_value);
continue; continue;
} }
entity_key = osint_dns_integration_build_key(entity_type, normalized_value); entity_key = osint_dns_integration_build_key(entity_type, normalized_value);
if (g_hash_table_contains(known_entities, entity_key)) target_identifier = g_strdup(
g_hash_table_lookup(known_entities, entity_key)
);
if (target_identifier != NULL)
{ {
skipped_count++; skipped_entity_count++;
g_free(entity_key);
g_free(normalized_value);
continue;
} }
else
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) target_identifier = g_uuid_string_random();
g_propagate_error(error, g_steal_pointer(&model_error)); 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(
target_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(target_identifier);
g_free(entity_key);
g_free(normalized_value);
database_transaction_rollback(database);
goto cleanup;
}
g_hash_table_insert(
known_entities, entity_key, g_strdup(target_identifier)
);
entity_key = NULL;
inserted_entity_count++;
entity_record_free(entity); entity_record_free(entity);
g_clear_error(&model_error); g_clear_error(&model_error);
g_free(description); 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); relation_key = osint_dns_integration_build_relation_key(
inserted_count++; source_entity_identifier, target_identifier, relation_type
entity_record_free(entity); );
g_clear_error(&model_error); if (g_strcmp0(source_entity_identifier, target_identifier) == 0 ||
g_free(description); g_hash_table_contains(known_relations, relation_key))
g_free(identifier); {
skipped_relation_count++;
}
else
{
relation_identifier = g_uuid_string_random();
relation_label = g_strdup_printf(
"%s — %s → %s",
osint_dns_proposal_get_target(proposal), relation_type,
normalized_value
);
relation_justification = g_strdup_printf(
"Relation 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)
);
relation = relation_record_new(
relation_identifier, source_entity_identifier,
target_identifier, relation_type, relation_label,
relation_justification, 50, timestamp, timestamp,
RELATION_STATUS_ACTIVE, &model_error
);
if (relation == NULL ||
!relation_dao_insert(relation_dao, relation, error))
{
if (error != NULL && *error == NULL && model_error != NULL)
g_propagate_error(error, g_steal_pointer(&model_error));
relation_record_free(relation);
g_clear_error(&model_error);
g_free(relation_identifier);
g_free(relation_label);
g_free(relation_justification);
g_free(relation_key);
g_free(target_identifier);
g_free(entity_key);
g_free(normalized_value);
database_transaction_rollback(database);
goto cleanup;
}
g_hash_table_add(known_relations, relation_key);
relation_key = NULL;
inserted_relation_count++;
relation_record_free(relation);
g_clear_error(&model_error);
g_free(relation_identifier);
g_free(relation_label);
g_free(relation_justification);
}
g_free(relation_key);
g_free(target_identifier);
g_free(entity_key);
g_free(normalized_value); g_free(normalized_value);
} }
@ -215,14 +349,24 @@ gboolean osint_dns_integration_apply(
goto cleanup; goto cleanup;
} }
success = TRUE; success = TRUE;
if (out_inserted_count != NULL) *out_inserted_count = inserted_count; if (out_inserted_entity_count != NULL)
if (out_skipped_count != NULL) *out_skipped_count = skipped_count; *out_inserted_entity_count = inserted_entity_count;
if (out_skipped_entity_count != NULL)
*out_skipped_entity_count = skipped_entity_count;
if (out_inserted_relation_count != NULL)
*out_inserted_relation_count = inserted_relation_count;
if (out_skipped_relation_count != NULL)
*out_skipped_relation_count = skipped_relation_count;
cleanup: cleanup:
g_free(timestamp); g_free(timestamp);
g_clear_pointer(&now, g_date_time_unref); g_clear_pointer(&now, g_date_time_unref);
g_clear_pointer(&known_entities, g_hash_table_unref); g_clear_pointer(&known_entities, g_hash_table_unref);
g_clear_pointer(&known_relations, g_hash_table_unref);
g_clear_pointer(&existing_entities, g_ptr_array_unref); g_clear_pointer(&existing_entities, g_ptr_array_unref);
g_clear_pointer(&existing_relations, g_ptr_array_unref);
entity_record_free(source_entity);
relation_dao_free(relation_dao);
entity_dao_free(entity_dao); entity_dao_free(entity_dao);
return success; return success;
} }

View file

@ -210,3 +210,19 @@ char *osint_dns_proposal_dup_normalized_value(
} }
return NULL; return NULL;
} }
const char *osint_dns_proposal_get_relation_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 ||
g_strcmp0(record_type, "PTR") == 0)
return "resolves_to";
if (g_strcmp0(record_type, "CNAME") == 0)
return "aliases_to";
if (g_strcmp0(record_type, "NS") == 0)
return "uses_name_server";
return NULL;
}

View file

@ -136,6 +136,7 @@ struct MainWindow
*/ */
static void main_window_on_osint_action_requested( static void main_window_on_osint_action_requested(
const char *action_identifier, const char *action_identifier,
const char *target_identifier,
const char *target_value, const char *target_value,
gpointer user_data gpointer user_data
) )
@ -149,6 +150,7 @@ static void main_window_on_osint_action_requested(
main_window->osint_action_callback( main_window->osint_action_callback(
action_identifier, action_identifier,
target_identifier,
target_value, target_value,
main_window->osint_action_user_data main_window->osint_action_user_data
); );

View file

@ -142,18 +142,25 @@ static GtkWidget *osint_dns_review_dialog_create_row(
{ {
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); GtkWidget *row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
const char *entity_type = osint_dns_proposal_get_entity_type(proposal); const char *entity_type = osint_dns_proposal_get_entity_type(proposal);
const char *relation_type = osint_dns_proposal_get_relation_type(proposal);
char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal); char *normalized_value = osint_dns_proposal_dup_normalized_value(proposal);
const gboolean compatible = entity_type != NULL && normalized_value != NULL; const gboolean compatible = entity_type != NULL && relation_type != NULL &&
normalized_value != NULL;
char *title = g_strdup_printf( char *title = g_strdup_printf(
"%s → %s", "%s → %s",
osint_dns_proposal_get_record_type(proposal), osint_dns_proposal_get_record_type(proposal),
osint_dns_proposal_get_value(proposal) osint_dns_proposal_get_value(proposal)
); );
char *details = g_strdup_printf( char *details = g_strdup_printf(
"Cible : %s\nPropriétaire : %s\nType Labfy : %s", "Cible : %s\nPropriétaire : %s\nType Labfy : %s\n"
"Relation prévue : %s — %s → %s",
osint_dns_proposal_get_target(proposal), osint_dns_proposal_get_target(proposal),
osint_dns_proposal_get_owner(proposal), osint_dns_proposal_get_owner(proposal),
compatible ? entity_type : "non pris en charge" compatible ? entity_type : "non pris en charge",
osint_dns_proposal_get_target(proposal),
compatible ? relation_type : "non prise en charge",
normalized_value != NULL ? normalized_value
: osint_dns_proposal_get_value(proposal)
); );
GtkWidget *selection_button = gtk_check_button_new_with_label(title); GtkWidget *selection_button = gtk_check_button_new_with_label(title);
GtkWidget *details_label = gtk_label_new(details); GtkWidget *details_label = gtk_label_new(details);

View file

@ -147,6 +147,7 @@ static void workspace_on_osint_action_clicked(
"osint-action-identifier" "osint-action-identifier"
); );
const char *target_value = NULL; const char *target_value = NULL;
const char *target_identifier = NULL;
if (workspace == NULL || workspace->osint_tools_status_label == NULL) if (workspace == NULL || workspace->osint_tools_status_label == NULL)
{ {
@ -165,9 +166,13 @@ static void workspace_on_osint_action_clicked(
target_value = osint_selection_context_get_value( target_value = osint_selection_context_get_value(
workspace->osint_selection_context workspace->osint_selection_context
); );
target_identifier = osint_selection_context_get_identifier(
workspace->osint_selection_context
);
if (workspace->osint_action_callback == NULL || if (workspace->osint_action_callback == NULL ||
action_identifier == NULL || target_value == NULL) action_identifier == NULL || target_identifier == NULL ||
target_value == NULL)
{ {
gtk_label_set_text( gtk_label_set_text(
GTK_LABEL(workspace->osint_tools_status_label), GTK_LABEL(workspace->osint_tools_status_label),
@ -187,6 +192,7 @@ static void workspace_on_osint_action_clicked(
workspace->osint_action_callback( workspace->osint_action_callback(
action_identifier, action_identifier,
target_identifier,
target_value, target_value,
workspace->osint_action_user_data workspace->osint_action_user_data
); );

View file

@ -6,8 +6,10 @@
#include "core/osint_dns_integration.h" #include "core/osint_dns_integration.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/relation_dao.h"
#include "database/database.h" #include "database/database.h"
#include "models/osint_dns_proposal.h" #include "models/osint_dns_proposal.h"
#include "models/relation_record.h"
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
@ -19,14 +21,26 @@ static void test_integration_and_duplicates(void)
char *database_path = g_build_filename(directory, "Enquete.sqlite", NULL); char *database_path = g_build_filename(directory, "Enquete.sqlite", NULL);
Database *database = NULL; Database *database = NULL;
EntityDao *entity_dao = NULL; EntityDao *entity_dao = NULL;
RelationDao *relation_dao = NULL;
EntityRecord *source = NULL;
GPtrArray *proposals = NULL; GPtrArray *proposals = NULL;
GPtrArray *entities = NULL; GPtrArray *entities = NULL;
GPtrArray *relations = NULL;
guint inserted = 0U; guint inserted = 0U;
guint skipped = 0U; guint skipped = 0U;
guint inserted_relations = 0U;
guint skipped_relations = 0U;
g_assert_no_error(error); g_assert_no_error(error);
g_assert_true(database_initialize(database_path, "DNS", directory)); g_assert_true(database_initialize(database_path, "DNS", directory));
database = database_open(database_path); database = database_open(database_path);
entity_dao = entity_dao_new(database, &error);
source = entity_record_new(
"11111111-1111-4111-8111-111111111111", "domain_name",
"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, source, &error));
proposals = osint_dns_proposal_parse( proposals = osint_dns_proposal_parse(
"example.org", "example.org",
"example.org. 60 IN A 192.0.2.10\n" "example.org. 60 IN A 192.0.2.10\n"
@ -34,22 +48,33 @@ static void test_integration_and_duplicates(void)
"example.org. 60 IN MX 10 mail.example.org.\n" "example.org. 60 IN MX 10 mail.example.org.\n"
); );
g_assert_true(osint_dns_integration_apply( g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error database, "11111111-1111-4111-8111-111111111111", proposals,
&inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_no_error(error); g_assert_no_error(error);
g_assert_cmpuint(inserted, ==, 2); g_assert_cmpuint(inserted, ==, 2);
g_assert_cmpuint(skipped, ==, 1); g_assert_cmpuint(skipped, ==, 1);
g_assert_cmpuint(inserted_relations, ==, 2);
g_assert_cmpuint(skipped_relations, ==, 1);
g_assert_true(osint_dns_integration_apply( g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error database, "11111111-1111-4111-8111-111111111111", proposals,
&inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_cmpuint(inserted, ==, 0); g_assert_cmpuint(inserted, ==, 0);
g_assert_cmpuint(skipped, ==, 3); g_assert_cmpuint(skipped, ==, 3);
entity_dao = entity_dao_new(database, &error); g_assert_cmpuint(inserted_relations, ==, 0);
g_assert_cmpuint(skipped_relations, ==, 3);
entities = entity_dao_list_all(entity_dao, &error); entities = entity_dao_list_all(entity_dao, &error);
g_assert_cmpuint(entities->len, ==, 2); g_assert_cmpuint(entities->len, ==, 3);
relation_dao = relation_dao_new(database, &error);
relations = relation_dao_list_all(relation_dao, &error);
g_assert_cmpuint(relations->len, ==, 2);
g_ptr_array_unref(relations);
relation_dao_free(relation_dao);
g_ptr_array_unref(entities); g_ptr_array_unref(entities);
entity_record_free(source);
entity_dao_free(entity_dao); entity_dao_free(entity_dao);
g_ptr_array_unref(proposals); g_ptr_array_unref(proposals);
database_close(database); database_close(database);
@ -63,7 +88,7 @@ static void test_invalid_arguments(void)
{ {
GError *error = NULL; GError *error = NULL;
g_assert_false(osint_dns_integration_apply( g_assert_false(osint_dns_integration_apply(
NULL, NULL, NULL, NULL, &error NULL, NULL, NULL, NULL, NULL, NULL, NULL, &error
)); ));
g_assert_error( g_assert_error(
error, OSINT_DNS_INTEGRATION_ERROR, error, OSINT_DNS_INTEGRATION_ERROR,
@ -83,6 +108,8 @@ static void test_normalized_existing_domain_is_skipped(void)
GPtrArray *proposals = NULL; GPtrArray *proposals = NULL;
guint inserted = 0U; guint inserted = 0U;
guint skipped = 0U; guint skipped = 0U;
guint inserted_relations = 0U;
guint skipped_relations = 0U;
g_assert_true(database_initialize(database_path, "DNS", directory)); g_assert_true(database_initialize(database_path, "DNS", directory));
database = database_open(database_path); database = database_open(database_path);
@ -97,10 +124,13 @@ static void test_normalized_existing_domain_is_skipped(void)
"example.org", "example.org. 60 IN CNAME www.example.org.\n" "example.org", "example.org. 60 IN CNAME www.example.org.\n"
); );
g_assert_true(osint_dns_integration_apply( g_assert_true(osint_dns_integration_apply(
database, proposals, &inserted, &skipped, &error database, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", proposals,
&inserted, &skipped, &inserted_relations, &skipped_relations, &error
)); ));
g_assert_cmpuint(inserted, ==, 0); g_assert_cmpuint(inserted, ==, 0);
g_assert_cmpuint(skipped, ==, 1); g_assert_cmpuint(skipped, ==, 1);
g_assert_cmpuint(inserted_relations, ==, 0);
g_assert_cmpuint(skipped_relations, ==, 1);
g_ptr_array_unref(proposals); g_ptr_array_unref(proposals);
entity_record_free(existing); entity_record_free(existing);

View file

@ -67,10 +67,17 @@ static void test_entity_mapping_and_normalization(void)
g_assert_cmpstr(osint_dns_proposal_get_entity_type(ipv4), ==, "ip_address"); g_assert_cmpstr(osint_dns_proposal_get_entity_type(ipv4), ==, "ip_address");
g_assert_cmpstr(ipv4_value, ==, "192.0.2.10"); g_assert_cmpstr(ipv4_value, ==, "192.0.2.10");
g_assert_cmpstr(
osint_dns_proposal_get_relation_type(ipv4), ==, "resolves_to"
);
g_assert_cmpstr(ipv6_value, ==, "2001:db8::1"); g_assert_cmpstr(ipv6_value, ==, "2001:db8::1");
g_assert_cmpstr(osint_dns_proposal_get_entity_type(domain), ==, "domain_name"); g_assert_cmpstr(osint_dns_proposal_get_entity_type(domain), ==, "domain_name");
g_assert_cmpstr(domain_value, ==, "www.example.org"); g_assert_cmpstr(domain_value, ==, "www.example.org");
g_assert_cmpstr(
osint_dns_proposal_get_relation_type(domain), ==, "aliases_to"
);
g_assert_null(osint_dns_proposal_get_entity_type(unsupported)); g_assert_null(osint_dns_proposal_get_entity_type(unsupported));
g_assert_null(osint_dns_proposal_get_relation_type(unsupported));
g_assert_null(osint_dns_proposal_dup_normalized_value(unsupported)); g_assert_null(osint_dns_proposal_dup_normalized_value(unsupported));
g_free(ipv4_value); g_free(ipv4_value);