feat(evidence): edit evidence classification

This commit is contained in:
grayTerminal-sh 2026-07-22 14:50:02 +02:00
parent 06fe3bcb21
commit d876590529
15 changed files with 859 additions and 3 deletions

View file

@ -120,6 +120,7 @@ TEST_OSINT_DNS_PROPOSAL := tests/test_osint_dns_proposal
TEST_OSINT_DNS_INTEGRATION := tests/test_osint_dns_integration 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
all: $(TARGET) all: $(TARGET)
@ -595,6 +596,19 @@ $(TEST_OSINT_EXECUTION_INTEGRITY): \
src/core/osint_execution_integrity.c src/core/osint_execution_integrity.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_EVIDENCE_RECLASSIFICATION): \
tests/test_evidence_reclassification.c \
src/core/evidence_reclassification.c \
src/core/file_hash.c \
src/dao/evidence_dao.c \
src/models/evidence_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 \
@ -669,7 +683,8 @@ test: \
$(TEST_OSINT_DNS_PROPOSAL) \ $(TEST_OSINT_DNS_PROPOSAL) \
$(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)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -724,6 +739,7 @@ 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)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -784,7 +800,8 @@ clean:
$(TEST_OSINT_DNS_PROPOSAL) \ $(TEST_OSINT_DNS_PROPOSAL) \
$(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)
-include $(DEP) -include $(DEP)

View file

@ -138,6 +138,8 @@ Le socle actuel comprend notamment :
- panneau dactivité GTK ; - panneau dactivité GTK ;
- import groupé de preuves avec confirmation globale, révision individuelle, - import groupé de preuves avec confirmation globale, révision individuelle,
copie contrôlée, empreinte SHA-256 et bilan détaillé ; copie contrôlée, empreinte SHA-256 et bilan détaillé ;
- correction du type, de la source et de la description d'une preuve avec
contrôle d'intégrité et déplacement cohérent de sa copie interne ;
- registre doutils externes ; - registre doutils externes ;
- exécution sécurisée par `GSubprocess` ; - exécution sécurisée par `GSubprocess` ;
- exécution doutils en tâche de fond ; - exécution doutils en tâche de fond ;

View file

@ -766,6 +766,13 @@ importé séparément. Les imports sont exécutés séquentiellement afin d'évi
les écritures SQLite concurrentes. L'échec d'un fichier n'annule pas les les écritures SQLite concurrentes. L'échec d'un fichier n'annule pas les
preuves déjà importées et apparaît dans le bilan final. preuves déjà importées et apparaît dans le bilan final.
Le classement, la source et la description peuvent être corrigés après
l'import. Un changement de type déplace la copie interne vers le dossier
adapté dans la même opération logique que la mise à jour SQLite. L'empreinte
et la taille sont vérifiées avant et après ; un échec annule la transaction et
restaure le chemin initial. L'UUID, le nom interne et la date d'import restent
inchangés.
--- ---
## 4.6 entites ## 4.6 entites

View file

@ -0,0 +1,44 @@
/******************************************************************************
* @file evidence_reclassification.h
* @brief Reclassement cohérent d'une preuve sans modifier son contenu.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EVIDENCE_RECLASSIFICATION_H
#define LABFY_INVESTIGATION_EVIDENCE_RECLASSIFICATION_H
#include "database/database.h"
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Déplace une preuve et met à jour ses métadonnées avec retour arrière.
*
* L'intégrité est contrôlée avant et après le déplacement. Le contenu,
* l'UUID, le nom interne, la taille, l'empreinte et la date d'import ne sont
* jamais modifiés.
*
* @param database Connexion SQLite ouverte.
* @param investigation_root Racine de l'enquête.
* @param evidence_identifier UUID de la preuve.
* @param type_identifier Nouveau type métier.
* @param relative_directory Dossier relatif de destination.
* @param source Nouvelle source, ou NULL.
* @param description Nouvelle description, ou NULL.
* @param error Adresse recevant une éventuelle erreur.
* @return TRUE si le reclassement complet réussit.
*/
gboolean evidence_reclassification_apply(
Database *database,
const char *investigation_root,
const char *evidence_identifier,
const char *type_identifier,
const char *relative_directory,
const char *source,
const char *description,
GError **error
);
G_END_DECLS
#endif

View file

@ -144,6 +144,34 @@ gboolean evidence_dao_update_integrity_status(
GError **error GError **error
); );
/**
* @brief Met à jour le classement et les métadonnées éditables d'une preuve.
*
* L'UUID, le nom interne, la taille, l'empreinte et la date d'import restent
* inchangés. La transaction éventuelle reste sous la responsabilité de
* l'appelant.
*
* @param evidence_dao DAO valide.
* @param identifier UUID de la preuve.
* @param type_identifier Nouveau code de type.
* @param relative_path Nouveau chemin relatif du fichier.
* @param source Nouvelle source, ou NULL.
* @param description Nouvelle description, ou NULL.
* @param updated_at Date UTC de modification.
* @param error Adresse recevant une éventuelle erreur.
* @return TRUE si la mise à jour réussit.
*/
gboolean evidence_dao_update_metadata(
EvidenceDao *evidence_dao,
const char *identifier,
const char *type_identifier,
const char *relative_path,
const char *source,
const char *description,
const char *updated_at,
GError **error
);
/** /**
* @brief Compte les preuves persistées. * @brief Compte les preuves persistées.
* *

View file

@ -0,0 +1,53 @@
/******************************************************************************
* @file evidence_metadata_dialog.h
* @brief Dialogue de modification des métadonnées éditables d'une preuve.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
#define LABFY_INVESTIGATION_EVIDENCE_METADATA_DIALOG_H
#include "models/evidence_record.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
/** @brief Résultat opaque possédé du dialogue. */
typedef struct EvidenceMetadataDialogResult EvidenceMetadataDialogResult;
/** @brief Callback recevant le résultat possédé, ou NULL après annulation. */
typedef void (*EvidenceMetadataDialogCallback)(
EvidenceMetadataDialogResult *result, gpointer user_data
);
/**
* @brief Présente le dialogue prérempli de modification des métadonnées.
* @param parent Fenêtre parente.
* @param record Preuve empruntée.
* @param evidence_types Tableau emprunté de EvidenceType.
* @param callback Callback appelé une fois à la fermeture.
* @param user_data Données empruntées du callback.
* @return TRUE si la fenêtre a é créée.
*/
gboolean evidence_metadata_dialog_present(
GtkWindow *parent,
const EvidenceRecord *record,
const GPtrArray *evidence_types,
EvidenceMetadataDialogCallback callback,
gpointer user_data
);
/** @brief Libère un résultat, ou accepte NULL. */
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result);
/** @brief Retourne le code de type emprunté. */
const char *evidence_metadata_dialog_result_get_type_identifier(
const EvidenceMetadataDialogResult *result);
/** @brief Retourne la source empruntée, ou NULL. */
const char *evidence_metadata_dialog_result_get_source(
const EvidenceMetadataDialogResult *result);
/** @brief Retourne la description empruntée, ou NULL. */
const char *evidence_metadata_dialog_result_get_description(
const EvidenceMetadataDialogResult *result);
G_END_DECLS
#endif

View file

@ -86,6 +86,12 @@ typedef void (*MainWindowVerifyEvidenceCallback)(
gpointer user_data gpointer user_data
); );
/** @brief Callback appelé pour modifier les métadonnées d'une preuve. */
typedef void (*MainWindowEditEvidenceCallback)(
const char *evidence_identifier,
gpointer user_data
);
/** /**
* @brief Callback appelé après le déplacement effectif d'un nœud. * @brief Callback appelé après le déplacement effectif d'un nœud.
* *
@ -151,6 +157,18 @@ void main_window_set_verify_evidence_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Définit le callback de modification de la preuve sélectionnée.
* @param main_window Fenêtre principale.
* @param callback Callback facultatif.
* @param user_data Données privées transmises au callback.
*/
void main_window_set_edit_evidence_callback(
MainWindow *main_window,
MainWindowEditEvidenceCallback callback,
gpointer user_data
);
/** /**
* @brief Définit le callback de fin de déplacement d'un nœud. * @brief Définit le callback de fin de déplacement d'un nœud.
* *

View file

@ -95,6 +95,12 @@ typedef void (*WorkspaceOsintActionCallback)(
gpointer user_data gpointer user_data
); );
/** @brief Callback appelé pour modifier les métadonnées d'une preuve. */
typedef void (*WorkspaceEditEvidenceCallback)(
const char *evidence_identifier,
gpointer user_data
);
/** /**
* @brief Crée une nouvelle zone de travail. * @brief Crée une nouvelle zone de travail.
* *
@ -204,6 +210,18 @@ void workspace_set_osint_action_callback(
gpointer user_data gpointer user_data
); );
/**
* @brief Définit le callback de modification de la preuve sélectionnée.
* @param workspace Zone de travail.
* @param callback Callback facultatif.
* @param user_data Données empruntées transmises au callback.
*/
void workspace_set_edit_evidence_callback(
Workspace *workspace,
WorkspaceEditEvidenceCallback callback,
gpointer user_data
);
/** /**
* @brief Définit le callback de vérification de la preuve affichée. * @brief Définit le callback de vérification de la preuve affichée.
* *

View file

@ -27,12 +27,14 @@
#include "core/tool_task.h" #include "core/tool_task.h"
#include "core/osint_dns_integration.h" #include "core/osint_dns_integration.h"
#include "core/osint_execution_integrity.h" #include "core/osint_execution_integrity.h"
#include "core/evidence_reclassification.h"
#include "views/file_dialog.h" #include "views/file_dialog.h"
#include "core/evidence_import_task.h" #include "core/evidence_import_task.h"
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include "dao/evidence_type_dao.h" #include "dao/evidence_type_dao.h"
#include "dao/graph_node_position_dao.h" #include "dao/graph_node_position_dao.h"
#include "views/evidence_import_dialog.h" #include "views/evidence_import_dialog.h"
#include "views/evidence_metadata_dialog.h"
#include "views/create_relation_dialog.h" #include "views/create_relation_dialog.h"
#include "views/osint_dns_review_dialog.h" #include "views/osint_dns_review_dialog.h"
#include "views/osint_execution_history_dialog.h" #include "views/osint_execution_history_dialog.h"
@ -159,6 +161,20 @@ typedef struct
char *file_path; char *file_path;
} ApplicationEvidenceDialogContext; } ApplicationEvidenceDialogContext;
/** @brief Contexte possédé pendant l'édition d'une preuve. */
typedef struct
{
Application *application;
char *evidence_identifier;
} ApplicationEvidenceMetadataContext;
/** @brief Données d'actualisation différée après reclassement. */
typedef struct
{
Application *application;
char *root_path;
} ApplicationEvidenceRefreshContext;
/** /**
* @brief Contexte conservé jusqu'à la fin d'une vérification. * @brief Contexte conservé jusqu'à la fin d'une vérification.
*/ */
@ -3138,6 +3154,131 @@ static const char *application_get_evidence_subdirectory(
return "Documents"; return "Documents";
} }
/** @brief Libère le contexte d'édition des métadonnées. */
static void application_evidence_metadata_context_free(
ApplicationEvidenceMetadataContext *context
)
{
if (context == NULL) return;
g_free(context->evidence_identifier); g_free(context);
}
/** @brief Actualise les vues après la destruction du dialogue d'édition. */
static gboolean application_refresh_after_evidence_reclassification(
gpointer user_data
)
{
ApplicationEvidenceRefreshContext *context = user_data;
GError *error = NULL;
if (context != NULL && context->application != NULL &&
context->application->main_window != NULL)
{
application_refresh_investigation_tree(
context->application, context->root_path);
application_refresh_evidence_models(context->application, &error);
main_window_set_selected_evidence(
context->application->main_window, NULL);
application_message_dialog_present(
main_window_get_window(context->application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Preuve reclassée",
"Les métadonnées et le dossier ont été mis à jour. Le SHA-256 est inchangé.");
}
g_clear_error(&error);
if (context != NULL) { g_free(context->root_path); g_free(context); }
return G_SOURCE_REMOVE;
}
/** @brief Applique le reclassement confirmé puis actualise les vues. */
static void application_on_evidence_metadata_edited(
EvidenceMetadataDialogResult *result, gpointer user_data
)
{
ApplicationEvidenceMetadataContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
const InvestigationProject *project = NULL;
const char *root_path = NULL; const char *subdirectory = NULL;
char *relative_directory = NULL; GError *error = NULL;
if (result == NULL)
{
application_evidence_metadata_context_free(context);
return;
}
if (application == NULL || application->session == NULL) goto cleanup;
project = investigation_session_get_project(application->session);
root_path = project != NULL ? investigation_project_get_root_path(project) : NULL;
subdirectory = application_get_evidence_subdirectory(
evidence_metadata_dialog_result_get_type_identifier(result));
relative_directory = g_build_filename(
APPLICATION_EVIDENCE_ROOT_DIRECTORY, subdirectory, NULL);
if (!evidence_reclassification_apply(
investigation_session_get_database(application->session), root_path,
context->evidence_identifier,
evidence_metadata_dialog_result_get_type_identifier(result),
relative_directory,
evidence_metadata_dialog_result_get_source(result),
evidence_metadata_dialog_result_get_description(result), &error))
{
application_present_error(application, "Modification impossible",
error != NULL ? error->message : "Le reclassement a échoué.");
g_clear_error(&error); goto cleanup;
}
{
ApplicationEvidenceRefreshContext *refresh_context =
g_new0(ApplicationEvidenceRefreshContext, 1);
refresh_context->application = application;
refresh_context->root_path = g_strdup(root_path);
g_timeout_add(50U,
application_refresh_after_evidence_reclassification,
refresh_context);
}
cleanup:
g_free(relative_directory);
evidence_metadata_dialog_result_free(result);
application_evidence_metadata_context_free(context);
}
/** @brief Charge et présente les métadonnées éditables d'une preuve. */
static void application_on_edit_evidence_requested(
const char *evidence_identifier, gpointer user_data
)
{
Application *application = user_data; Database *database = NULL;
EvidenceDao *evidence_dao = NULL; EvidenceTypeDao *type_dao = NULL;
EvidenceRecord *record = NULL; GPtrArray *types = NULL;
ApplicationEvidenceMetadataContext *context = NULL; GError *error = NULL;
if (application == NULL || application->session == NULL ||
evidence_identifier == NULL) return;
database = investigation_session_get_database(application->session);
evidence_dao = evidence_dao_new(database, &error);
type_dao = evidence_type_dao_new(database, &error);
if (evidence_dao != NULL)
record = evidence_dao_find_by_identifier(
evidence_dao, evidence_identifier, &error);
if (type_dao != NULL) types = evidence_type_dao_list_all(type_dao, &error);
if (record == NULL || types == NULL || types->len == 0U)
{
application_present_error(application, "Modification impossible",
error != NULL ? error->message : "Les métadonnées sont indisponibles.");
goto cleanup;
}
context = g_new0(ApplicationEvidenceMetadataContext, 1);
context->application = application;
context->evidence_identifier = g_strdup(evidence_identifier);
if (!evidence_metadata_dialog_present(
main_window_get_window(application->main_window), record, types,
application_on_evidence_metadata_edited, context))
{
application_evidence_metadata_context_free(context);
context = NULL;
application_present_error(application, "Modification impossible",
"Le dialogue de métadonnées n'a pas pu être créé.");
}
cleanup:
g_clear_error(&error); g_clear_pointer(&types, g_ptr_array_unref);
evidence_record_free(record); evidence_type_dao_free(type_dao);
evidence_dao_free(evidence_dao);
}
/** /**
* @brief Prépare et démarre limport asynchrone dun fichier. * @brief Prépare et démarre limport asynchrone dun fichier.
*/ */
@ -5952,6 +6093,12 @@ static void application_on_activate(
application application
); );
main_window_set_edit_evidence_callback(
application->main_window,
application_on_edit_evidence_requested,
application
);
main_window_set_graph_node_moved_callback( main_window_set_graph_node_moved_callback(
application->main_window, application->main_window,
application_on_graph_node_moved, application_on_graph_node_moved,

View file

@ -0,0 +1,120 @@
/******************************************************************************
* @file evidence_reclassification.c
* @brief Reclassement cohérent d'une preuve sans modifier son contenu.
******************************************************************************/
#include "core/evidence_reclassification.h"
#include "core/file_hash.h"
#include "dao/evidence_dao.h"
#include "database/transaction.h"
#include "models/evidence_record.h"
#include <glib/gstdio.h>
#include <errno.h>
/** @brief Enregistre une erreur de reclassement si aucune n'existe encore. */
static void evidence_reclassification_set_error(
GError **error, const char *message
)
{
if (error != NULL && *error == NULL)
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, message);
}
gboolean evidence_reclassification_apply(
Database *database, const char *investigation_root,
const char *evidence_identifier, const char *type_identifier,
const char *relative_directory, const char *source,
const char *description, GError **error
)
{
EvidenceDao *dao = NULL; EvidenceRecord *record = NULL;
char *old_path = NULL; char *new_path = NULL; char *new_relative_path = NULL;
char *new_parent = NULL; char *sha256 = NULL; char *updated_at = NULL;
guint64 size_bytes = 0U; GDateTime *now = NULL;
gboolean moved = FALSE; gboolean transaction_started = FALSE;
gboolean success = FALSE;
if (database == NULL || investigation_root == NULL ||
evidence_identifier == NULL || type_identifier == NULL ||
relative_directory == NULL || g_path_is_absolute(relative_directory) ||
strstr(relative_directory, "..") != NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres de reclassement sont invalides.");
return FALSE;
}
dao = evidence_dao_new(database, error);
if (dao == NULL) goto cleanup;
record = evidence_dao_find_by_identifier(dao, evidence_identifier, error);
if (record == NULL) goto cleanup;
old_path = g_build_filename(investigation_root,
evidence_record_get_relative_path(record), NULL);
new_relative_path = g_build_filename(relative_directory,
evidence_record_get_internal_name(record), NULL);
new_path = g_build_filename(investigation_root, new_relative_path, NULL);
new_parent = g_path_get_dirname(new_path);
if (!file_hash_compute_sha256(old_path, NULL, &sha256, &size_bytes, error) ||
g_strcmp0(sha256, evidence_record_get_sha256(record)) != 0 ||
size_bytes != evidence_record_get_size_bytes(record))
{
evidence_reclassification_set_error(error,
"L'intégrité de la preuve source n'est pas conforme.");
goto cleanup;
}
g_clear_pointer(&sha256, g_free);
if (g_mkdir_with_parents(new_parent, 0700) != 0)
{
g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno),
"Impossible de préparer le dossier de destination : %s",
g_strerror(errno));
goto cleanup;
}
if (!database_transaction_begin(database))
{
evidence_reclassification_set_error(error,
"Impossible de démarrer la transaction de reclassement.");
goto cleanup;
}
transaction_started = TRUE;
if (g_strcmp0(old_path, new_path) != 0)
{
if (g_file_test(new_path, G_FILE_TEST_EXISTS) ||
g_rename(old_path, new_path) != 0)
{
g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno),
"Impossible de déplacer la preuve : %s", g_strerror(errno));
goto cleanup;
}
moved = TRUE;
}
now = g_date_time_new_now_utc();
updated_at = now != NULL
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
if (!evidence_dao_update_metadata(dao, evidence_identifier,
type_identifier, new_relative_path, source, description,
updated_at, error) ||
!file_hash_compute_sha256(new_path, NULL, &sha256, &size_bytes, error) ||
g_strcmp0(sha256, evidence_record_get_sha256(record)) != 0 ||
size_bytes != evidence_record_get_size_bytes(record))
{
evidence_reclassification_set_error(error,
"L'intégrité après reclassement n'est pas conforme.");
goto cleanup;
}
if (!database_transaction_commit(database))
{
evidence_reclassification_set_error(error,
"Impossible de valider le reclassement.");
goto cleanup;
}
transaction_started = FALSE; success = TRUE;
cleanup:
if (!success && transaction_started) database_transaction_rollback(database);
if (!success && moved && g_rename(new_path, old_path) != 0)
g_warning("Impossible de restaurer la preuve '%s'.", old_path);
g_clear_pointer(&now, g_date_time_unref); g_free(updated_at); g_free(sha256);
g_free(new_parent); g_free(new_path); g_free(new_relative_path); g_free(old_path);
evidence_record_free(record); evidence_dao_free(dao);
return success;
}

View file

@ -156,6 +156,11 @@ static const char *const evidence_dao_update_integrity_status_sql =
"SET integrity_status = ? " "SET integrity_status = ? "
"WHERE id = ?;"; "WHERE id = ?;";
/** @brief Requête de mise à jour des métadonnées éditables. */
static const char *const evidence_dao_update_metadata_sql =
"UPDATE preuves SET type_id=(SELECT id FROM types_preuve WHERE code=?),"
"relative_path=?,source=?,description=?,updated_at=? WHERE id=?;";
/** /**
* @brief Enregistre une erreur littérale. * @brief Enregistre une erreur littérale.
*/ */
@ -1344,6 +1349,49 @@ cleanup:
return success; return success;
} }
gboolean evidence_dao_update_metadata(
EvidenceDao *evidence_dao, const char *identifier,
const char *type_identifier, const char *relative_path,
const char *source, const char *description, const char *updated_at,
GError **error
)
{
DatabaseStatement *statement = NULL;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (evidence_dao == NULL || evidence_dao->database == NULL ||
identifier == NULL || !g_uuid_string_is_valid(identifier) ||
type_identifier == NULL || type_identifier[0] == '\0' ||
relative_path == NULL || relative_path[0] == '\0' ||
g_path_is_absolute(relative_path) || updated_at == NULL ||
strlen(updated_at) != 20U)
{
evidence_dao_set_error_literal(error,
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
"Les métadonnées de reclassement sont invalides.");
return FALSE;
}
statement = database_statement_prepare(
evidence_dao->database, evidence_dao_update_metadata_sql);
if (statement == NULL) goto cleanup;
success = database_statement_bind_text(statement, 1, type_identifier) &&
database_statement_bind_text(statement, 2, relative_path) &&
(source != NULL ? database_statement_bind_text(statement, 3, source)
: database_statement_bind_null(statement, 3)) &&
(description != NULL
? database_statement_bind_text(statement, 4, description)
: database_statement_bind_null(statement, 4)) &&
database_statement_bind_text(statement, 5, updated_at) &&
database_statement_bind_text(statement, 6, identifier) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
cleanup:
if (!success) evidence_dao_set_database_error(evidence_dao, error,
EVIDENCE_DAO_ERROR_EXECUTE,
"Impossible de mettre à jour les métadonnées de la preuve");
database_statement_finalize(statement);
return success;
}
gboolean evidence_dao_count( gboolean evidence_dao_count(
EvidenceDao *evidence_dao, EvidenceDao *evidence_dao,
guint64 *out_count, guint64 *out_count,

View file

@ -0,0 +1,212 @@
/******************************************************************************
* @file evidence_metadata_dialog.c
* @brief Dialogue de modification des métadonnées éditables d'une preuve.
******************************************************************************/
#include "views/evidence_metadata_dialog.h"
#include "models/evidence_type.h"
struct EvidenceMetadataDialogResult
{
char *type_identifier;
char *source;
char *description;
};
typedef struct
{
GtkWindow *window;
GtkDropDown *type_dropdown;
GtkEditable *source_entry;
GtkTextBuffer *description_buffer;
GtkStringList *type_labels;
GPtrArray *type_identifiers;
EvidenceMetadataDialogCallback callback;
gpointer user_data;
gboolean completed;
} EvidenceMetadataDialogContext;
/** @brief Copie un texte facultatif et transforme une chaîne vide en NULL. */
static char *evidence_metadata_dialog_copy_optional(const char *text)
{
char *copy = text != NULL ? g_strdup(text) : NULL;
if (copy != NULL) g_strstrip(copy);
if (copy != NULL && copy[0] == '\0') g_clear_pointer(&copy, g_free);
return copy;
}
/** @brief Libère le contexte détenu par la fenêtre. */
static void evidence_metadata_dialog_context_free(gpointer data)
{
EvidenceMetadataDialogContext *context = data;
if (context == NULL) return;
g_clear_pointer(&context->type_identifiers, g_ptr_array_unref);
g_clear_object(&context->type_labels);
g_free(context);
}
/** @brief Détruit la fenêtre hors de la pile du signal GTK courant. */
static gboolean evidence_metadata_dialog_destroy_idle(gpointer data)
{
GtkWindow *window = GTK_WINDOW(data);
gtk_window_destroy(window);
g_object_unref(window);
return G_SOURCE_REMOVE;
}
/** @brief Termine le dialogue et transmet éventuellement un résultat. */
static void evidence_metadata_dialog_complete(
EvidenceMetadataDialogContext *context,
EvidenceMetadataDialogResult *result
)
{
if (context == NULL || context->completed) return;
context->completed = TRUE;
if (context->callback != NULL) context->callback(result, context->user_data);
else evidence_metadata_dialog_result_free(result);
gtk_widget_set_visible(GTK_WIDGET(context->window), FALSE);
g_idle_add(
evidence_metadata_dialog_destroy_idle,
g_object_ref(context->window)
);
}
/** @brief Annule la modification. */
static void evidence_metadata_dialog_on_cancel(
GtkButton *button, gpointer user_data
)
{
(void) button;
evidence_metadata_dialog_complete(user_data, NULL);
}
/** @brief Traite la fermeture native comme une annulation explicite. */
static gboolean evidence_metadata_dialog_on_close_request(
GtkWindow *window, gpointer user_data
)
{
(void) window;
evidence_metadata_dialog_complete(user_data, NULL);
return TRUE;
}
/** @brief Construit et transmet les valeurs modifiées. */
static void evidence_metadata_dialog_on_save(
GtkButton *button, gpointer user_data
)
{
EvidenceMetadataDialogContext *context = user_data;
EvidenceMetadataDialogResult *result = NULL;
GtkTextIter start; GtkTextIter end; char *description = NULL;
guint selected = 0U;
(void) button;
if (context == NULL) return;
selected = gtk_drop_down_get_selected(context->type_dropdown);
if (selected >= context->type_identifiers->len) return;
gtk_text_buffer_get_bounds(context->description_buffer, &start, &end);
description = gtk_text_buffer_get_text(
context->description_buffer, &start, &end, FALSE);
result = g_new0(EvidenceMetadataDialogResult, 1);
result->type_identifier = g_strdup(
g_ptr_array_index(context->type_identifiers, selected));
result->source = evidence_metadata_dialog_copy_optional(
gtk_editable_get_text(context->source_entry));
result->description = evidence_metadata_dialog_copy_optional(description);
g_free(description);
evidence_metadata_dialog_complete(context, result);
}
gboolean evidence_metadata_dialog_present(
GtkWindow *parent, const EvidenceRecord *record,
const GPtrArray *evidence_types, EvidenceMetadataDialogCallback callback,
gpointer user_data
)
{
EvidenceMetadataDialogContext *context = NULL;
GtkStringList *labels = NULL; GtkWidget *main_box = NULL;
GtkWidget *description_view = NULL; GtkWidget *description_scroll = NULL;
GtkWidget *buttons = NULL; GtkWidget *cancel = NULL; GtkWidget *save = NULL;
guint selected = 0U;
if (record == NULL || evidence_types == NULL || evidence_types->len == 0U ||
callback == NULL) return FALSE;
context = g_new0(EvidenceMetadataDialogContext, 1);
context->window = GTK_WINDOW(gtk_window_new());
context->callback = callback; context->user_data = user_data;
context->type_identifiers = g_ptr_array_new_with_free_func(g_free);
labels = gtk_string_list_new(NULL);
context->type_labels = labels;
for (guint index = 0U; index < evidence_types->len; index++)
{
const EvidenceType *type = g_ptr_array_index((GPtrArray *) evidence_types, index);
gtk_string_list_append(labels, evidence_type_get_label(type));
g_ptr_array_add(context->type_identifiers,
g_strdup(evidence_type_get_code(type)));
if (g_strcmp0(evidence_type_get_code(type),
evidence_record_get_type_identifier(record)) == 0) selected = index;
}
gtk_window_set_title(context->window, "Modifier les métadonnées");
gtk_window_set_default_size(context->window, 560, 420);
gtk_window_set_modal(context->window, TRUE);
gtk_window_set_destroy_with_parent(context->window, TRUE);
if (parent != NULL) gtk_window_set_transient_for(context->window, parent);
g_object_set_data_full(G_OBJECT(context->window), "evidence-metadata-context",
context, evidence_metadata_dialog_context_free);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(main_box, 18); gtk_widget_set_margin_end(main_box, 18);
gtk_widget_set_margin_top(main_box, 18); gtk_widget_set_margin_bottom(main_box, 18);
gtk_box_append(GTK_BOX(main_box), gtk_label_new(
"Le fichier, son UUID, sa taille, sa date d'import et son SHA-256 resteront inchangés."));
context->type_dropdown = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(labels), NULL));
gtk_drop_down_set_selected(context->type_dropdown, selected);
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Type de preuve"));
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->type_dropdown));
context->source_entry = GTK_EDITABLE(gtk_entry_new());
gtk_editable_set_text(context->source_entry,
evidence_record_get_source(record) != NULL
? evidence_record_get_source(record) : "");
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Source"));
gtk_box_append(GTK_BOX(main_box), GTK_WIDGET(context->source_entry));
description_view = gtk_text_view_new();
context->description_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(description_view));
gtk_text_buffer_set_text(context->description_buffer,
evidence_record_get_description(record) != NULL
? evidence_record_get_description(record) : "", -1);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(description_view), GTK_WRAP_WORD_CHAR);
description_scroll = gtk_scrolled_window_new();
gtk_widget_set_vexpand(description_scroll, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(description_scroll), description_view);
gtk_box_append(GTK_BOX(main_box), gtk_label_new("Description"));
gtk_box_append(GTK_BOX(main_box), description_scroll);
buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(buttons, GTK_ALIGN_END);
cancel = gtk_button_new_with_label("Annuler");
save = gtk_button_new_with_label("Enregistrer");
gtk_widget_add_css_class(save, "suggested-action");
g_signal_connect(cancel, "clicked", G_CALLBACK(evidence_metadata_dialog_on_cancel), context);
g_signal_connect(save, "clicked", G_CALLBACK(evidence_metadata_dialog_on_save), context);
g_signal_connect(context->window, "close-request",
G_CALLBACK(evidence_metadata_dialog_on_close_request), context);
gtk_box_append(GTK_BOX(buttons), cancel); gtk_box_append(GTK_BOX(buttons), save);
gtk_box_append(GTK_BOX(main_box), buttons);
gtk_window_set_child(context->window, main_box);
gtk_window_present(context->window);
return TRUE;
}
void evidence_metadata_dialog_result_free(EvidenceMetadataDialogResult *result)
{
if (result == NULL) return;
g_free(result->type_identifier); g_free(result->source);
g_free(result->description); g_free(result);
}
const char *evidence_metadata_dialog_result_get_type_identifier(
const EvidenceMetadataDialogResult *result)
{ return result != NULL ? result->type_identifier : NULL; }
const char *evidence_metadata_dialog_result_get_source(
const EvidenceMetadataDialogResult *result)
{ return result != NULL ? result->source : NULL; }
const char *evidence_metadata_dialog_result_get_description(
const EvidenceMetadataDialogResult *result)
{ return result != NULL ? result->description : NULL; }

View file

@ -24,7 +24,7 @@
/** /**
* @brief Position initiale de la séparation horizontale. * @brief Position initiale de la séparation horizontale.
*/ */
#define MAIN_WINDOW_SIDEBAR_POSITION 320 #define MAIN_WINDOW_SIDEBAR_POSITION 640
/** /**
* @brief Position initiale de la séparation verticale. * @brief Position initiale de la séparation verticale.
@ -99,6 +99,9 @@ struct MainWindow
gpointer gpointer
verify_evidence_user_data; verify_evidence_user_data;
MainWindowEditEvidenceCallback edit_evidence_callback;
gpointer edit_evidence_user_data;
MainWindowGraphNodeMovedCallback MainWindowGraphNodeMovedCallback
graph_node_moved_callback; graph_node_moved_callback;
@ -156,6 +159,17 @@ static void main_window_on_osint_action_requested(
); );
} }
/** @brief Relaie la modification d'une preuve vers l'application. */
static void main_window_on_edit_evidence_requested(
const char *evidence_identifier, gpointer user_data
)
{
MainWindow *main_window = user_data;
if (main_window == NULL || main_window->edit_evidence_callback == NULL) return;
main_window->edit_evidence_callback(
evidence_identifier, main_window->edit_evidence_user_data);
}
/** /**
* @brief Ouvre dans le workspace l'entité choisie dans la sidebar. * @brief Ouvre dans le workspace l'entité choisie dans la sidebar.
*/ */
@ -782,6 +796,12 @@ MainWindow *main_window_new(
main_window main_window
); );
workspace_set_edit_evidence_callback(
main_window->workspace,
main_window_on_edit_evidence_requested,
main_window
);
workspace_widget = workspace_get_widget( workspace_widget = workspace_get_widget(
main_window->workspace main_window->workspace
); );
@ -1297,6 +1317,16 @@ void main_window_set_osint_action_callback(
main_window->osint_action_user_data = user_data; main_window->osint_action_user_data = user_data;
} }
void main_window_set_edit_evidence_callback(
MainWindow *main_window, MainWindowEditEvidenceCallback callback,
gpointer user_data
)
{
if (main_window == NULL) return;
main_window->edit_evidence_callback = callback;
main_window->edit_evidence_user_data = user_data;
}
void main_window_set_tree_selection_callback( void main_window_set_tree_selection_callback(
MainWindow *main_window, MainWindow *main_window,
InvestigationTreeViewSelectionCallback callback, InvestigationTreeViewSelectionCallback callback,

View file

@ -85,6 +85,7 @@ struct Workspace
GtkWidget *evidence_identifier_label; GtkWidget *evidence_identifier_label;
GtkWidget *evidence_sha256_label; GtkWidget *evidence_sha256_label;
GtkWidget *verify_evidence_button; GtkWidget *verify_evidence_button;
GtkWidget *edit_evidence_button;
char *selected_evidence_identifier; char *selected_evidence_identifier;
@ -94,6 +95,9 @@ struct Workspace
gpointer gpointer
verify_evidence_user_data; verify_evidence_user_data;
WorkspaceEditEvidenceCallback edit_evidence_callback;
gpointer edit_evidence_user_data;
WorkspaceGraphNodeMovedCallback WorkspaceGraphNodeMovedCallback
graph_node_moved_callback; graph_node_moved_callback;
@ -834,6 +838,20 @@ static void workspace_on_verify_evidence_clicked(
); );
} }
/** @brief Transmet la demande de modification de la preuve affichée. */
static void workspace_on_edit_evidence_clicked(
GtkButton *button, gpointer user_data
)
{
Workspace *workspace = user_data;
(void) button;
if (workspace == NULL || workspace->edit_evidence_callback == NULL ||
workspace->selected_evidence_identifier == NULL) return;
workspace->edit_evidence_callback(
workspace->selected_evidence_identifier,
workspace->edit_evidence_user_data);
}
Workspace *workspace_new(void) Workspace *workspace_new(void)
{ {
GtkWidget *evidence_content = NULL; GtkWidget *evidence_content = NULL;
@ -1153,6 +1171,16 @@ Workspace *workspace_new(void)
workspace->verify_evidence_button workspace->verify_evidence_button
); );
workspace->edit_evidence_button = gtk_button_new_with_label(
"Modifier les métadonnées");
gtk_widget_set_halign(workspace->edit_evidence_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
gtk_widget_set_tooltip_text(workspace->edit_evidence_button,
"Corriger le classement sans modifier le fichier original");
g_signal_connect(workspace->edit_evidence_button, "clicked",
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
evidence_separator = evidence_separator =
gtk_separator_new( gtk_separator_new(
GTK_ORIENTATION_HORIZONTAL GTK_ORIENTATION_HORIZONTAL
@ -2079,6 +2107,8 @@ void workspace_set_selected_node(
FALSE FALSE
); );
} }
if (workspace->edit_evidence_button != NULL)
gtk_widget_set_sensitive(workspace->edit_evidence_button, FALSE);
if (node == NULL) if (node == NULL)
{ {
@ -2233,6 +2263,7 @@ void workspace_set_selected_evidence(
workspace->verify_evidence_button, workspace->verify_evidence_button,
TRUE TRUE
); );
gtk_widget_set_sensitive(workspace->edit_evidence_button, TRUE);
size_bytes = size_bytes =
evidence_record_get_size_bytes( evidence_record_get_size_bytes(
@ -2774,6 +2805,16 @@ void workspace_set_verify_evidence_callback(
user_data; user_data;
} }
void workspace_set_edit_evidence_callback(
Workspace *workspace, WorkspaceEditEvidenceCallback callback,
gpointer user_data
)
{
if (workspace == NULL) return;
workspace->edit_evidence_callback = callback;
workspace->edit_evidence_user_data = user_data;
}
void workspace_set_graph_node_moved_callback( void workspace_set_graph_node_moved_callback(
Workspace *workspace, Workspace *workspace,
WorkspaceGraphNodeMovedCallback callback, WorkspaceGraphNodeMovedCallback callback,

View file

@ -0,0 +1,71 @@
/******************************************************************************
* @file test_evidence_reclassification.c
* @brief Tests du reclassement cohérent des preuves.
******************************************************************************/
#include "core/evidence_reclassification.h"
#include "core/file_hash.h"
#include "dao/evidence_dao.h"
#include "database/database.h"
#include "models/evidence_record.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_reclassification_preserves_identity_and_hash(void)
{
GError *error = NULL; guint64 size = 0U; char *sha256 = NULL;
char *root = g_dir_make_tmp("labfy-reclassify-XXXXXX", &error);
char *database_dir = g_build_filename(root, "00_BaseDeDonnees", NULL);
char *database_path = g_build_filename(database_dir, "Enquete.sqlite", NULL);
char *old_dir = g_build_filename(root, "01_Preuves_Originales", "Documents", NULL);
char *old_path = g_build_filename(old_dir, "internal.bin", NULL);
char *new_path = g_build_filename(root, "01_Preuves_Originales", "Photos",
"internal.bin", NULL);
Database *database = NULL; EvidenceDao *dao = NULL;
EvidenceRecord *record = NULL; EvidenceRecord *loaded = NULL;
g_assert_no_error(error);
g_assert_cmpint(g_mkdir_with_parents(database_dir, 0700), ==, 0);
g_assert_cmpint(g_mkdir_with_parents(old_dir, 0700), ==, 0);
g_assert_true(g_file_set_contents(old_path, "binary\0content", 14, &error));
g_assert_true(file_hash_compute_sha256(old_path, NULL, &sha256, &size, &error));
g_assert_true(database_initialize(database_path, "Test", root));
database = database_open(database_path); dao = evidence_dao_new(database, &error);
record = evidence_record_new(
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "original.bin", "internal.bin",
"01_Preuves_Originales/Documents/internal.bin", "document", size, sha256,
"2026-07-22T10:00:00Z", "2026-07-18T10:00:00Z", "Victime", "Avant",
EVIDENCE_INTEGRITY_STATUS_UNKNOWN, &error);
g_assert_nonnull(record); g_assert_true(evidence_dao_insert(dao, record, &error));
g_assert_true(evidence_reclassification_apply(
database, root, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "photo",
"01_Preuves_Originales/Photos", "Victime", "Après", &error));
g_assert_false(g_file_test(old_path, G_FILE_TEST_EXISTS));
g_assert_true(g_file_test(new_path, G_FILE_TEST_IS_REGULAR));
loaded = evidence_dao_find_by_identifier(
dao, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", &error);
g_assert_cmpstr(evidence_record_get_type_identifier(loaded), ==, "photo");
g_assert_cmpstr(evidence_record_get_sha256(loaded), ==, sha256);
g_assert_cmpstr(evidence_record_get_imported_at(loaded), ==,
"2026-07-22T10:00:00Z");
g_assert_cmpstr(evidence_record_get_description(loaded), ==, "Après");
evidence_record_free(loaded); evidence_record_free(record);
evidence_dao_free(dao); database_close(database);
g_assert_cmpint(g_remove(new_path), ==, 0);
g_assert_cmpint(g_remove(database_path), ==, 0);
char *photos = g_path_get_dirname(new_path);
char *originals = g_path_get_dirname(photos);
g_assert_cmpint(g_rmdir(old_dir), ==, 0); g_assert_cmpint(g_rmdir(photos), ==, 0);
g_assert_cmpint(g_rmdir(originals), ==, 0); g_assert_cmpint(g_rmdir(database_dir), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0);
g_free(photos); g_free(originals); g_free(sha256); g_free(new_path);
g_free(old_path); g_free(old_dir); g_free(database_path); g_free(database_dir); g_free(root);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-reclassification/preserve",
test_reclassification_preserves_identity_and_hash);
return g_test_run();
}