feat: add asynchronous evidence integrity task

This commit is contained in:
grayTerminal-sh 2026-07-20 13:41:41 +02:00
parent a4765e8de2
commit 5ad959315a
10 changed files with 4302 additions and 3 deletions

View file

@ -37,6 +37,10 @@ EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS := \
EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := \
$(shell $(PKG_CONFIG) --libs gtk4)
EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := \
$(TEST_CFLAGS) \
-Wpedantic
SRC := $(shell find src -name "*.c")
OBJ := $(SRC:.c=.o)
@ -64,6 +68,7 @@ TEST_TOOL_TASK := tests/test_tool_task
TEST_TOOL_CATALOG := tests/test_tool_catalog
TEST_TOOL_INITIALIZER := tests/test_tool_initializer
TEST_FILE_HASH := tests/test_file_hash
TEST_EVIDENCE_INTEGRITY_VERIFIER := tests/test_evidence_integrity_verifier
TEST_EVIDENCE_COPY := tests/test_evidence_copy
TEST_EVIDENCE_IMPORTER := tests/test_evidence_importer
TEST_EVIDENCE_IMPORT_TASK := tests/test_evidence_import_task
@ -74,6 +79,7 @@ TEST_EVIDENCE_LIST_ITEM := tests/test_evidence_list_item
TEST_EVIDENCE_LIST_MODEL := tests/test_evidence_list_model
TEST_EVIDENCE_CATEGORY_ITEM := tests/test_evidence_category_item
TEST_EVIDENCE_CATEGORY_MODEL := tests/test_evidence_category_model
TEST_EVIDENCE_INTEGRITY_TASK := tests/test_evidence_integrity_task
all: $(TARGET)
@ -254,6 +260,13 @@ $(TEST_FILE_HASH): \
-DFILE_HASH_ENABLE_TEST_HOOKS \
$^ -o $@ $(TEST_LDFLAGS)
$(TEST_EVIDENCE_INTEGRITY_VERIFIER): \
tests/test_evidence_integrity_verifier.c \
src/core/evidence_integrity_verifier.c \
src/core/file_hash.c
$(CC) $(EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS) \
$^ -o $@ $(TEST_LDFLAGS)
$(TEST_EVIDENCE_COPY): \
tests/test_evidence_copy.c \
src/core/evidence_copy.c \
@ -336,6 +349,17 @@ $(TEST_EVIDENCE_CATEGORY_MODEL): \
src/models/evidence_record.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_EVIDENCE_INTEGRITY_TASK): \
tests/test_evidence_integrity_task.c \
src/core/evidence_integrity_task.c \
src/core/evidence_integrity_verifier.c \
src/core/file_hash.c \
src/core/background_task.c \
src/core/task_manager.c
$(CC) $(TEST_CFLAGS) \
-DFILE_HASH_ENABLE_TEST_HOOKS \
$^ -o $@ $(TEST_LDFLAGS)
test: \
$(TEST_NODE) \
$(TEST_TREE_MODEL) \
@ -364,10 +388,12 @@ test: \
$(TEST_TOOL_CATALOG) \
$(TEST_TOOL_INITIALIZER) \
$(TEST_FILE_HASH) \
$(TEST_EVIDENCE_INTEGRITY_VERIFIER) \
$(TEST_EVIDENCE_COPY) \
$(TEST_EVIDENCE_IMPORTER) \
$(TEST_EVIDENCE_IMPORT_TASK) \
$(TEST_EVIDENCE_IMPORT_DIALOG)
$(TEST_EVIDENCE_IMPORT_DIALOG) \
$(TEST_EVIDENCE_INTEGRITY_TASK)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -396,10 +422,12 @@ test: \
@$(TEST_TOOL_CATALOG)
@$(TEST_TOOL_INITIALIZER)
@$(TEST_FILE_HASH)
@$(TEST_EVIDENCE_INTEGRITY_VERIFIER)
@$(TEST_EVIDENCE_COPY)
@$(TEST_EVIDENCE_IMPORTER)
@$(TEST_EVIDENCE_IMPORT_TASK)
@$(TEST_EVIDENCE_IMPORT_DIALOG)
@$(TEST_EVIDENCE_INTEGRITY_TASK)
@echo "Tous les tests sont valides."
%.o: %.c
@ -437,10 +465,12 @@ clean:
$(TEST_TOOL_CATALOG) \
$(TEST_TOOL_INITIALIZER) \
$(TEST_FILE_HASH) \
$(TEST_EVIDENCE_INTEGRITY_VERIFIER) \
$(TEST_EVIDENCE_COPY) \
$(TEST_EVIDENCE_IMPORTER) \
$(TEST_EVIDENCE_IMPORT_TASK) \
$(TEST_EVIDENCE_IMPORT_DIALOG)
$(TEST_EVIDENCE_IMPORT_DIALOG) \
$(TEST_EVIDENCE_INTEGRITY_TASK)
.PHONY: clean run test

View file

@ -0,0 +1,80 @@
/******************************************************************************
* @file evidence_integrity_task.h
* @brief Tâche asynchrone de vérification d'intégrité d'une preuve.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_TASK_H
#define LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_TASK_H
#include "core/background_task.h"
#include "core/task_manager.h"
#include <glib.h>
/**
* @brief Codes d'erreur propres à la tâche de vérification.
*/
typedef enum
{
EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT,
EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY
} EvidenceIntegrityTaskError;
/**
* @brief Domaine d'erreur du module.
*/
#define EVIDENCE_INTEGRITY_TASK_ERROR \
evidence_integrity_task_error_quark()
/**
* @brief Paramètres nécessaires à une vérification d'intégrité.
*
* Toutes les chaînes sont copiées avant le démarrage du worker.
*/
typedef struct
{
const char *investigation_root_path;
const char *relative_path;
const char *expected_sha256;
} EvidenceIntegrityTaskRequest;
/**
* @brief Retourne le domaine d'erreur du module.
*/
GQuark evidence_integrity_task_error_quark(void);
/**
* @brief Crée, enregistre et démarre une tâche de vérification.
*
* Le résultat final est un EvidenceIntegrityVerificationResult emprunté
* à la BackgroundTask. Il reste valide tant que la tâche existe.
*
* En cas de succès :
*
* - TaskManager conserve une référence ;
* - le worker conserve une référence pendant son exécution ;
* - l'appelant reçoit sa référence initiale et doit la libérer avec
* background_task_unref().
*
* completion_data est transféré à BackgroundTask uniquement si le
* démarrage réussit. En cas d'échec, l'appelant en reste propriétaire.
*
* @param task_manager Gestionnaire recevant la tâche.
* @param request Paramètres de la vérification.
* @param completion_callback Callback final facultatif.
* @param completion_data Données du callback final.
* @param completion_data_destroy Destructeur de completion_data.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouvelle tâche démarrée, ou NULL.
*/
BackgroundTask *evidence_integrity_task_start(
TaskManager *task_manager,
const EvidenceIntegrityTaskRequest *request,
BackgroundTaskCompletionCallback completion_callback,
gpointer completion_data,
GDestroyNotify completion_data_destroy,
GError **error
);
#endif

View file

@ -0,0 +1,115 @@
/******************************************************************************
* @file evidence_integrity_verifier.h
* @brief Vérification de l'intégrité d'une preuve numérique.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_VERIFIER_H
#define LABFY_INVESTIGATION_EVIDENCE_INTEGRITY_VERIFIER_H
#include "models/evidence_record.h"
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Erreurs techniques du vérificateur.
*/
typedef enum
{
EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY
} EvidenceIntegrityVerifierError;
#define EVIDENCE_INTEGRITY_VERIFIER_ERROR \
evidence_integrity_verifier_error_quark()
GQuark evidence_integrity_verifier_error_quark(void);
/**
* @brief Résultat opaque d'une vérification.
*/
typedef struct EvidenceIntegrityVerificationResult
EvidenceIntegrityVerificationResult;
/**
* @brief Vérifie une preuve à partir de son chemin relatif.
*
* Le chemin est résolu sous investigation_root_path.
* Les chemins absolus, les composants "." et ".." et les liens
* symboliques sont refusés.
*
* Les résultats MISSING et ERROR sont des résultats métier normaux :
* la fonction retourne alors un objet résultat sans remplir GError.
*
* Une erreur technique d'argument, de racine, de mémoire ou une
* annulation retourne NULL.
*
* @param investigation_root_path Racine de l'enquête.
* @param relative_path Chemin relatif enregistré dans SQLite.
* @param expected_sha256 Empreinte enregistrée.
* @param cancellable Objet d'annulation facultatif.
* @param error Adresse recevant une erreur technique.
*
* @return Nouveau résultat possédé par l'appelant, ou NULL.
*/
EvidenceIntegrityVerificationResult *
evidence_integrity_verifier_verify(
const char *investigation_root_path,
const char *relative_path,
const char *expected_sha256,
GCancellable *cancellable,
GError **error
);
/**
* @brief Retourne le statut calculé.
*/
EvidenceIntegrityStatus
evidence_integrity_verification_result_get_status(
const EvidenceIntegrityVerificationResult *result
);
/**
* @brief Retourne l'empreinte recalculée.
*
* La chaîne est empruntée. Elle vaut NULL lorsque le fichier
* n'a pas pu être lu.
*/
const char *
evidence_integrity_verification_result_get_computed_sha256(
const EvidenceIntegrityVerificationResult *result
);
/**
* @brief Retourne la taille effectivement lue.
*/
guint64 evidence_integrity_verification_result_get_size_bytes(
const EvidenceIntegrityVerificationResult *result
);
/**
* @brief Retourne un diagnostic technique facultatif.
*
* La chaîne est empruntée.
*/
const char *
evidence_integrity_verification_result_get_diagnostic(
const EvidenceIntegrityVerificationResult *result
);
/**
* @brief Libère un résultat.
*
* Cette fonction accepte NULL.
*/
void evidence_integrity_verification_result_free(
EvidenceIntegrityVerificationResult *result
);
G_END_DECLS
#endif

View file

@ -17,6 +17,7 @@
typedef enum
{
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
EVIDENCE_DAO_ERROR_NOT_FOUND,
EVIDENCE_DAO_ERROR_MEMORY,
EVIDENCE_DAO_ERROR_PREPARE,
EVIDENCE_DAO_ERROR_BIND,
@ -124,6 +125,25 @@ GPtrArray *evidence_dao_list_all(
GError **error
);
/**
* @brief Met à jour le statut d'intégrité d'une preuve.
*
* Seule la colonne du statut d'intégrité est modifiée.
*
* @param evidence_dao DAO valide.
* @param identifier Identifiant UUID de la preuve.
* @param integrity_status Nouveau statut d'intégrité.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si la mise à jour réussit, sinon FALSE.
*/
gboolean evidence_dao_update_integrity_status(
EvidenceDao *evidence_dao,
const char *identifier,
EvidenceIntegrityStatus integrity_status,
GError **error
);
/**
* @brief Compte les preuves persistées.
*

View file

@ -0,0 +1,522 @@
/******************************************************************************
* @file evidence_integrity_task.c
* @brief Exécution asynchrone de la vérification d'intégrité.
******************************************************************************/
#include "core/evidence_integrity_task.h"
#include "core/evidence_integrity_verifier.h"
#include <gio/gio.h>
#include <glib.h>
/**
* @brief Copie privée des paramètres nécessaires au worker.
*/
typedef struct
{
char *investigation_root_path;
char *relative_path;
char *expected_sha256;
} EvidenceIntegrityTaskData;
/**
* @brief Libère les données privées du worker.
*/
static void evidence_integrity_task_data_free(
gpointer user_data
)
{
EvidenceIntegrityTaskData *task_data =
user_data;
if (task_data == NULL)
{
return;
}
g_free(
task_data->expected_sha256
);
g_free(
task_data->relative_path
);
g_free(
task_data->investigation_root_path
);
g_free(
task_data
);
}
/**
* @brief Vérifie les paramètres obligatoires.
*/
static gboolean evidence_integrity_task_request_is_valid(
const EvidenceIntegrityTaskRequest *request
)
{
if (request == NULL)
{
return FALSE;
}
if (request->investigation_root_path == NULL ||
request->investigation_root_path[0] == '\0')
{
return FALSE;
}
if (request->relative_path == NULL ||
request->relative_path[0] == '\0')
{
return FALSE;
}
if (request->expected_sha256 == NULL ||
request->expected_sha256[0] == '\0')
{
return FALSE;
}
return TRUE;
}
/**
* @brief Copie les paramètres avant le lancement du thread.
*/
static EvidenceIntegrityTaskData *
evidence_integrity_task_data_new(
const EvidenceIntegrityTaskRequest *request
)
{
EvidenceIntegrityTaskData *task_data =
NULL;
if (!evidence_integrity_task_request_is_valid(
request
))
{
return NULL;
}
task_data =
g_try_new0(
EvidenceIntegrityTaskData,
1
);
if (task_data == NULL)
{
return NULL;
}
task_data->investigation_root_path =
g_strdup(
request->investigation_root_path
);
task_data->relative_path =
g_strdup(
request->relative_path
);
task_data->expected_sha256 =
g_strdup(
request->expected_sha256
);
return task_data;
}
/**
* @brief Libère le résultat de vérification d'une tâche.
*/
static void evidence_integrity_task_result_free(
gpointer user_data
)
{
evidence_integrity_verification_result_free(
user_data
);
}
/**
* @brief Traduit une annulation métier en annulation GIO.
*
* BackgroundTask reconnaît l'état CANCELLED uniquement avec :
*
* G_IO_ERROR / G_IO_ERROR_CANCELLED.
*/
static void evidence_integrity_task_propagate_verify_error(
GError **error,
GError *verify_error
)
{
if (verify_error != NULL &&
g_error_matches(
verify_error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED
))
{
g_clear_error(
&verify_error
);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"La vérification d'intégrité a été annulée."
);
return;
}
if (verify_error != NULL)
{
g_propagate_error(
error,
verify_error
);
return;
}
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_TASK_ERROR,
EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY,
"La vérification a échoué sans fournir d'erreur."
);
}
/**
* @brief Retourne le message final correspondant au statut métier.
*/
static const char *evidence_integrity_task_get_status_message(
EvidenceIntegrityStatus integrity_status
)
{
switch (integrity_status)
{
case EVIDENCE_INTEGRITY_STATUS_VALID:
return "Intégrité vérifiée : fichier intact";
case EVIDENCE_INTEGRITY_STATUS_MODIFIED:
return "Intégrité compromise : empreinte différente";
case EVIDENCE_INTEGRITY_STATUS_MISSING:
return "Vérification terminée : fichier absent";
case EVIDENCE_INTEGRITY_STATUS_ERROR:
return "Vérification terminée : erreur de lecture";
case EVIDENCE_INTEGRITY_STATUS_UNKNOWN:
default:
return "Vérification terminée";
}
}
/**
* @brief Exécute la vérification dans le thread secondaire.
*/
static gboolean evidence_integrity_task_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
EvidenceIntegrityTaskData *task_data =
worker_data;
EvidenceIntegrityVerificationResult *verification_result =
NULL;
EvidenceIntegrityStatus integrity_status =
EVIDENCE_INTEGRITY_STATUS_UNKNOWN;
GError *verify_error =
NULL;
if (task == NULL ||
cancellable == NULL ||
task_data == NULL ||
result == NULL ||
error == NULL)
{
return FALSE;
}
*result =
NULL;
if (g_cancellable_set_error_if_cancelled(
cancellable,
error
))
{
return FALSE;
}
background_task_report_progress(
task,
0.10,
"Préparation de la vérification"
);
verification_result =
evidence_integrity_verifier_verify(
task_data->investigation_root_path,
task_data->relative_path,
task_data->expected_sha256,
cancellable,
&verify_error
);
if (verification_result == NULL)
{
evidence_integrity_task_propagate_verify_error(
error,
verify_error
);
return FALSE;
}
integrity_status =
evidence_integrity_verification_result_get_status(
verification_result
);
background_task_report_progress(
task,
1.0,
evidence_integrity_task_get_status_message(
integrity_status
)
);
*result =
verification_result;
return TRUE;
}
/**
* @brief Transmet une erreur secondaire au code appelant.
*/
static void evidence_integrity_task_propagate_start_error(
GError **error,
GError *start_error,
const char *prefix
)
{
if (start_error == NULL)
{
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_TASK_ERROR,
EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY,
"La tâche de vérification n'a pas pu être démarrée."
);
return;
}
if (error != NULL)
{
g_propagate_prefixed_error(
error,
start_error,
"%s",
prefix
);
return;
}
g_clear_error(
&start_error
);
}
GQuark evidence_integrity_task_error_quark(void)
{
return g_quark_from_static_string(
"evidence-integrity-task-error-quark"
);
}
BackgroundTask *evidence_integrity_task_start(
TaskManager *task_manager,
const EvidenceIntegrityTaskRequest *request,
BackgroundTaskCompletionCallback completion_callback,
gpointer completion_data,
GDestroyNotify completion_data_destroy,
GError **error
)
{
EvidenceIntegrityTaskData *task_data =
NULL;
BackgroundTask *task =
NULL;
char *evidence_name =
NULL;
char *task_title =
NULL;
GError *start_error =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (task_manager == NULL ||
!evidence_integrity_task_request_is_valid(
request
))
{
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_TASK_ERROR,
EVIDENCE_INTEGRITY_TASK_ERROR_INVALID_ARGUMENT,
"Les paramètres de la tâche de vérification sont invalides."
);
return NULL;
}
task_data =
evidence_integrity_task_data_new(
request
);
if (task_data == NULL)
{
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_TASK_ERROR,
EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY,
"Impossible d'allouer les données de la tâche "
"de vérification."
);
return NULL;
}
evidence_name =
g_path_get_basename(
request->relative_path
);
task_title =
g_strdup_printf(
"Vérifier l'intégrité : %s",
evidence_name
);
task =
background_task_new(
task_title
);
g_free(
task_title
);
g_free(
evidence_name
);
if (task == NULL)
{
evidence_integrity_task_data_free(
task_data
);
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_TASK_ERROR,
EVIDENCE_INTEGRITY_TASK_ERROR_VERIFY,
"Impossible de créer la tâche de vérification."
);
return NULL;
}
if (!task_manager_add(
task_manager,
task,
&start_error
))
{
evidence_integrity_task_data_free(
task_data
);
background_task_unref(
task
);
evidence_integrity_task_propagate_start_error(
error,
start_error,
"Impossible d'ajouter la tâche : "
);
return NULL;
}
if (!background_task_start(
task,
evidence_integrity_task_worker,
task_data,
evidence_integrity_task_data_free,
evidence_integrity_task_result_free,
completion_callback,
completion_data,
completion_data_destroy,
&start_error
))
{
/*
* background_task_start() n'a pas pris possession
* de task_data ni de completion_data en cas d'échec.
*/
evidence_integrity_task_data_free(
task_data
);
task_manager_remove(
task_manager,
task
);
background_task_unref(
task
);
evidence_integrity_task_propagate_start_error(
error,
start_error,
"Impossible de démarrer la tâche : "
);
return NULL;
}
return task;
}

View file

@ -0,0 +1,666 @@
/******************************************************************************
* @file evidence_integrity_verifier.c
* @brief Vérification de l'intégrité d'une preuve numérique.
******************************************************************************/
#define _XOPEN_SOURCE 700
#include "core/evidence_integrity_verifier.h"
#include "core/file_hash.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/**
* @brief Résultat interne d'une vérification.
*/
struct EvidenceIntegrityVerificationResult
{
EvidenceIntegrityStatus status;
char *computed_sha256;
guint64 size_bytes;
char *diagnostic;
};
/**
* @brief Enregistre une erreur technique.
*/
static void evidence_integrity_verifier_set_error_literal(
GError **error,
EvidenceIntegrityVerifierError error_code,
const char *message
)
{
if (error == NULL)
{
return;
}
g_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR,
error_code,
message
);
}
/**
* @brief Vérifie qu'une empreinte SHA-256 est canonique.
*/
static gboolean evidence_integrity_verifier_sha256_is_valid(
const char *sha256
)
{
gsize character_index = 0;
if (sha256 == NULL ||
strlen(sha256) != 64U)
{
return FALSE;
}
for (character_index = 0;
character_index < 64U;
character_index++)
{
if (!g_ascii_isdigit(sha256[character_index]) &&
(
sha256[character_index] < 'a' ||
sha256[character_index] > 'f'
))
{
return FALSE;
}
}
return TRUE;
}
/**
* @brief Crée un résultat métier.
*/
static EvidenceIntegrityVerificationResult *
evidence_integrity_verification_result_new(
EvidenceIntegrityStatus status,
const char *computed_sha256,
guint64 size_bytes,
const char *diagnostic,
GError **error
)
{
EvidenceIntegrityVerificationResult *result =
NULL;
result =
g_try_new0(
EvidenceIntegrityVerificationResult,
1
);
if (result == NULL)
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY,
"Impossible d'allouer le résultat de la vérification."
);
return NULL;
}
result->status =
status;
result->size_bytes =
size_bytes;
if (computed_sha256 != NULL)
{
result->computed_sha256 =
g_strdup(
computed_sha256
);
}
if (diagnostic != NULL)
{
result->diagnostic =
g_strdup(
diagnostic
);
}
return result;
}
/**
* @brief Vérifie si une annulation a é demandée.
*/
static gboolean evidence_integrity_verifier_is_cancelled(
GCancellable *cancellable,
GError **error
)
{
if (cancellable == NULL ||
!g_cancellable_is_cancelled(
cancellable
))
{
return FALSE;
}
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED,
"La vérification d'intégrité a été annulée."
);
return TRUE;
}
GQuark evidence_integrity_verifier_error_quark(void)
{
return g_quark_from_static_string(
"evidence-integrity-verifier-error-quark"
);
}
EvidenceIntegrityVerificationResult *
evidence_integrity_verifier_verify(
const char *investigation_root_path,
const char *relative_path,
const char *expected_sha256,
GCancellable *cancellable,
GError **error
)
{
EvidenceIntegrityVerificationResult *result =
NULL;
GError *hash_error =
NULL;
char **path_components =
NULL;
char *resolved_root_path =
NULL;
char *current_path =
NULL;
char *next_path =
NULL;
char *computed_sha256 =
NULL;
char *diagnostic =
NULL;
struct stat root_status;
struct stat path_status;
guint64 size_bytes =
0;
guint component_index =
0;
gboolean is_last_component =
FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (investigation_root_path == NULL ||
investigation_root_path[0] == '\0' ||
relative_path == NULL ||
relative_path[0] == '\0' ||
!evidence_integrity_verifier_sha256_is_valid(
expected_sha256
))
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_INVALID_ARGUMENT,
"Les paramètres de la vérification sont invalides."
);
return NULL;
}
if (evidence_integrity_verifier_is_cancelled(
cancellable,
error
))
{
return NULL;
}
/*
* La racine existe déjà lorsqu'une InvestigationSession est ouverte.
* realpath() résout son éventuel lien symbolique et la canonicalise.
*/
resolved_root_path =
realpath(
investigation_root_path,
NULL
);
if (resolved_root_path == NULL)
{
g_set_error(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT,
"Impossible de résoudre la racine '%s' : %s",
investigation_root_path,
g_strerror(errno)
);
return NULL;
}
if (stat(
resolved_root_path,
&root_status
) != 0 ||
!S_ISDIR(root_status.st_mode))
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_ROOT,
"La racine de l'enquête n'est pas un dossier valide."
);
goto cleanup;
}
/*
* Un chemin SQLite doit toujours être relatif à l'enquête.
*/
if (g_path_is_absolute(
relative_path
))
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
"Le chemin enregistré est absolu.",
error
);
goto cleanup;
}
path_components =
g_strsplit(
relative_path,
G_DIR_SEPARATOR_S,
-1
);
if (path_components == NULL)
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY,
"Impossible de décomposer le chemin de la preuve."
);
goto cleanup;
}
current_path =
g_strdup(
resolved_root_path
);
if (current_path == NULL)
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY,
"Impossible de conserver le chemin de la racine."
);
goto cleanup;
}
for (component_index = 0;
path_components[component_index] != NULL;
component_index++)
{
const char *component =
path_components[component_index];
is_last_component =
path_components[component_index + 1U] == NULL;
/*
* Le refus explicite évite toute traversée hors de la racine.
*/
if (component[0] == '\0' ||
g_strcmp0(component, ".") == 0 ||
g_strcmp0(component, "..") == 0)
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
"Le chemin contient un composant interdit.",
error
);
goto cleanup;
}
next_path =
g_build_filename(
current_path,
component,
NULL
);
if (next_path == NULL)
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_MEMORY,
"Impossible de construire le chemin de la preuve."
);
goto cleanup;
}
g_free(
current_path
);
current_path =
next_path;
next_path =
NULL;
if (lstat(
current_path,
&path_status
) != 0)
{
if (errno == ENOENT ||
errno == ENOTDIR)
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_MISSING,
NULL,
0,
"Le fichier de preuve est absent.",
error
);
}
else
{
diagnostic =
g_strdup_printf(
"Impossible d'inspecter '%s' : %s",
current_path,
g_strerror(errno)
);
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
diagnostic,
error
);
}
goto cleanup;
}
/*
* Tous les liens symboliques sont refusés, y compris lorsqu'ils
* semblent rester à l'intérieur de l'enquête.
*/
if (S_ISLNK(path_status.st_mode))
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
"Le chemin de la preuve contient un lien symbolique.",
error
);
goto cleanup;
}
if (!is_last_component &&
!S_ISDIR(path_status.st_mode))
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
"Un composant intermédiaire du chemin "
"n'est pas un dossier.",
error
);
goto cleanup;
}
}
if (evidence_integrity_verifier_is_cancelled(
cancellable,
error
))
{
goto cleanup;
}
if (!file_hash_compute_sha256(
current_path,
cancellable,
&computed_sha256,
&size_bytes,
&hash_error
))
{
if (g_error_matches(
hash_error,
FILE_HASH_ERROR,
FILE_HASH_ERROR_CANCELLED
))
{
evidence_integrity_verifier_set_error_literal(
error,
EVIDENCE_INTEGRITY_VERIFIER_ERROR_CANCELLED,
"La vérification d'intégrité a été annulée."
);
goto cleanup;
}
if (g_error_matches(
hash_error,
FILE_HASH_ERROR,
FILE_HASH_ERROR_NOT_FOUND
))
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_MISSING,
NULL,
0,
hash_error->message,
error
);
goto cleanup;
}
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_ERROR,
NULL,
0,
hash_error != NULL
? hash_error->message
: "Le calcul SHA-256 a échoué.",
error
);
goto cleanup;
}
if (strcmp(
computed_sha256,
expected_sha256
) == 0)
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_VALID,
computed_sha256,
size_bytes,
NULL,
error
);
}
else
{
result =
evidence_integrity_verification_result_new(
EVIDENCE_INTEGRITY_STATUS_MODIFIED,
computed_sha256,
size_bytes,
"L'empreinte recalculée diffère "
"de l'empreinte enregistrée.",
error
);
}
cleanup:
g_clear_error(
&hash_error
);
g_free(
diagnostic
);
g_free(
computed_sha256
);
g_free(
next_path
);
g_free(
current_path
);
g_strfreev(
path_components
);
free(
resolved_root_path
);
return result;
}
EvidenceIntegrityStatus
evidence_integrity_verification_result_get_status(
const EvidenceIntegrityVerificationResult *result
)
{
if (result == NULL)
{
return EVIDENCE_INTEGRITY_STATUS_ERROR;
}
return result->status;
}
const char *
evidence_integrity_verification_result_get_computed_sha256(
const EvidenceIntegrityVerificationResult *result
)
{
if (result == NULL)
{
return NULL;
}
return result->computed_sha256;
}
guint64 evidence_integrity_verification_result_get_size_bytes(
const EvidenceIntegrityVerificationResult *result
)
{
if (result == NULL)
{
return 0;
}
return result->size_bytes;
}
const char *
evidence_integrity_verification_result_get_diagnostic(
const EvidenceIntegrityVerificationResult *result
)
{
if (result == NULL)
{
return NULL;
}
return result->diagnostic;
}
void evidence_integrity_verification_result_free(
EvidenceIntegrityVerificationResult *result
)
{
if (result == NULL)
{
return;
}
g_free(
result->diagnostic
);
g_free(
result->computed_sha256
);
g_free(
result
);
}

View file

@ -148,6 +148,14 @@ static const char *const evidence_dao_list_all_sql =
" preuves.imported_at ASC,"
" preuves.id ASC;";
/**
* @brief Requête de mise à jour du statut d'intégrité.
*/
static const char *const evidence_dao_update_integrity_status_sql =
"UPDATE preuves "
"SET integrity_status = ? "
"WHERE id = ?;";
/**
* @brief Enregistre une erreur littérale.
*/
@ -1191,6 +1199,151 @@ cleanup:
return evidence_record;
}
gboolean evidence_dao_update_integrity_status(
EvidenceDao *evidence_dao,
const char *identifier,
EvidenceIntegrityStatus integrity_status,
GError **error
)
{
DatabaseStatement *statement =
NULL;
gboolean identifier_exists =
FALSE;
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
))
{
evidence_dao_set_error_literal(
error,
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
"Le DAO ou l'identifiant de preuve est invalide."
);
return FALSE;
}
if (integrity_status <
EVIDENCE_INTEGRITY_STATUS_UNKNOWN ||
integrity_status >
EVIDENCE_INTEGRITY_STATUS_ERROR)
{
evidence_dao_set_error_literal(
error,
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
"Le statut d'intégrité demandé est invalide."
);
return FALSE;
}
/*
* Cette vérification permet de distinguer une preuve absente
* d'une mise à jour SQLite ayant réellement échoué.
*/
if (!evidence_dao_value_exists(
evidence_dao,
evidence_dao_identifier_exists_sql,
identifier,
&identifier_exists,
error
))
{
return FALSE;
}
if (!identifier_exists)
{
evidence_dao_set_error_literal(
error,
EVIDENCE_DAO_ERROR_NOT_FOUND,
"La preuve à mettre à jour n'existe pas."
);
return FALSE;
}
statement =
database_statement_prepare(
evidence_dao->database,
evidence_dao_update_integrity_status_sql
);
if (statement == NULL)
{
evidence_dao_set_database_error(
evidence_dao,
error,
EVIDENCE_DAO_ERROR_PREPARE,
"Impossible de préparer la mise à jour "
"du statut d'intégrité"
);
goto cleanup;
}
if (!database_statement_bind_int64(
statement,
1,
(int64_t) integrity_status
) ||
!database_statement_bind_text(
statement,
2,
identifier
))
{
evidence_dao_set_database_error(
evidence_dao,
error,
EVIDENCE_DAO_ERROR_BIND,
"Impossible de lier les paramètres "
"du statut d'intégrité"
);
goto cleanup;
}
if (database_statement_step(
statement
) != DATABASE_STATEMENT_STEP_DONE)
{
evidence_dao_set_database_error(
evidence_dao,
error,
EVIDENCE_DAO_ERROR_EXECUTE,
"Impossible de mettre à jour "
"le statut d'intégrité"
);
goto cleanup;
}
success =
TRUE;
cleanup:
database_statement_finalize(
statement
);
return success;
}
gboolean evidence_dao_count(
EvidenceDao *evidence_dao,
guint64 *out_count,

View file

@ -468,6 +468,75 @@ static TestEvidenceDaoFixture test_evidence_dao_fixture_create(void)
return fixture;
}
/**
* @brief Crée une preuve destinée aux tests de statut d'intégrité.
*/
static EvidenceRecord *test_evidence_dao_create_integrity_record(
const char *identifier,
EvidenceIntegrityStatus integrity_status
)
{
EvidenceRecord *evidence_record =
NULL;
char *internal_name =
NULL;
char *relative_path =
NULL;
GError *error =
NULL;
assert(identifier != NULL);
internal_name =
g_strdup_printf(
"%s.bin",
identifier
);
relative_path =
g_strdup_printf(
"01_Preuves_Originales/Documents/%s.bin",
identifier
);
assert(internal_name != NULL);
assert(relative_path != NULL);
evidence_record =
evidence_record_new(
identifier,
"preuve_integrite.bin",
internal_name,
relative_path,
"document",
128,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"2026-07-20T10:00:00Z",
NULL,
NULL,
NULL,
integrity_status,
&error
);
assert(evidence_record != NULL);
assert(error == NULL);
g_free(
relative_path
);
g_free(
internal_name
);
return evidence_record;
}
/**
* @brief Détruit une fixture et ses fichiers temporaires.
*/
@ -1743,13 +1812,315 @@ static void test_evidence_dao_list_invalid_arguments(void)
);
}
/**
* @brief Vérifie la mise à jour d'une preuve vers VALID.
*/
static void test_evidence_dao_update_integrity_valid(void)
{
TestEvidenceDaoFixture fixture =
test_evidence_dao_fixture_create();
EvidenceRecord *inserted_record =
NULL;
EvidenceRecord *loaded_record =
NULL;
GError *error =
NULL;
const char *identifier =
"12345678-1111-4111-8111-111111111111";
inserted_record =
test_evidence_dao_create_integrity_record(
identifier,
EVIDENCE_INTEGRITY_STATUS_UNKNOWN
);
assert(
evidence_dao_insert(
fixture.evidence_dao,
inserted_record,
&error
)
);
assert(error == NULL);
assert(
evidence_dao_update_integrity_status(
fixture.evidence_dao,
identifier,
EVIDENCE_INTEGRITY_STATUS_VALID,
&error
)
);
assert(error == NULL);
loaded_record =
evidence_dao_find_by_identifier(
fixture.evidence_dao,
identifier,
&error
);
assert(loaded_record != NULL);
assert(error == NULL);
assert(
evidence_record_get_integrity_status(
loaded_record
) == EVIDENCE_INTEGRITY_STATUS_VALID
);
evidence_record_free(
loaded_record
);
evidence_record_free(
inserted_record
);
test_evidence_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie la mise à jour d'une preuve vers MODIFIED.
*/
static void test_evidence_dao_update_integrity_modified(void)
{
TestEvidenceDaoFixture fixture =
test_evidence_dao_fixture_create();
EvidenceRecord *inserted_record =
NULL;
EvidenceRecord *loaded_record =
NULL;
GError *error =
NULL;
const char *identifier =
"12345678-2222-4222-8222-222222222222";
inserted_record =
test_evidence_dao_create_integrity_record(
identifier,
EVIDENCE_INTEGRITY_STATUS_VALID
);
assert(
evidence_dao_insert(
fixture.evidence_dao,
inserted_record,
&error
)
);
assert(error == NULL);
assert(
evidence_dao_update_integrity_status(
fixture.evidence_dao,
identifier,
EVIDENCE_INTEGRITY_STATUS_MODIFIED,
&error
)
);
assert(error == NULL);
loaded_record =
evidence_dao_find_by_identifier(
fixture.evidence_dao,
identifier,
&error
);
assert(loaded_record != NULL);
assert(error == NULL);
assert(
evidence_record_get_integrity_status(
loaded_record
) == EVIDENCE_INTEGRITY_STATUS_MODIFIED
);
evidence_record_free(
loaded_record
);
evidence_record_free(
inserted_record
);
test_evidence_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie le signalement d'une preuve inexistante.
*/
static void test_evidence_dao_update_integrity_missing(void)
{
TestEvidenceDaoFixture fixture =
test_evidence_dao_fixture_create();
GError *error =
NULL;
assert(
!evidence_dao_update_integrity_status(
fixture.evidence_dao,
"12345678-3333-4333-8333-333333333333",
EVIDENCE_INTEGRITY_STATUS_VALID,
&error
)
);
assert(error != NULL);
assert(error->domain == EVIDENCE_DAO_ERROR);
assert(
error->code ==
(gint) EVIDENCE_DAO_ERROR_NOT_FOUND
);
g_clear_error(
&error
);
test_evidence_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie le refus d'un statut hors de l'énumération.
*/
static void test_evidence_dao_update_integrity_invalid_status(void)
{
TestEvidenceDaoFixture fixture =
test_evidence_dao_fixture_create();
GError *error =
NULL;
assert(
!evidence_dao_update_integrity_status(
fixture.evidence_dao,
"12345678-4444-4444-8444-444444444444",
(EvidenceIntegrityStatus)
(EVIDENCE_INTEGRITY_STATUS_ERROR + 1),
&error
)
);
assert(error != NULL);
assert(error->domain == EVIDENCE_DAO_ERROR);
assert(
error->code ==
(gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT
);
g_clear_error(
&error
);
test_evidence_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie le refus des arguments invalides.
*/
static void test_evidence_dao_update_integrity_invalid_arguments(void)
{
TestEvidenceDaoFixture fixture =
test_evidence_dao_fixture_create();
GError *error =
NULL;
assert(
!evidence_dao_update_integrity_status(
NULL,
"12345678-5555-4555-8555-555555555555",
EVIDENCE_INTEGRITY_STATUS_VALID,
&error
)
);
assert(error != NULL);
assert(
error->code ==
(gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT
);
g_clear_error(
&error
);
assert(
!evidence_dao_update_integrity_status(
fixture.evidence_dao,
NULL,
EVIDENCE_INTEGRITY_STATUS_VALID,
&error
)
);
assert(error != NULL);
assert(
error->code ==
(gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT
);
g_clear_error(
&error
);
assert(
!evidence_dao_update_integrity_status(
fixture.evidence_dao,
"identifiant-invalide",
EVIDENCE_INTEGRITY_STATUS_VALID,
&error
)
);
assert(error != NULL);
assert(
error->code ==
(gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT
);
g_clear_error(
&error
);
test_evidence_dao_fixture_clear(
&fixture
);
}
int main(void)
{
test_evidence_dao_new_null_database();
test_evidence_dao_new_valid();
test_evidence_dao_free_null();
test_evidence_dao_insert_valid_full();
test_evidence_dao_insert_valid_full();
test_evidence_dao_insert_optional_null();
test_evidence_dao_insert_duplicate_sha256();
test_evidence_dao_insert_duplicate_identifier();
@ -1764,6 +2135,11 @@ int main(void)
test_evidence_dao_list_empty();
test_evidence_dao_list_order();
test_evidence_dao_list_invalid_arguments();
test_evidence_dao_update_integrity_valid();
test_evidence_dao_update_integrity_modified();
test_evidence_dao_update_integrity_missing();
test_evidence_dao_update_integrity_invalid_status();
test_evidence_dao_update_integrity_invalid_arguments();
printf(
"EvidenceDao : tests de construction valides.\n"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff