feat: add secure external tool execution

This commit is contained in:
grayTerminal-sh 2026-07-17 15:42:30 +02:00
parent c0ef751434
commit dc45abaf0d
7 changed files with 3340 additions and 3 deletions

View file

@ -43,6 +43,7 @@ TEST_INVESTIGATION_SESSION := tests/test_investigation_session
TEST_BACKGROUND_TASK := tests/test_background_task TEST_BACKGROUND_TASK := tests/test_background_task
TEST_TASK_MANAGER := tests/test_task_manager TEST_TASK_MANAGER := tests/test_task_manager
TEST_TOOL_REGISTRY := tests/test_tool_registry TEST_TOOL_REGISTRY := tests/test_tool_registry
TEST_TOOL_PROCESS := tests/test_tool_process
all: $(TARGET) all: $(TARGET)
@ -156,6 +157,12 @@ $(TEST_TOOL_REGISTRY): \
src/core/tool_registry.c src/core/tool_registry.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_TOOL_PROCESS): \
tests/test_tool_process.c \
src/core/tool_process.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
test: \ test: \
$(TEST_NODE) \ $(TEST_NODE) \
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
@ -170,7 +177,8 @@ test: \
$(TEST_INVESTIGATION_SESSION) \ $(TEST_INVESTIGATION_SESSION) \
$(TEST_BACKGROUND_TASK) \ $(TEST_BACKGROUND_TASK) \
$(TEST_TASK_MANAGER) \ $(TEST_TASK_MANAGER) \
$(TEST_TOOL_REGISTRY) $(TEST_TOOL_REGISTRY) \
$(TEST_TOOL_PROCESS)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -186,6 +194,7 @@ test: \
@$(TEST_BACKGROUND_TASK) @$(TEST_BACKGROUND_TASK)
@$(TEST_TASK_MANAGER) @$(TEST_TASK_MANAGER)
@$(TEST_TOOL_REGISTRY) @$(TEST_TOOL_REGISTRY)
@$(TEST_TOOL_PROCESS)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -209,6 +218,7 @@ clean:
$(TEST_INVESTIGATION_SESSION) \ $(TEST_INVESTIGATION_SESSION) \
$(TEST_BACKGROUND_TASK) \ $(TEST_BACKGROUND_TASK) \
$(TEST_TASK_MANAGER) \ $(TEST_TASK_MANAGER) \
$(TEST_TOOL_REGISTRY) $(TEST_TOOL_REGISTRY) \
$(TEST_TOOL_PROCESS)
.PHONY: clean run test .PHONY: clean run test

File diff suppressed because it is too large Load diff

189
include/core/tool_process.h Normal file
View file

@ -0,0 +1,189 @@
/******************************************************************************
* @file tool_process.h
* @brief Exécution sécurisée des outils externes.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_TOOL_PROCESS_H
#define LABFY_INVESTIGATION_TOOL_PROCESS_H
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Erreurs produites pendant lexécution dun outil externe.
*/
typedef enum
{
/**
* Un argument transmis à la fonction est invalide.
*/
TOOL_PROCESS_ERROR_INVALID_ARGUMENT,
/**
* Le processus na pas pu être créé ou lancé.
*/
TOOL_PROCESS_ERROR_SPAWN,
/**
* La communication avec le processus a échoué.
*/
TOOL_PROCESS_ERROR_COMMUNICATION,
/**
* Lexécution a é annulée.
*/
TOOL_PROCESS_ERROR_CANCELLED,
/**
* Le processus sest terminé sans produire un résultat exploitable.
*/
TOOL_PROCESS_ERROR_INVALID_RESULT
} ToolProcessError;
/**
* @brief Domaine derreur de ToolProcess.
*/
#define TOOL_PROCESS_ERROR \
tool_process_error_quark()
/**
* @brief Résultat opaque dune exécution externe.
*/
typedef struct ToolProcessResult ToolProcessResult;
/**
* @brief Retourne le domaine derreur de ToolProcess.
*
* @return Quark GLib du domaine derreur.
*/
GQuark tool_process_error_quark(void);
/**
* @brief Exécute un outil externe sans passer par un shell.
*
* Le programme et chacun de ses arguments sont transmis séparément à
* GSubprocess.
*
* Un code de sortie différent de zéro ne constitue pas une erreur de cette
* fonction. Dans ce cas, la fonction retourne TRUE et le code de sortie est
* disponible dans ToolProcessResult.
*
* @param executable_path Chemin de lexécutable.
* @param arguments Arguments supplémentaires terminés par NULL, ou NULL.
* @param working_directory Dossier de travail, ou NULL.
* @param cancellable Objet dannulation facultatif.
* @param out_result Emplacement recevant le résultat.
* @param error Emplacement facultatif pour lerreur.
*
* @return TRUE lorsquun résultat exploitable a é obtenu, sinon FALSE.
*/
gboolean tool_process_run(
const char *executable_path,
const char *const arguments[],
const char *working_directory,
GCancellable *cancellable,
ToolProcessResult **out_result,
GError **error
);
/**
* @brief Libère un résultat dexécution.
*
* @param result Résultat à libérer, ou NULL.
*/
void tool_process_result_free(
ToolProcessResult *result
);
/**
* @brief Retourne une nouvelle référence sur la sortie standard.
*
* La référence retournée doit être libérée avec g_bytes_unref().
*
* @param result Résultat consulté.
*
* @return Nouvelle référence sur stdout, ou NULL.
*/
GBytes *tool_process_result_ref_stdout(
const ToolProcessResult *result
);
/**
* @brief Retourne une nouvelle référence sur la sortie derreur.
*
* La référence retournée doit être libérée avec g_bytes_unref().
*
* @param result Résultat consulté.
*
* @return Nouvelle référence sur stderr, ou NULL.
*/
GBytes *tool_process_result_ref_stderr(
const ToolProcessResult *result
);
/**
* @brief Indique si le processus sest terminé normalement.
*
* @param result Résultat consulté.
*
* @return TRUE si le processus a appelé exit() ou retourné depuis main().
*/
gboolean tool_process_result_exited_normally(
const ToolProcessResult *result
);
/**
* @brief Retourne le code de sortie du processus.
*
* @param result Résultat consulté.
*
* @return Code de sortie, ou -1 si le processus ne sest pas terminé
* normalement.
*/
int tool_process_result_get_exit_status(
const ToolProcessResult *result
);
/**
* @brief Indique si le processus a é terminé par un signal.
*
* @param result Résultat consulté.
*
* @return TRUE si le processus a é terminé par un signal.
*/
gboolean tool_process_result_was_signaled(
const ToolProcessResult *result
);
/**
* @brief Retourne le signal ayant terminé le processus.
*
* @param result Résultat consulté.
*
* @return Numéro du signal, ou zéro si aucun signal nest disponible.
*/
int tool_process_result_get_termination_signal(
const ToolProcessResult *result
);
/**
* @brief Indique si le programme sest terminé avec succès.
*
* Le succès fonctionnel signifie :
*
* - fin normale ;
* - code de sortie égal à zéro.
*
* @param result Résultat consulté.
*
* @return TRUE si le programme a retourné zéro.
*/
gboolean tool_process_result_is_success(
const ToolProcessResult *result
);
G_END_DECLS
#endif

Binary file not shown.

612
src/core/tool_process.c Normal file
View file

@ -0,0 +1,612 @@
/******************************************************************************
* @file tool_process.c
* @brief Exécution sécurisée des outils externes.
******************************************************************************/
#include "core/tool_process.h"
/**
* @struct ToolProcessResult
* @brief Résultat interne dune exécution externe.
*/
struct ToolProcessResult
{
GBytes *stdout_bytes;
GBytes *stderr_bytes;
gboolean exited_normally;
int exit_status;
gboolean was_signaled;
int termination_signal;
};
/**
* @brief Vérifie quune chaîne est définie et non vide.
*
* @param text Chaîne à vérifier.
*
* @return TRUE si la chaîne est valide.
*/
static gboolean tool_process_string_is_valid(
const char *text
)
{
return text != NULL &&
text[0] != '\0';
}
/**
* @brief Construit le vecteur argv transmis à GSubprocess.
*
* Les chaînes ne sont pas dupliquées. Le tableau les emprunte uniquement
* pendant la création du processus.
*
* @param executable_path Chemin de lexécutable.
* @param arguments Arguments supplémentaires, ou NULL.
*
* @return Nouveau GPtrArray terminé par NULL.
*/
static GPtrArray *tool_process_build_argv(
const char *executable_path,
const char *const arguments[]
)
{
GPtrArray *argument_vector = NULL;
gsize argument_index = 0;
argument_vector = g_ptr_array_new();
g_ptr_array_add(
argument_vector,
(gpointer) executable_path
);
if (arguments != NULL)
{
for (argument_index = 0;
arguments[argument_index] != NULL;
argument_index++)
{
g_ptr_array_add(
argument_vector,
(gpointer) arguments[argument_index]
);
}
}
/*
* argv doit obligatoirement se terminer par NULL.
*/
g_ptr_array_add(
argument_vector,
NULL
);
return argument_vector;
}
/**
* @brief Produit une erreur ToolProcess à partir dune erreur interne.
*
* @param error Emplacement facultatif de lerreur.
* @param error_code Code ToolProcess.
* @param context_message Description de lopération ayant échoué.
* @param cause Erreur interne facultative.
*/
static void tool_process_set_wrapped_error(
GError **error,
ToolProcessError error_code,
const char *context_message,
const GError *cause
)
{
const char *cause_message = NULL;
cause_message =
cause != NULL &&
cause->message != NULL
? cause->message
: "erreur inconnue";
g_set_error(
error,
TOOL_PROCESS_ERROR,
error_code,
"%s : %s",
context_message,
cause_message
);
}
/**
* @brief Force larrêt dun processus puis attend sa terminaison.
*
* Cette fonction est utilisée après une annulation ou une erreur de
* communication afin de ne pas abandonner le processus enfant.
*
* @param subprocess Processus à arrêter.
*/
static void tool_process_force_exit_and_wait(
GSubprocess *subprocess
)
{
GError *wait_error = NULL;
if (subprocess == NULL)
{
return;
}
g_subprocess_force_exit(
subprocess
);
/*
* Lattente ne doit pas utiliser le GCancellable déjà annulé.
*/
if (!g_subprocess_wait(
subprocess,
NULL,
&wait_error
))
{
g_clear_error(
&wait_error
);
}
}
GQuark tool_process_error_quark(void)
{
return g_quark_from_static_string(
"labfy-investigation-tool-process-error"
);
}
gboolean tool_process_run(
const char *executable_path,
const char *const arguments[],
const char *working_directory,
GCancellable *cancellable,
ToolProcessResult **out_result,
GError **error
)
{
GPtrArray *argument_vector = NULL;
GSubprocessLauncher *launcher = NULL;
GSubprocess *subprocess = NULL;
GBytes *stdout_bytes = NULL;
GBytes *stderr_bytes = NULL;
ToolProcessResult *result = NULL;
GError *local_error = NULL;
GSubprocessFlags subprocess_flags =
G_SUBPROCESS_FLAGS_STDOUT_PIPE |
G_SUBPROCESS_FLAGS_STDERR_PIPE;
gboolean communication_success = FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (!tool_process_string_is_valid(
executable_path
) ||
out_result == NULL ||
*out_result != NULL ||
(working_directory != NULL &&
working_directory[0] == '\0'))
{
g_set_error_literal(
error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_INVALID_ARGUMENT,
"Les arguments fournis à ToolProcess sont invalides."
);
return FALSE;
}
/*
* Ne pas lancer un processus si lopération est déjà annulée.
*/
if (cancellable != NULL &&
g_cancellable_is_cancelled(
cancellable
))
{
g_set_error_literal(
error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_CANCELLED,
"Lexécution de loutil a été annulée avant son lancement."
);
return FALSE;
}
argument_vector = tool_process_build_argv(
executable_path,
arguments
);
if (argument_vector == NULL)
{
g_set_error_literal(
error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_INVALID_ARGUMENT,
"Le vecteur darguments na pas pu être construit."
);
return FALSE;
}
launcher = g_subprocess_launcher_new(
subprocess_flags
);
if (launcher == NULL)
{
g_ptr_array_unref(
argument_vector
);
g_set_error_literal(
error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_SPAWN,
"Le lanceur de processus na pas pu être créé."
);
return FALSE;
}
if (working_directory != NULL)
{
g_subprocess_launcher_set_cwd(
launcher,
working_directory
);
}
/*
* Chaque argument est transmis séparément.
*
* Aucun shell ninterprète les espaces, étoiles, points-virgules
* ou substitutions présents dans les arguments.
*/
subprocess = g_subprocess_launcher_spawnv(
launcher,
(const char *const *) argument_vector->pdata,
&local_error
);
g_clear_object(
&launcher
);
g_ptr_array_unref(
argument_vector
);
argument_vector = NULL;
if (subprocess == NULL)
{
tool_process_set_wrapped_error(
error,
TOOL_PROCESS_ERROR_SPAWN,
"Impossible de lancer loutil externe",
local_error
);
g_clear_error(
&local_error
);
return FALSE;
}
communication_success = g_subprocess_communicate(
subprocess,
NULL,
cancellable,
&stdout_bytes,
&stderr_bytes,
&local_error
);
if (!communication_success)
{
gboolean was_cancelled = FALSE;
was_cancelled =
(local_error != NULL &&
g_error_matches(
local_error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED
)) ||
(cancellable != NULL &&
g_cancellable_is_cancelled(
cancellable
));
/*
* En cas derreur, GSubprocess ne garantit pas que les valeurs
* de sortie soient exploitables.
*/
g_clear_pointer(
&stdout_bytes,
g_bytes_unref
);
g_clear_pointer(
&stderr_bytes,
g_bytes_unref
);
tool_process_force_exit_and_wait(
subprocess
);
if (was_cancelled)
{
tool_process_set_wrapped_error(
error,
TOOL_PROCESS_ERROR_CANCELLED,
"Lexécution de loutil a été annulée",
local_error
);
}
else
{
tool_process_set_wrapped_error(
error,
TOOL_PROCESS_ERROR_COMMUNICATION,
"La communication avec loutil externe a échoué",
local_error
);
}
g_clear_error(
&local_error
);
g_clear_object(
&subprocess
);
return FALSE;
}
/*
* communicate() a réussi : le processus est terminé et son état
* peut maintenant être consulté.
*/
result = g_new0(
ToolProcessResult,
1
);
if (stdout_bytes != NULL)
{
result->stdout_bytes =
stdout_bytes;
stdout_bytes = NULL;
}
else
{
result->stdout_bytes =
g_bytes_new_static(
"",
0
);
}
if (stderr_bytes != NULL)
{
result->stderr_bytes =
stderr_bytes;
stderr_bytes = NULL;
}
else
{
result->stderr_bytes =
g_bytes_new_static(
"",
0
);
}
result->exited_normally =
g_subprocess_get_if_exited(
subprocess
);
result->was_signaled =
g_subprocess_get_if_signaled(
subprocess
);
result->exit_status = -1;
result->termination_signal = 0;
if (result->exited_normally)
{
result->exit_status =
g_subprocess_get_exit_status(
subprocess
);
}
if (result->was_signaled)
{
result->termination_signal =
g_subprocess_get_term_sig(
subprocess
);
}
/*
* Un processus terminé doit avoir soit quitté normalement,
* soit é terminé par un signal.
*/
if (!result->exited_normally &&
!result->was_signaled)
{
tool_process_result_free(
result
);
result = NULL;
g_clear_object(
&subprocess
);
g_set_error_literal(
error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_INVALID_RESULT,
"Le processus sest terminé dans un état non exploitable."
);
return FALSE;
}
g_clear_object(
&subprocess
);
*out_result = result;
return TRUE;
}
void tool_process_result_free(
ToolProcessResult *result
)
{
if (result == NULL)
{
return;
}
g_clear_pointer(
&result->stdout_bytes,
g_bytes_unref
);
g_clear_pointer(
&result->stderr_bytes,
g_bytes_unref
);
g_free(
result
);
}
GBytes *tool_process_result_ref_stdout(
const ToolProcessResult *result
)
{
if (result == NULL ||
result->stdout_bytes == NULL)
{
return NULL;
}
return g_bytes_ref(
result->stdout_bytes
);
}
GBytes *tool_process_result_ref_stderr(
const ToolProcessResult *result
)
{
if (result == NULL ||
result->stderr_bytes == NULL)
{
return NULL;
}
return g_bytes_ref(
result->stderr_bytes
);
}
gboolean tool_process_result_exited_normally(
const ToolProcessResult *result
)
{
if (result == NULL)
{
return FALSE;
}
return result->exited_normally;
}
int tool_process_result_get_exit_status(
const ToolProcessResult *result
)
{
if (result == NULL ||
!result->exited_normally)
{
return -1;
}
return result->exit_status;
}
gboolean tool_process_result_was_signaled(
const ToolProcessResult *result
)
{
if (result == NULL)
{
return FALSE;
}
return result->was_signaled;
}
int tool_process_result_get_termination_signal(
const ToolProcessResult *result
)
{
if (result == NULL ||
!result->was_signaled)
{
return 0;
}
return result->termination_signal;
}
gboolean tool_process_result_is_success(
const ToolProcessResult *result
)
{
if (result == NULL)
{
return FALSE;
}
return result->exited_normally &&
result->exit_status == 0;
}

BIN
tests/test_tool_process Executable file

Binary file not shown.

1397
tests/test_tool_process.c Normal file

File diff suppressed because it is too large Load diff