feat(entities): add social media accounts
This commit is contained in:
parent
d876590529
commit
e91c5f5ee0
18 changed files with 934 additions and 9 deletions
21
Makefile
21
Makefile
|
|
@ -121,6 +121,7 @@ TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration
|
||||||
TEST_OSINT_EXECUTION_DAO := tests/test_osint_execution_dao
|
TEST_OSINT_EXECUTION_DAO := tests/test_osint_execution_dao
|
||||||
TEST_OSINT_EXECUTION_INTEGRITY := tests/test_osint_execution_integrity
|
TEST_OSINT_EXECUTION_INTEGRITY := tests/test_osint_execution_integrity
|
||||||
TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
|
TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
|
||||||
|
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
|
|
@ -609,6 +610,19 @@ $(TEST_EVIDENCE_RECLASSIFICATION): \
|
||||||
src/database/error.c
|
src/database/error.c
|
||||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||||
|
|
||||||
|
$(TEST_SOCIAL_ACCOUNT_SERVICE): \
|
||||||
|
tests/test_social_account_service.c \
|
||||||
|
src/core/social_account_service.c \
|
||||||
|
src/dao/entity_dao.c \
|
||||||
|
src/dao/evidence_entity_dao.c \
|
||||||
|
src/models/entity_record.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): \
|
$(TEST_INVESTIGATION_GRAPH_LOAD_TASK): \
|
||||||
tests/test_investigation_graph_load_task.c \
|
tests/test_investigation_graph_load_task.c \
|
||||||
src/core/investigation_graph_load_task.c \
|
src/core/investigation_graph_load_task.c \
|
||||||
|
|
@ -684,7 +698,8 @@ test: \
|
||||||
$(TEST_OSINT_DNS_INTEGRATION) \
|
$(TEST_OSINT_DNS_INTEGRATION) \
|
||||||
$(TEST_OSINT_EXECUTION_DAO) \
|
$(TEST_OSINT_EXECUTION_DAO) \
|
||||||
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
||||||
$(TEST_EVIDENCE_RECLASSIFICATION)
|
$(TEST_EVIDENCE_RECLASSIFICATION) \
|
||||||
|
$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||||
@echo "Exécution des tests..."
|
@echo "Exécution des tests..."
|
||||||
@./$(TEST_NODE)
|
@./$(TEST_NODE)
|
||||||
@./$(TEST_TREE_MODEL)
|
@./$(TEST_TREE_MODEL)
|
||||||
|
|
@ -740,6 +755,7 @@ test: \
|
||||||
@$(TEST_OSINT_EXECUTION_DAO)
|
@$(TEST_OSINT_EXECUTION_DAO)
|
||||||
@$(TEST_OSINT_EXECUTION_INTEGRITY)
|
@$(TEST_OSINT_EXECUTION_INTEGRITY)
|
||||||
@$(TEST_EVIDENCE_RECLASSIFICATION)
|
@$(TEST_EVIDENCE_RECLASSIFICATION)
|
||||||
|
@$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||||
@echo "Tous les tests sont valides."
|
@echo "Tous les tests sont valides."
|
||||||
|
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|
@ -801,7 +817,8 @@ clean:
|
||||||
$(TEST_OSINT_DNS_INTEGRATION) \
|
$(TEST_OSINT_DNS_INTEGRATION) \
|
||||||
$(TEST_OSINT_EXECUTION_DAO) \
|
$(TEST_OSINT_EXECUTION_DAO) \
|
||||||
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
$(TEST_OSINT_EXECUTION_INTEGRITY) \
|
||||||
$(TEST_EVIDENCE_RECLASSIFICATION)
|
$(TEST_EVIDENCE_RECLASSIFICATION) \
|
||||||
|
$(TEST_SOCIAL_ACCOUNT_SERVICE)
|
||||||
|
|
||||||
-include $(DEP)
|
-include $(DEP)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,9 @@ Le socle actuel comprend notamment :
|
||||||
`uses_name_server` depuis l'entité interrogée ;
|
`uses_name_server` depuis l'entité interrogée ;
|
||||||
- provenance OSINT SQLite V3 conservant les arguments, sorties brutes,
|
- provenance OSINT SQLite V3 conservant les arguments, sorties brutes,
|
||||||
empreinte SHA-256 et liaisons vers les entités et relations intégrées ;
|
empreinte SHA-256 et liaisons vers les entités et relations intégrées ;
|
||||||
|
- comptes sociaux structurés en SQLite V4 (TikTok, Instagram, Facebook, X,
|
||||||
|
Telegram ou autre), avec URL, pseudonyme, identifiant stable facultatif,
|
||||||
|
première observation, état, notes et rattachement à une preuve ;
|
||||||
- historique OSINT contextuel en lecture seule avec détail des exécutions,
|
- historique OSINT contextuel en lecture seule avec détail des exécutions,
|
||||||
sorties standard et d'erreur, et objets créés ou réutilisés ;
|
sorties standard et d'erreur, et objets créés ou réutilisés ;
|
||||||
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans
|
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/******************************************************************************
|
/******************************************************************************
|
||||||
* Labfy Investigation
|
* Labfy Investigation
|
||||||
*
|
*
|
||||||
* Extensions idempotentes du schéma SQLite courant V3
|
* Extensions idempotentes du schéma SQLite courant V4
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
39
database/schema_v4.sql
Normal file
39
database/schema_v4.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* Labfy Investigation
|
||||||
|
*
|
||||||
|
* Migration du schéma SQLite V3 vers V4 : comptes de réseaux sociaux
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
INSERT INTO types_entite (code, label, description) VALUES
|
||||||
|
('tiktok_account', 'Compte TikTok', NULL),
|
||||||
|
('x_account', 'Compte X', NULL),
|
||||||
|
('telegram_account', 'Compte Telegram', NULL),
|
||||||
|
('social_account', 'Autre compte social', NULL);
|
||||||
|
|
||||||
|
CREATE TABLE comptes_sociaux
|
||||||
|
(
|
||||||
|
entite_id TEXT PRIMARY KEY,
|
||||||
|
plateforme TEXT NOT NULL,
|
||||||
|
url_profil TEXT NOT NULL,
|
||||||
|
pseudonyme TEXT NOT NULL,
|
||||||
|
identifiant_plateforme TEXT,
|
||||||
|
premiere_observation TEXT NOT NULL,
|
||||||
|
etat_compte TEXT NOT NULL DEFAULT 'unknown',
|
||||||
|
notes TEXT,
|
||||||
|
|
||||||
|
FOREIGN KEY (entite_id)
|
||||||
|
REFERENCES entites(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
UNIQUE (plateforme, url_profil),
|
||||||
|
|
||||||
|
CHECK (plateforme IN ('tiktok', 'instagram', 'facebook', 'x', 'telegram', 'other')),
|
||||||
|
CHECK (length(trim(url_profil)) > 0),
|
||||||
|
CHECK (length(trim(pseudonyme)) > 0),
|
||||||
|
CHECK (length(premiere_observation) = 20),
|
||||||
|
CHECK (etat_compte IN ('active', 'private', 'suspended', 'deleted', 'unknown'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_comptes_sociaux_pseudonyme
|
||||||
|
ON comptes_sociaux(plateforme, pseudonyme);
|
||||||
|
|
@ -1148,6 +1148,19 @@ lors de l'enregistrement. Le résultat indique si les sorties sont intactes,
|
||||||
altérées ou impossibles à vérifier. Ce contrôle est strictement en lecture
|
altérées ou impossibles à vérifier. Ce contrôle est strictement en lecture
|
||||||
seule : aucune sortie ni empreinte persistée n'est corrigée automatiquement.
|
seule : aucune sortie ni empreinte persistée n'est corrigée automatiquement.
|
||||||
|
|
||||||
|
### Comptes sociaux structurés — schéma V4
|
||||||
|
|
||||||
|
La table `comptes_sociaux` complète une ligne de `entites` sans dupliquer le
|
||||||
|
nœud affiché dans le graphe. Elle conserve la plateforme, l'URL de profil, le
|
||||||
|
pseudonyme affiché, l'identifiant stable facultatif, la première observation,
|
||||||
|
l'état observé et les notes factuelles. La contrainte unique sur
|
||||||
|
`(plateforme, url_profil)` évite les doublons.
|
||||||
|
|
||||||
|
Une capture, une vidéo ou un courriel déjà importé peut être rattaché au
|
||||||
|
compte via `preuve_entites`. La création des deux lignes et de cette liaison
|
||||||
|
est transactionnelle : aucun nœud incomplet n'est conservé si une étape
|
||||||
|
échoue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 6. Tables de liaison
|
# 6. Tables de liaison
|
||||||
|
|
|
||||||
50
include/core/social_account_service.h
Normal file
50
include/core/social_account_service.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file social_account_service.h
|
||||||
|
* @brief Création transactionnelle d'entités de comptes sociaux.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#ifndef LABFY_INVESTIGATION_SOCIAL_ACCOUNT_SERVICE_H
|
||||||
|
#define LABFY_INVESTIGATION_SOCIAL_ACCOUNT_SERVICE_H
|
||||||
|
|
||||||
|
#include "database/database.h"
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
/** @brief Données nécessaires à l'enregistrement d'un compte social. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *platform;
|
||||||
|
const char *profile_url;
|
||||||
|
const char *username;
|
||||||
|
const char *platform_identifier;
|
||||||
|
const char *first_observed_at;
|
||||||
|
const char *account_state;
|
||||||
|
const char *notes;
|
||||||
|
const char *evidence_identifier;
|
||||||
|
} SocialAccountInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Valide et enregistre un compte social et sa preuve facultative.
|
||||||
|
*
|
||||||
|
* L'entité, sa fiche spécialisée et son éventuelle association à une preuve
|
||||||
|
* sont créées dans une même transaction. L'identifiant retourné appartient à
|
||||||
|
* l'appelant et doit être libéré avec g_free().
|
||||||
|
*
|
||||||
|
* @param database Connexion ouverte empruntée.
|
||||||
|
* @param input Données du compte à créer.
|
||||||
|
* @param out_entity_identifier Destination facultative de l'UUID créé.
|
||||||
|
* @param error Emplacement facultatif recevant une erreur.
|
||||||
|
* @return TRUE lorsque toutes les écritures sont validées.
|
||||||
|
*/
|
||||||
|
gboolean social_account_service_create(
|
||||||
|
Database *database,
|
||||||
|
const SocialAccountInput *input,
|
||||||
|
char **out_entity_identifier,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -68,6 +68,19 @@ bool schema_install_v3(
|
||||||
Database *database
|
Database *database
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Installe la migration des comptes sociaux du schéma V4.
|
||||||
|
*
|
||||||
|
* La connexion doit être valide et une transaction doit déjà être active.
|
||||||
|
* La fonction ne réalise ni COMMIT ni ROLLBACK.
|
||||||
|
*
|
||||||
|
* @param database Connexion Database ouverte.
|
||||||
|
* @return true si la migration V4 a été appliquée, sinon false.
|
||||||
|
*/
|
||||||
|
bool schema_install_v4(
|
||||||
|
Database *database
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit la présence des extensions du schéma courant V2.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
67
include/views/create_social_account_dialog.h
Normal file
67
include/views/create_social_account_dialog.h
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file create_social_account_dialog.h
|
||||||
|
* @brief Dialogue GTK de création d'un compte social observé.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#ifndef LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||||
|
#define LABFY_INVESTIGATION_CREATE_SOCIAL_ACCOUNT_DIALOG_H
|
||||||
|
|
||||||
|
#include "models/evidence_record.h"
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
/** @brief Résultat opaque du dialogue de compte social. */
|
||||||
|
typedef struct CreateSocialAccountDialogResult CreateSocialAccountDialogResult;
|
||||||
|
|
||||||
|
/** @brief Callback appelé après validation ou annulation. */
|
||||||
|
typedef void (*CreateSocialAccountDialogCallback)(
|
||||||
|
CreateSocialAccountDialogResult *result,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Présente le formulaire de création d'un compte social.
|
||||||
|
*
|
||||||
|
* Le tableau de preuves est uniquement emprunté pendant cet appel ; les
|
||||||
|
* informations nécessaires au formulaire sont copiées.
|
||||||
|
*
|
||||||
|
* @param parent Fenêtre parente.
|
||||||
|
* @param evidence_records Tableau facultatif de EvidenceRecord.
|
||||||
|
* @param callback Callback de fin.
|
||||||
|
* @param user_data Données privées du callback.
|
||||||
|
* @return TRUE si le dialogue a été présenté.
|
||||||
|
*/
|
||||||
|
gboolean create_social_account_dialog_present(
|
||||||
|
GtkWindow *parent,
|
||||||
|
const GPtrArray *evidence_records,
|
||||||
|
CreateSocialAccountDialogCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @brief Libère un résultat, y compris toutes ses chaînes. */
|
||||||
|
void create_social_account_dialog_result_free(
|
||||||
|
CreateSocialAccountDialogResult *result
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @brief Retourne le code de la plateforme sélectionnée. */
|
||||||
|
const char *create_social_account_dialog_result_get_platform(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne l'URL complète du profil. */
|
||||||
|
const char *create_social_account_dialog_result_get_profile_url(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne le pseudonyme affiché. */
|
||||||
|
const char *create_social_account_dialog_result_get_username(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne l'identifiant stable facultatif de la plateforme. */
|
||||||
|
const char *create_social_account_dialog_result_get_platform_identifier(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne la date UTC de première observation. */
|
||||||
|
const char *create_social_account_dialog_result_get_first_observed_at(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne l'état observé du compte. */
|
||||||
|
const char *create_social_account_dialog_result_get_account_state(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne les notes factuelles facultatives. */
|
||||||
|
const char *create_social_account_dialog_result_get_notes(const CreateSocialAccountDialogResult *result);
|
||||||
|
/** @brief Retourne l'UUID de la preuve associée, ou NULL. */
|
||||||
|
const char *create_social_account_dialog_result_get_evidence_identifier(const CreateSocialAccountDialogResult *result);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -50,6 +50,11 @@ typedef void (*MainWindowImportEvidenceCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Callback appelé pour ajouter un compte de réseau social. */
|
||||||
|
typedef void (*MainWindowAddSocialAccountCallback)(
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback appelé lorsque l'utilisateur revient au graphe.
|
* @brief Callback appelé lorsque l'utilisateur revient au graphe.
|
||||||
*
|
*
|
||||||
|
|
@ -531,6 +536,18 @@ void main_window_set_import_evidence_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Définit le callback de l'action d'ajout d'un compte social.
|
||||||
|
* @param main_window Fenêtre principale.
|
||||||
|
* @param callback Callback facultatif.
|
||||||
|
* @param user_data Données privées transmises au callback.
|
||||||
|
*/
|
||||||
|
void main_window_set_add_social_account_callback(
|
||||||
|
MainWindow *main_window,
|
||||||
|
MainWindowAddSocialAccountCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Définit le callback du bouton « Revenir au graphe ».
|
* @brief Définit le callback du bouton « Revenir au graphe ».
|
||||||
*
|
*
|
||||||
|
|
@ -557,6 +574,16 @@ void main_window_set_import_evidence_enabled(
|
||||||
gboolean enabled
|
gboolean enabled
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Active ou désactive l'ajout d'un compte social.
|
||||||
|
* @param main_window Fenêtre principale.
|
||||||
|
* @param enabled TRUE si une enquête est ouverte.
|
||||||
|
*/
|
||||||
|
void main_window_set_add_social_account_enabled(
|
||||||
|
MainWindow *main_window,
|
||||||
|
gboolean enabled
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Libère les ressources de la fenêtre.
|
* @brief Libère les ressources de la fenêtre.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,9 @@
|
||||||
#include "core/evidence_integrity_task.h"
|
#include "core/evidence_integrity_task.h"
|
||||||
#include "core/evidence_integrity_verifier.h"
|
#include "core/evidence_integrity_verifier.h"
|
||||||
#include "core/relation_service.h"
|
#include "core/relation_service.h"
|
||||||
|
#include "core/social_account_service.h"
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
|
#include "views/create_social_account_dialog.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
@ -2267,6 +2269,11 @@ static gboolean application_install_session(
|
||||||
TRUE
|
TRUE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
main_window_set_add_social_account_enabled(
|
||||||
|
application->main_window,
|
||||||
|
TRUE
|
||||||
|
);
|
||||||
|
|
||||||
application_start_graph_loading(
|
application_start_graph_loading(
|
||||||
application,
|
application,
|
||||||
database_path
|
database_path
|
||||||
|
|
@ -4091,6 +4098,80 @@ static void application_on_import_evidence_requested(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Persiste le résultat validé du dialogue de compte social. */
|
||||||
|
static void application_on_social_account_completed(
|
||||||
|
CreateSocialAccountDialogResult *result, gpointer user_data)
|
||||||
|
{
|
||||||
|
Application *application = user_data;
|
||||||
|
SocialAccountInput input = {0};
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
if (result == NULL || application == NULL || application->session == NULL)
|
||||||
|
{
|
||||||
|
create_social_account_dialog_result_free(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.platform = create_social_account_dialog_result_get_platform(result);
|
||||||
|
input.profile_url = create_social_account_dialog_result_get_profile_url(result);
|
||||||
|
input.username = create_social_account_dialog_result_get_username(result);
|
||||||
|
input.platform_identifier = create_social_account_dialog_result_get_platform_identifier(result);
|
||||||
|
input.first_observed_at = create_social_account_dialog_result_get_first_observed_at(result);
|
||||||
|
input.account_state = create_social_account_dialog_result_get_account_state(result);
|
||||||
|
input.notes = create_social_account_dialog_result_get_notes(result);
|
||||||
|
input.evidence_identifier = create_social_account_dialog_result_get_evidence_identifier(result);
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
if (!social_account_service_create(database, &input, &identifier, &error))
|
||||||
|
{
|
||||||
|
application_present_error(application, "Compte social non créé",
|
||||||
|
error != NULL ? error->message : "L'écriture dans l'enquête a échoué.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
project = investigation_session_get_project(application->session);
|
||||||
|
application_message_dialog_present(
|
||||||
|
main_window_get_window(application->main_window),
|
||||||
|
APPLICATION_MESSAGE_DIALOG_INFORMATION,
|
||||||
|
"Compte social ajouté",
|
||||||
|
"Le profil et son éventuelle preuve associée ont été ajoutés au graphe.");
|
||||||
|
application_start_graph_loading(application,
|
||||||
|
investigation_project_get_database_path(project));
|
||||||
|
}
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_free(identifier);
|
||||||
|
create_social_account_dialog_result_free(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Charge les preuves puis ouvre le formulaire de compte social. */
|
||||||
|
static void application_on_add_social_account_requested(gpointer user_data)
|
||||||
|
{
|
||||||
|
Application *application = user_data;
|
||||||
|
Database *database = NULL;
|
||||||
|
EvidenceDao *dao = NULL;
|
||||||
|
GPtrArray *evidence_records = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
if (application == NULL || application->main_window == NULL ||
|
||||||
|
application->session == NULL) return;
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
dao = evidence_dao_new(database, &error);
|
||||||
|
if (dao != NULL) evidence_records = evidence_dao_list_all(dao, &error);
|
||||||
|
if (evidence_records == NULL)
|
||||||
|
{
|
||||||
|
application_present_error(application, "Formulaire indisponible",
|
||||||
|
error != NULL ? error->message : "Impossible de charger les preuves.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
create_social_account_dialog_present(
|
||||||
|
main_window_get_window(application->main_window), evidence_records,
|
||||||
|
application_on_social_account_completed, application);
|
||||||
|
}
|
||||||
|
g_clear_pointer(&evidence_records, g_ptr_array_unref);
|
||||||
|
evidence_dao_free(dao);
|
||||||
|
g_clear_error(&error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Traite la sélection d'un nœud dans l'arborescence.
|
* @brief Traite la sélection d'un nœud dans l'arborescence.
|
||||||
*
|
*
|
||||||
|
|
@ -6147,11 +6228,22 @@ static void application_on_activate(
|
||||||
application
|
application
|
||||||
);
|
);
|
||||||
|
|
||||||
|
main_window_set_add_social_account_callback(
|
||||||
|
application->main_window,
|
||||||
|
application_on_add_social_account_requested,
|
||||||
|
application
|
||||||
|
);
|
||||||
|
|
||||||
main_window_set_import_evidence_enabled(
|
main_window_set_import_evidence_enabled(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
application->session != NULL
|
application->session != NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
main_window_set_add_social_account_enabled(
|
||||||
|
application->main_window,
|
||||||
|
application->session != NULL
|
||||||
|
);
|
||||||
|
|
||||||
main_window_set_quit_callback(
|
main_window_set_quit_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
application_on_quit_requested,
|
application_on_quit_requested,
|
||||||
|
|
|
||||||
153
src/core/social_account_service.c
Normal file
153
src/core/social_account_service.c
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file social_account_service.c
|
||||||
|
* @brief Création transactionnelle d'entités de comptes sociaux.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#include "core/social_account_service.h"
|
||||||
|
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/evidence_entity_dao.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/** @brief Domaine d'erreur privé du service. */
|
||||||
|
#define SOCIAL_ACCOUNT_SERVICE_ERROR social_account_service_error_quark()
|
||||||
|
|
||||||
|
/** @brief Retourne le domaine d'erreur du service. */
|
||||||
|
static GQuark social_account_service_error_quark(void)
|
||||||
|
{
|
||||||
|
return g_quark_from_static_string("social-account-service-error");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Associe une plateforme au type d'entité correspondant. */
|
||||||
|
static const char *social_account_service_entity_type(const char *platform)
|
||||||
|
{
|
||||||
|
if (g_strcmp0(platform, "tiktok") == 0) return "tiktok_account";
|
||||||
|
if (g_strcmp0(platform, "instagram") == 0) return "instagram_account";
|
||||||
|
if (g_strcmp0(platform, "facebook") == 0) return "facebook_account";
|
||||||
|
if (g_strcmp0(platform, "x") == 0) return "x_account";
|
||||||
|
if (g_strcmp0(platform, "telegram") == 0) return "telegram_account";
|
||||||
|
if (g_strcmp0(platform, "other") == 0) return "social_account";
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Indique si l'état du compte est pris en charge. */
|
||||||
|
static gboolean social_account_service_state_is_valid(const char *state)
|
||||||
|
{
|
||||||
|
return g_strcmp0(state, "active") == 0 ||
|
||||||
|
g_strcmp0(state, "private") == 0 ||
|
||||||
|
g_strcmp0(state, "suspended") == 0 ||
|
||||||
|
g_strcmp0(state, "deleted") == 0 ||
|
||||||
|
g_strcmp0(state, "unknown") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Lie un texte ou SQL NULL à une requête. */
|
||||||
|
static gboolean social_account_service_bind_optional(
|
||||||
|
DatabaseStatement *statement, int index, const char *value)
|
||||||
|
{
|
||||||
|
return value != NULL && value[0] != '\0'
|
||||||
|
? database_statement_bind_text(statement, index, value)
|
||||||
|
: database_statement_bind_null(statement, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean social_account_service_create(
|
||||||
|
Database *database,
|
||||||
|
const SocialAccountInput *input,
|
||||||
|
char **out_entity_identifier,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
static const char *const insert_sql =
|
||||||
|
"INSERT INTO comptes_sociaux"
|
||||||
|
"(entite_id, plateforme, url_profil, pseudonyme, "
|
||||||
|
"identifiant_plateforme, premiere_observation, etat_compte, notes) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
EntityRecord *record = NULL;
|
||||||
|
GDateTime *parsed_date = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
char *now_text = NULL;
|
||||||
|
const char *type = NULL;
|
||||||
|
gboolean transaction_active = FALSE;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (out_entity_identifier != NULL) *out_entity_identifier = NULL;
|
||||||
|
type = input != NULL
|
||||||
|
? social_account_service_entity_type(input->platform) : NULL;
|
||||||
|
parsed_date = input != NULL && input->first_observed_at != NULL
|
||||||
|
? g_date_time_new_from_iso8601(input->first_observed_at, NULL) : NULL;
|
||||||
|
if (database == NULL || input == NULL || type == NULL ||
|
||||||
|
input->profile_url == NULL ||
|
||||||
|
(!g_str_has_prefix(input->profile_url, "https://") &&
|
||||||
|
!g_str_has_prefix(input->profile_url, "http://")) ||
|
||||||
|
input->username == NULL || input->username[0] == '\0' ||
|
||||||
|
parsed_date == NULL ||
|
||||||
|
!social_account_service_state_is_valid(input->account_state))
|
||||||
|
{
|
||||||
|
g_set_error_literal(error, SOCIAL_ACCOUNT_SERVICE_ERROR, 1,
|
||||||
|
"Les informations du compte social sont invalides.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
identifier = g_uuid_string_random();
|
||||||
|
{
|
||||||
|
GDateTime *now = g_date_time_new_now_utc();
|
||||||
|
now_text = now != NULL ? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||||
|
g_clear_pointer(&now, g_date_time_unref);
|
||||||
|
}
|
||||||
|
if (identifier == NULL || now_text == NULL ||
|
||||||
|
!database_transaction_begin(database)) goto cleanup;
|
||||||
|
transaction_active = TRUE;
|
||||||
|
entity_dao = entity_dao_new(database, error);
|
||||||
|
record = entity_record_new(identifier, type, input->profile_url,
|
||||||
|
input->username, input->notes, 50, now_text, now_text,
|
||||||
|
ENTITY_STATUS_ACTIVE, error);
|
||||||
|
if (entity_dao == NULL || record == NULL ||
|
||||||
|
!entity_dao_insert(entity_dao, record, error)) goto cleanup;
|
||||||
|
statement = database_statement_prepare(database, insert_sql);
|
||||||
|
if (statement == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 1, identifier) ||
|
||||||
|
!database_statement_bind_text(statement, 2, input->platform) ||
|
||||||
|
!database_statement_bind_text(statement, 3, input->profile_url) ||
|
||||||
|
!database_statement_bind_text(statement, 4, input->username) ||
|
||||||
|
!social_account_service_bind_optional(statement, 5, input->platform_identifier) ||
|
||||||
|
!database_statement_bind_text(statement, 6, input->first_observed_at) ||
|
||||||
|
!database_statement_bind_text(statement, 7, input->account_state) ||
|
||||||
|
!social_account_service_bind_optional(statement, 8, input->notes) ||
|
||||||
|
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
{
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
g_set_error_literal(error, SOCIAL_ACCOUNT_SERVICE_ERROR, 2,
|
||||||
|
"Impossible d'enregistrer la fiche du compte social.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (input->evidence_identifier != NULL &&
|
||||||
|
input->evidence_identifier[0] != '\0')
|
||||||
|
{
|
||||||
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
|
if (link_dao == NULL || !evidence_entity_dao_link(
|
||||||
|
link_dao, input->evidence_identifier, identifier, error))
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!database_transaction_commit(database)) goto cleanup;
|
||||||
|
transaction_active = FALSE;
|
||||||
|
success = TRUE;
|
||||||
|
if (out_entity_identifier != NULL)
|
||||||
|
*out_entity_identifier = g_strdup(identifier);
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (!success && transaction_active)
|
||||||
|
database_transaction_rollback(database);
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
entity_record_free(record);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
g_clear_pointer(&parsed_date, g_date_time_unref);
|
||||||
|
g_free(now_text);
|
||||||
|
g_free(identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 3
|
#define DATABASE_SCHEMA_VERSION_CURRENT 4
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "3"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "4"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -692,6 +692,26 @@ rollback:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Applique atomiquement la migration du schéma V3 vers V4. */
|
||||||
|
static bool database_migrate_v3_to_v4(Database *database)
|
||||||
|
{
|
||||||
|
bool transaction_started = false;
|
||||||
|
|
||||||
|
if (database == NULL || !database_transaction_begin(database))
|
||||||
|
return false;
|
||||||
|
transaction_started = true;
|
||||||
|
if (!schema_install_v4(database) ||
|
||||||
|
!database_update_schema_version(database, "4") ||
|
||||||
|
!database_transaction_commit(database))
|
||||||
|
goto rollback;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
if (transaction_started && !database_transaction_rollback(database))
|
||||||
|
g_warning("Impossible d’annuler la migration SQLite V3 vers V4.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -884,6 +904,12 @@ bool database_migrate_to_latest(
|
||||||
schema_version = 3;
|
schema_version = 3;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
if (!database_migrate_v3_to_v4(database))
|
||||||
|
return false;
|
||||||
|
schema_version = 4;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
database,
|
database,
|
||||||
|
|
@ -1000,6 +1026,11 @@ bool database_initialize(
|
||||||
goto rollback;
|
goto rollback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!schema_install_v4(database))
|
||||||
|
{
|
||||||
|
goto rollback;
|
||||||
|
}
|
||||||
|
|
||||||
if (!schema_ensure_current(
|
if (!schema_ensure_current(
|
||||||
database
|
database
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,17 @@ bool schema_install_v3(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool schema_install_v4(
|
||||||
|
Database *database
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return schema_execute_file(
|
||||||
|
database,
|
||||||
|
"database/schema_v4.sql",
|
||||||
|
"la migration SQLite V4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bool schema_ensure_current(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
277
src/views/create_social_account_dialog.c
Normal file
277
src/views/create_social_account_dialog.c
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file create_social_account_dialog.c
|
||||||
|
* @brief Dialogue GTK de création d'un compte social observé.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#include "views/create_social_account_dialog.h"
|
||||||
|
|
||||||
|
struct CreateSocialAccountDialogResult
|
||||||
|
{
|
||||||
|
char *platform;
|
||||||
|
char *profile_url;
|
||||||
|
char *username;
|
||||||
|
char *platform_identifier;
|
||||||
|
char *first_observed_at;
|
||||||
|
char *account_state;
|
||||||
|
char *notes;
|
||||||
|
char *evidence_identifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
GtkWindow *window;
|
||||||
|
GtkDropDown *platform;
|
||||||
|
GtkEntry *url;
|
||||||
|
GtkEntry *username;
|
||||||
|
GtkEntry *platform_identifier;
|
||||||
|
GtkEntry *observed_at;
|
||||||
|
GtkDropDown *state;
|
||||||
|
GtkTextView *notes;
|
||||||
|
GtkDropDown *evidence;
|
||||||
|
GtkLabel *error;
|
||||||
|
GPtrArray *evidence_identifiers;
|
||||||
|
CreateSocialAccountDialogCallback callback;
|
||||||
|
gpointer user_data;
|
||||||
|
gboolean completed;
|
||||||
|
} CreateSocialAccountDialogState;
|
||||||
|
|
||||||
|
static const char *const platform_codes[] = {
|
||||||
|
"tiktok", "instagram", "facebook", "x", "telegram", "other"
|
||||||
|
};
|
||||||
|
static const char *const state_codes[] = {
|
||||||
|
"unknown", "active", "private", "suspended", "deleted"
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @brief Retourne une copie nettoyée, ou NULL pour un texte vide. */
|
||||||
|
static char *create_social_account_dialog_copy(const char *text)
|
||||||
|
{
|
||||||
|
char *copy = text != NULL ? g_strdup(text) : NULL;
|
||||||
|
if (copy == NULL) return NULL;
|
||||||
|
g_strstrip(copy);
|
||||||
|
if (copy[0] == '\0') { g_free(copy); return NULL; }
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Extrait le texte du champ de notes. */
|
||||||
|
static char *create_social_account_dialog_notes(CreateSocialAccountDialogState *state)
|
||||||
|
{
|
||||||
|
GtkTextBuffer *buffer = gtk_text_view_get_buffer(state->notes);
|
||||||
|
GtkTextIter start;
|
||||||
|
GtkTextIter end;
|
||||||
|
gtk_text_buffer_get_bounds(buffer, &start, &end);
|
||||||
|
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Libère l'état privé attaché à la fenêtre. */
|
||||||
|
static void create_social_account_dialog_state_free(gpointer user_data)
|
||||||
|
{
|
||||||
|
CreateSocialAccountDialogState *state = user_data;
|
||||||
|
if (state == NULL) return;
|
||||||
|
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
|
||||||
|
g_free(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Signale une annulation au callback une seule fois. */
|
||||||
|
static void create_social_account_dialog_cancel(CreateSocialAccountDialogState *state)
|
||||||
|
{
|
||||||
|
if (state == NULL || state->completed) return;
|
||||||
|
state->completed = TRUE;
|
||||||
|
if (state->callback != NULL) state->callback(NULL, state->user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Traite la fermeture native du dialogue. */
|
||||||
|
static gboolean create_social_account_dialog_on_close(GtkWindow *window, gpointer user_data)
|
||||||
|
{
|
||||||
|
(void) window;
|
||||||
|
create_social_account_dialog_cancel(user_data);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Traite le bouton d'annulation. */
|
||||||
|
static void create_social_account_dialog_on_cancel(GtkButton *button, gpointer user_data)
|
||||||
|
{
|
||||||
|
CreateSocialAccountDialogState *state = user_data;
|
||||||
|
(void) button;
|
||||||
|
create_social_account_dialog_cancel(state);
|
||||||
|
gtk_window_close(state->window);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Valide le formulaire et transmet un résultat possédé. */
|
||||||
|
static void create_social_account_dialog_on_create(GtkButton *button, gpointer user_data)
|
||||||
|
{
|
||||||
|
CreateSocialAccountDialogState *state = user_data;
|
||||||
|
CreateSocialAccountDialogResult *result = NULL;
|
||||||
|
GDateTime *date = NULL;
|
||||||
|
char *notes = NULL;
|
||||||
|
guint platform = gtk_drop_down_get_selected(state->platform);
|
||||||
|
guint account_state = gtk_drop_down_get_selected(state->state);
|
||||||
|
guint evidence = gtk_drop_down_get_selected(state->evidence);
|
||||||
|
const char *url = gtk_editable_get_text(GTK_EDITABLE(state->url));
|
||||||
|
const char *username = gtk_editable_get_text(GTK_EDITABLE(state->username));
|
||||||
|
const char *observed_at = gtk_editable_get_text(GTK_EDITABLE(state->observed_at));
|
||||||
|
(void) button;
|
||||||
|
date = g_date_time_new_from_iso8601(observed_at, NULL);
|
||||||
|
if (platform >= G_N_ELEMENTS(platform_codes) ||
|
||||||
|
account_state >= G_N_ELEMENTS(state_codes) ||
|
||||||
|
url == NULL ||
|
||||||
|
(!g_str_has_prefix(url, "https://") && !g_str_has_prefix(url, "http://")) ||
|
||||||
|
username == NULL || username[0] == '\0' || date == NULL)
|
||||||
|
{
|
||||||
|
gtk_label_set_text(state->error,
|
||||||
|
"Renseignez une URL http(s), un pseudonyme et une date ISO 8601 valide.");
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
|
||||||
|
g_clear_pointer(&date, g_date_time_unref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notes = create_social_account_dialog_notes(state);
|
||||||
|
result = g_new0(CreateSocialAccountDialogResult, 1);
|
||||||
|
result->platform = g_strdup(platform_codes[platform]);
|
||||||
|
result->profile_url = create_social_account_dialog_copy(url);
|
||||||
|
result->username = create_social_account_dialog_copy(username);
|
||||||
|
result->platform_identifier = create_social_account_dialog_copy(
|
||||||
|
gtk_editable_get_text(GTK_EDITABLE(state->platform_identifier)));
|
||||||
|
result->first_observed_at = create_social_account_dialog_copy(observed_at);
|
||||||
|
result->account_state = g_strdup(state_codes[account_state]);
|
||||||
|
result->notes = create_social_account_dialog_copy(notes);
|
||||||
|
if (evidence > 0 && evidence - 1 < state->evidence_identifiers->len)
|
||||||
|
result->evidence_identifier = g_strdup(g_ptr_array_index(
|
||||||
|
state->evidence_identifiers, evidence - 1));
|
||||||
|
state->completed = TRUE;
|
||||||
|
if (state->callback != NULL) state->callback(result, state->user_data);
|
||||||
|
else create_social_account_dialog_result_free(result);
|
||||||
|
g_free(notes);
|
||||||
|
g_date_time_unref(date);
|
||||||
|
gtk_window_close(state->window);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Ajoute une ligne libellée au formulaire. */
|
||||||
|
static void create_social_account_dialog_add_row(
|
||||||
|
GtkGrid *grid, int row, const char *label, GtkWidget *field)
|
||||||
|
{
|
||||||
|
GtkWidget *caption = gtk_label_new(label);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(caption), 0.0f);
|
||||||
|
gtk_widget_set_halign(caption, GTK_ALIGN_START);
|
||||||
|
gtk_widget_set_hexpand(field, TRUE);
|
||||||
|
gtk_grid_attach(grid, caption, 0, row, 1, 1);
|
||||||
|
gtk_grid_attach(grid, field, 1, row, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean create_social_account_dialog_present(
|
||||||
|
GtkWindow *parent, const GPtrArray *evidence_records,
|
||||||
|
CreateSocialAccountDialogCallback callback, gpointer user_data)
|
||||||
|
{
|
||||||
|
static const char *const platform_labels[] = {
|
||||||
|
"TikTok", "Instagram", "Facebook", "X", "Telegram", "Autre", NULL
|
||||||
|
};
|
||||||
|
static const char *const state_labels[] = {
|
||||||
|
"Inconnu", "Actif", "Privé", "Suspendu", "Supprimé", NULL
|
||||||
|
};
|
||||||
|
CreateSocialAccountDialogState *state = NULL;
|
||||||
|
GtkWidget *box = NULL;
|
||||||
|
GtkWidget *grid = NULL;
|
||||||
|
GtkWidget *actions = NULL;
|
||||||
|
GtkWidget *cancel = NULL;
|
||||||
|
GtkWidget *create = NULL;
|
||||||
|
GtkStringList *evidence_labels = NULL;
|
||||||
|
GDateTime *now = NULL;
|
||||||
|
char *now_text = NULL;
|
||||||
|
guint index = 0;
|
||||||
|
if (parent == NULL) return FALSE;
|
||||||
|
state = g_new0(CreateSocialAccountDialogState, 1);
|
||||||
|
state->callback = callback;
|
||||||
|
state->user_data = user_data;
|
||||||
|
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
|
||||||
|
state->window = GTK_WINDOW(gtk_window_new());
|
||||||
|
gtk_window_set_title(state->window, "Ajouter un compte social");
|
||||||
|
gtk_window_set_transient_for(state->window, parent);
|
||||||
|
gtk_window_set_modal(state->window, TRUE);
|
||||||
|
gtk_window_set_default_size(state->window, 620, 560);
|
||||||
|
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||||
|
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
|
||||||
|
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
|
||||||
|
grid = gtk_grid_new();
|
||||||
|
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||||
|
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||||
|
state->platform = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(platform_labels));
|
||||||
|
state->url = GTK_ENTRY(gtk_entry_new());
|
||||||
|
gtk_entry_set_placeholder_text(state->url, "https://www.tiktok.com/@compte");
|
||||||
|
state->username = GTK_ENTRY(gtk_entry_new());
|
||||||
|
gtk_entry_set_placeholder_text(state->username, "@pseudonyme affiché");
|
||||||
|
state->platform_identifier = GTK_ENTRY(gtk_entry_new());
|
||||||
|
gtk_entry_set_placeholder_text(state->platform_identifier, "Identifiant stable, si visible");
|
||||||
|
state->observed_at = GTK_ENTRY(gtk_entry_new());
|
||||||
|
now = g_date_time_new_now_utc();
|
||||||
|
now_text = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(state->observed_at), now_text);
|
||||||
|
state->state = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(state_labels));
|
||||||
|
evidence_labels = gtk_string_list_new(NULL);
|
||||||
|
gtk_string_list_append(evidence_labels, "Aucune preuve associée");
|
||||||
|
for (index = 0; evidence_records != NULL && index < evidence_records->len; index++)
|
||||||
|
{
|
||||||
|
EvidenceRecord *record = g_ptr_array_index((GPtrArray *) evidence_records, index);
|
||||||
|
const char *identifier = evidence_record_get_identifier(record);
|
||||||
|
const char *name = evidence_record_get_original_name(record);
|
||||||
|
if (identifier == NULL || name == NULL) continue;
|
||||||
|
gtk_string_list_append(evidence_labels, name);
|
||||||
|
g_ptr_array_add(state->evidence_identifiers, g_strdup(identifier));
|
||||||
|
}
|
||||||
|
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
|
||||||
|
G_LIST_MODEL(evidence_labels), NULL));
|
||||||
|
g_object_unref(evidence_labels);
|
||||||
|
state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
|
||||||
|
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
|
||||||
|
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 100);
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 0, "Plateforme", GTK_WIDGET(state->platform));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 1, "URL complète", GTK_WIDGET(state->url));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->username));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 3, "ID plateforme", GTK_WIDGET(state->platform_identifier));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 4, "Première observation (UTC)", GTK_WIDGET(state->observed_at));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 5, "État observé", GTK_WIDGET(state->state));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 6, "Preuve associée", GTK_WIDGET(state->evidence));
|
||||||
|
create_social_account_dialog_add_row(GTK_GRID(grid), 7, "Notes factuelles", GTK_WIDGET(state->notes));
|
||||||
|
state->error = GTK_LABEL(gtk_label_new(NULL));
|
||||||
|
gtk_label_set_wrap(state->error, TRUE);
|
||||||
|
gtk_widget_add_css_class(GTK_WIDGET(state->error), "error");
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
|
||||||
|
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||||
|
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||||
|
cancel = gtk_button_new_with_label("Annuler");
|
||||||
|
create = gtk_button_new_with_label("Ajouter au graphe");
|
||||||
|
gtk_widget_add_css_class(create, "suggested-action");
|
||||||
|
gtk_box_append(GTK_BOX(actions), cancel); gtk_box_append(GTK_BOX(actions), create);
|
||||||
|
gtk_box_append(GTK_BOX(box), grid);
|
||||||
|
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
|
||||||
|
gtk_box_append(GTK_BOX(box), actions);
|
||||||
|
gtk_window_set_child(state->window, box);
|
||||||
|
g_signal_connect(state->window, "close-request", G_CALLBACK(create_social_account_dialog_on_close), state);
|
||||||
|
g_signal_connect(cancel, "clicked", G_CALLBACK(create_social_account_dialog_on_cancel), state);
|
||||||
|
g_signal_connect(create, "clicked", G_CALLBACK(create_social_account_dialog_on_create), state);
|
||||||
|
g_object_set_data_full(G_OBJECT(state->window), "social-account-state", state,
|
||||||
|
create_social_account_dialog_state_free);
|
||||||
|
gtk_window_present(state->window);
|
||||||
|
g_free(now_text); g_date_time_unref(now);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void create_social_account_dialog_result_free(CreateSocialAccountDialogResult *result)
|
||||||
|
{
|
||||||
|
if (result == NULL) return;
|
||||||
|
g_free(result->platform); g_free(result->profile_url); g_free(result->username);
|
||||||
|
g_free(result->platform_identifier); g_free(result->first_observed_at);
|
||||||
|
g_free(result->account_state); g_free(result->notes);
|
||||||
|
g_free(result->evidence_identifier); g_free(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define SOCIAL_RESULT_GETTER(name, field) \
|
||||||
|
const char *create_social_account_dialog_result_get_##name( \
|
||||||
|
const CreateSocialAccountDialogResult *result) \
|
||||||
|
{ return result != NULL ? result->field : NULL; }
|
||||||
|
|
||||||
|
SOCIAL_RESULT_GETTER(platform, platform)
|
||||||
|
SOCIAL_RESULT_GETTER(profile_url, profile_url)
|
||||||
|
SOCIAL_RESULT_GETTER(username, username)
|
||||||
|
SOCIAL_RESULT_GETTER(platform_identifier, platform_identifier)
|
||||||
|
SOCIAL_RESULT_GETTER(first_observed_at, first_observed_at)
|
||||||
|
SOCIAL_RESULT_GETTER(account_state, account_state)
|
||||||
|
SOCIAL_RESULT_GETTER(notes, notes)
|
||||||
|
SOCIAL_RESULT_GETTER(evidence_identifier, evidence_identifier)
|
||||||
|
|
@ -59,6 +59,7 @@ struct MainWindow
|
||||||
GtkWidget *new_investigation_button;
|
GtkWidget *new_investigation_button;
|
||||||
GtkWidget *open_investigation_button;
|
GtkWidget *open_investigation_button;
|
||||||
GtkWidget *import_evidence_button;
|
GtkWidget *import_evidence_button;
|
||||||
|
GtkWidget *add_social_account_button;
|
||||||
GtkWidget *show_graph_button;
|
GtkWidget *show_graph_button;
|
||||||
GtkWidget *content_paned;
|
GtkWidget *content_paned;
|
||||||
GtkWidget *main_paned;
|
GtkWidget *main_paned;
|
||||||
|
|
@ -87,6 +88,9 @@ struct MainWindow
|
||||||
gpointer
|
gpointer
|
||||||
import_evidence_user_data;
|
import_evidence_user_data;
|
||||||
|
|
||||||
|
MainWindowAddSocialAccountCallback add_social_account_callback;
|
||||||
|
gpointer add_social_account_user_data;
|
||||||
|
|
||||||
MainWindowShowGraphCallback
|
MainWindowShowGraphCallback
|
||||||
show_graph_callback;
|
show_graph_callback;
|
||||||
|
|
||||||
|
|
@ -294,6 +298,18 @@ static void main_window_on_import_evidence_clicked(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Relaie la demande d'ajout d'un compte social. */
|
||||||
|
static void main_window_on_add_social_account_clicked(
|
||||||
|
GtkButton *button, gpointer user_data)
|
||||||
|
{
|
||||||
|
MainWindow *main_window = user_data;
|
||||||
|
(void) button;
|
||||||
|
if (main_window == NULL || main_window->add_social_account_callback == NULL)
|
||||||
|
return;
|
||||||
|
main_window->add_social_account_callback(
|
||||||
|
main_window->add_social_account_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Transmet la demande de fermeture au contrôleur.
|
* @brief Transmet la demande de fermeture au contrôleur.
|
||||||
*
|
*
|
||||||
|
|
@ -594,6 +610,12 @@ MainWindow *main_window_new(
|
||||||
"Importer une preuve"
|
"Importer une preuve"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
main_window->add_social_account_button =
|
||||||
|
main_window_create_action_button(
|
||||||
|
"contact-new-symbolic",
|
||||||
|
"Ajouter un compte social"
|
||||||
|
);
|
||||||
|
|
||||||
main_window->show_graph_button =
|
main_window->show_graph_button =
|
||||||
main_window_create_action_button(
|
main_window_create_action_button(
|
||||||
"go-previous-symbolic",
|
"go-previous-symbolic",
|
||||||
|
|
@ -625,6 +647,8 @@ MainWindow *main_window_new(
|
||||||
FALSE
|
FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gtk_widget_set_sensitive(main_window->add_social_account_button, FALSE);
|
||||||
|
|
||||||
gtk_widget_set_sensitive(
|
gtk_widget_set_sensitive(
|
||||||
main_window->show_graph_button,
|
main_window->show_graph_button,
|
||||||
FALSE
|
FALSE
|
||||||
|
|
@ -645,6 +669,9 @@ MainWindow *main_window_new(
|
||||||
main_window->import_evidence_button
|
main_window->import_evidence_button
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gtk_box_append(GTK_BOX(main_window->action_bar),
|
||||||
|
main_window->add_social_account_button);
|
||||||
|
|
||||||
gtk_box_append(
|
gtk_box_append(
|
||||||
GTK_BOX(main_window->action_bar),
|
GTK_BOX(main_window->action_bar),
|
||||||
main_window->show_graph_button
|
main_window->show_graph_button
|
||||||
|
|
@ -687,6 +714,9 @@ MainWindow *main_window_new(
|
||||||
main_window
|
main_window
|
||||||
);
|
);
|
||||||
|
|
||||||
|
g_signal_connect(main_window->add_social_account_button, "clicked",
|
||||||
|
G_CALLBACK(main_window_on_add_social_account_clicked), main_window);
|
||||||
|
|
||||||
g_signal_connect(
|
g_signal_connect(
|
||||||
main_window->show_graph_button,
|
main_window->show_graph_button,
|
||||||
"clicked",
|
"clicked",
|
||||||
|
|
@ -1411,6 +1441,16 @@ void main_window_set_import_evidence_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_add_social_account_callback(
|
||||||
|
MainWindow *main_window,
|
||||||
|
MainWindowAddSocialAccountCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) return;
|
||||||
|
main_window->add_social_account_callback = callback;
|
||||||
|
main_window->add_social_account_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_show_graph_callback(
|
void main_window_set_show_graph_callback(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
MainWindowShowGraphCallback callback,
|
MainWindowShowGraphCallback callback,
|
||||||
|
|
@ -1446,6 +1486,14 @@ void main_window_set_import_evidence_enabled(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_add_social_account_enabled(
|
||||||
|
MainWindow *main_window, gboolean enabled)
|
||||||
|
{
|
||||||
|
if (main_window == NULL || main_window->add_social_account_button == NULL)
|
||||||
|
return;
|
||||||
|
gtk_widget_set_sensitive(main_window->add_social_account_button, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_verify_evidence_callback(
|
void main_window_set_verify_evidence_callback(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
MainWindowVerifyEvidenceCallback callback,
|
MainWindowVerifyEvidenceCallback callback,
|
||||||
|
|
|
||||||
|
|
@ -525,10 +525,11 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "3") == 0);
|
assert(strcmp(schema_version, "4") == 0);
|
||||||
test_database_assert_table_exists(database, "osint_executions");
|
test_database_assert_table_exists(database, "osint_executions");
|
||||||
test_database_assert_table_exists(database, "osint_execution_entities");
|
test_database_assert_table_exists(database, "osint_execution_entities");
|
||||||
test_database_assert_table_exists(database, "osint_execution_relations");
|
test_database_assert_table_exists(database, "osint_execution_relations");
|
||||||
|
test_database_assert_table_exists(database, "comptes_sociaux");
|
||||||
assert(strcmp(application_name, "Labfy Investigation") == 0);
|
assert(strcmp(application_name, "Labfy Investigation") == 0);
|
||||||
|
|
||||||
assert(created_at[0] != '\0');
|
assert(created_at[0] != '\0');
|
||||||
|
|
@ -987,7 +988,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"3"
|
"4"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,11 @@ static void test_entity_type_dao_list_all(void)
|
||||||
"domain_name",
|
"domain_name",
|
||||||
"ip_address",
|
"ip_address",
|
||||||
"organization",
|
"organization",
|
||||||
"other"
|
"other",
|
||||||
|
"tiktok_account",
|
||||||
|
"x_account",
|
||||||
|
"telegram_account",
|
||||||
|
"social_account"
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const expected_labels[] =
|
static const char *const expected_labels[] =
|
||||||
|
|
@ -263,7 +267,11 @@ static void test_entity_type_dao_list_all(void)
|
||||||
"Nom de domaine",
|
"Nom de domaine",
|
||||||
"Adresse IP",
|
"Adresse IP",
|
||||||
"Organisation",
|
"Organisation",
|
||||||
"Autre"
|
"Autre",
|
||||||
|
"Compte TikTok",
|
||||||
|
"Compte X",
|
||||||
|
"Compte Telegram",
|
||||||
|
"Autre compte social"
|
||||||
};
|
};
|
||||||
|
|
||||||
TestEntityTypeDaoFixture fixture =
|
TestEntityTypeDaoFixture fixture =
|
||||||
|
|
|
||||||
75
tests/test_social_account_service.c
Normal file
75
tests/test_social_account_service.c
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file test_social_account_service.c
|
||||||
|
* @brief Tests transactionnels de création des comptes sociaux.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#include "core/social_account_service.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "database/statement.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
|
||||||
|
/** @brief Lit un comptage entier dans la base de test. */
|
||||||
|
static gint64 test_social_account_count(Database *database, const char *sql)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = database_statement_prepare(database, sql);
|
||||||
|
int64_t count = -1;
|
||||||
|
assert(statement != NULL);
|
||||||
|
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
|
||||||
|
assert(database_statement_column_int64(statement, 0, &count));
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Vérifie la création et le rollback d'un doublon. */
|
||||||
|
static void test_social_account_create_and_duplicate(void)
|
||||||
|
{
|
||||||
|
char *directory = NULL;
|
||||||
|
char *path = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
SocialAccountInput input = {
|
||||||
|
.platform = "tiktok",
|
||||||
|
.profile_url = "https://www.tiktok.com/@profil.test",
|
||||||
|
.username = "@profil.test",
|
||||||
|
.platform_identifier = "123456789",
|
||||||
|
.first_observed_at = "2026-07-22T10:00:00Z",
|
||||||
|
.account_state = "active",
|
||||||
|
.notes = "URL transmise par la victime.",
|
||||||
|
.evidence_identifier = NULL
|
||||||
|
};
|
||||||
|
directory = g_dir_make_tmp("labfy-social-account-test-XXXXXX", &error);
|
||||||
|
assert(directory != NULL && error == NULL);
|
||||||
|
path = g_build_filename(directory, "Enquete.sqlite", NULL);
|
||||||
|
assert(database_initialize(path, "Enquete_Social", directory));
|
||||||
|
database = database_open(path);
|
||||||
|
assert(database != NULL);
|
||||||
|
assert(database_migrate_to_latest(database));
|
||||||
|
assert(social_account_service_create(database, &input, &identifier, &error));
|
||||||
|
assert(identifier != NULL && error == NULL);
|
||||||
|
assert(test_social_account_count(database, "SELECT COUNT(*) FROM entites;") == 1);
|
||||||
|
assert(test_social_account_count(database, "SELECT COUNT(*) FROM comptes_sociaux;") == 1);
|
||||||
|
g_clear_pointer(&identifier, g_free);
|
||||||
|
assert(!social_account_service_create(database, &input, &identifier, &error));
|
||||||
|
assert(error != NULL);
|
||||||
|
assert(test_social_account_count(database, "SELECT COUNT(*) FROM entites;") == 1);
|
||||||
|
assert(test_social_account_count(database, "SELECT COUNT(*) FROM comptes_sociaux;") == 1);
|
||||||
|
g_clear_error(&error);
|
||||||
|
database_close(database);
|
||||||
|
assert(g_remove(path) == 0);
|
||||||
|
assert(g_rmdir(directory) == 0);
|
||||||
|
g_free(path);
|
||||||
|
g_free(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_social_account_create_and_duplicate();
|
||||||
|
puts("SocialAccountService : tous les tests sont valides.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue