diff --git a/Makefile b/Makefile index d797e70..6c48d4e 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ TEST_INVESTIGATION_SESSION := tests/test_investigation_session TEST_BACKGROUND_TASK := tests/test_background_task TEST_TASK_MANAGER := tests/test_task_manager TEST_TOOL_REGISTRY := tests/test_tool_registry +TEST_TOOL_PROCESS := tests/test_tool_process all: $(TARGET) @@ -156,6 +157,12 @@ $(TEST_TOOL_REGISTRY): \ src/core/tool_registry.c $(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_NODE) \ $(TEST_TREE_MODEL) \ @@ -170,7 +177,8 @@ test: \ $(TEST_INVESTIGATION_SESSION) \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ - $(TEST_TOOL_REGISTRY) + $(TEST_TOOL_REGISTRY) \ + $(TEST_TOOL_PROCESS) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -186,6 +194,7 @@ test: \ @$(TEST_BACKGROUND_TASK) @$(TEST_TASK_MANAGER) @$(TEST_TOOL_REGISTRY) + @$(TEST_TOOL_PROCESS) @echo "Tous les tests sont valides." %.o: %.c @@ -208,7 +217,8 @@ clean: $(TEST_INVESTIGATION_DAO) \ $(TEST_INVESTIGATION_SESSION) \ $(TEST_BACKGROUND_TASK) \ - $(TEST_TASK_MANAGER) \ - $(TEST_TOOL_REGISTRY) + $(TEST_TASK_MANAGER) \ + $(TEST_TOOL_REGISTRY) \ + $(TEST_TOOL_PROCESS) .PHONY: clean run test diff --git a/docs/tickets/closed/TICKET-037.md b/docs/tickets/closed/TICKET-037.md new file mode 100644 index 0000000..8557a19 --- /dev/null +++ b/docs/tickets/closed/TICKET-037.md @@ -0,0 +1,1129 @@ +# TICKET-037 — Exécution sécurisée des outils externes avec GSubprocess + +## Statut + +À faire + +## Priorité + +Haute + +## Objectif + +Créer une abstraction C centralisée permettant à Labfy Investigation d’exécuter un outil externe de manière contrôlée avec `GSubprocess`, sans passer par un shell. + +Le module devra : + +- recevoir le chemin d’un exécutable ; +- recevoir une liste d’arguments séparés ; +- lancer directement le programme ; +- capturer intégralement `stdout` et `stderr` ; +- conserver le code de sortie ; +- distinguer une fin normale d’une terminaison par signal ; +- permettre l’annulation avec `GCancellable` ; +- restituer un résultat structuré ; +- rester indépendant de GTK ; +- pouvoir être utilisé dans un worker `BackgroundTask`. + +--- + +## Contexte + +Le ticket #036 fournit désormais un registre capable de détecter les outils présents sur la machine et de mémoriser leur chemin. + +Le prochain besoin est d’exécuter ces outils sans introduire de vulnérabilité liée à une commande shell. + +Labfy Investigation devra progressivement lancer des outils tels que : + +```text +dig +curl +openssl +whois +exiftool +``` + +Les arguments pourront contenir : + +- des noms de domaines ; +- des chemins de fichiers ; +- des URL ; +- des options ; +- des espaces ; +- des caractères normalement interprétés par un shell. + +Ces valeurs ne doivent jamais être concaténées dans une commande textuelle. + +--- + +## Principe de sécurité + +Le module doit lancer l’exécutable et ses arguments sous forme de tableau. + +Exemple conceptuel correct : + +```c +const char *arguments[] = +{ + "+short", + "example.org", + NULL +}; +``` + +Exemple interdit : + +```c +char *command = g_strdup_printf( + "dig +short %s", + domain_name +); + +system(command); +``` + +Les API suivantes sont interdites dans le code de production : + +```text +system() +popen() +execl("/bin/sh", ...) +sh -c +bash -c +g_spawn_command_line_sync() +g_spawn_command_line_async() +``` + +Le module doit utiliser `GSubprocess` ou `GSubprocessLauncher` avec un vecteur d’arguments. + +--- + +## Périmètre + +Ce ticket doit fournir : + +1. un module autonome `tool_process` ; +2. une structure opaque `ToolProcessResult` ; +3. une fonction synchrone et annulable d’exécution ; +4. la capture brute de `stdout` ; +5. la capture brute de `stderr` ; +6. le code de sortie ; +7. l’état de fin normale ou par signal ; +8. la conservation du signal de terminaison lorsqu’il est disponible ; +9. la gestion d’un dossier de travail facultatif ; +10. une distinction claire entre : + - une erreur de lancement ; + - une erreur de communication ; + - une annulation ; + - un programme ayant simplement retourné un code différent de zéro ; +11. des tests unitaires avec de faux outils temporaires ; +12. l’intégration au `Makefile`. + +--- + +## Hors périmètre + +Ce ticket ne doit pas encore : + +- créer une interface GTK ; +- lancer automatiquement les outils du registre ; +- analyser la sortie de `dig`, `curl`, `openssl` ou d’un autre outil ; +- déterminer la version d’un outil ; +- ajouter une limite de durée automatique ; +- gérer un pool de processus ; +- gérer une file d’attente ; +- enregistrer les sorties dans SQLite ; +- créer une preuve ou une observation OSINT ; +- modifier l’environnement complet du processus ; +- afficher la progression détaillée d’une commande ; +- ajouter un pseudo-terminal ; +- exécuter une commande à travers un shell. + +L’intégration avec `BackgroundTask` sera effectuée après validation de cette abstraction. + +--- + +## Fichiers attendus + +```text +include/core/tool_process.h +src/core/tool_process.c +tests/test_tool_process.c +``` + +Le `Makefile` devra compiler le module dans l’application principale et fournir une cible : + +```text +tests/test_tool_process +``` + +--- + +## Contraintes générales + +- C17. +- GLib et GIO. +- Aucune dépendance GTK. +- Compilation stricte : + +```text +-Wall -Wextra -Wpedantic -Werror +``` + +- Structure publique opaque. +- Fonctions préfixées par : + +```text +tool_process_ +tool_process_result_ +``` + +- Aucun état global mutable. +- Aucun shell. +- Aucun chemin d’outil codé en dur dans le module. +- Les arguments doivent être transmis séparément. +- Les sorties doivent être conservées sous forme d’octets bruts. +- Les données reçues doivent être copiées ou référencées avec une propriété clairement documentée. + +--- + +## Modèle public + +### ToolProcessResult + +```c +typedef struct ToolProcessResult ToolProcessResult; +``` + +La structure doit rester opaque. + +Elle doit contenir au minimum : + +```text +stdout_bytes +stderr_bytes +exited_normally +exit_status +was_signaled +termination_signal +``` + +### Propriété + +Le résultat retourné appartient à l’appelant. + +Il doit être libéré avec : + +```c +void tool_process_result_free( + ToolProcessResult *result +); +``` + +Les sorties doivent être conservées avec `GBytes`. + +--- + +## Domaine d’erreur + +Créer : + +```c +#define TOOL_PROCESS_ERROR \ + tool_process_error_quark() +``` + +Énumération minimale : + +```c +typedef enum +{ + TOOL_PROCESS_ERROR_INVALID_ARGUMENT, + TOOL_PROCESS_ERROR_SPAWN, + TOOL_PROCESS_ERROR_COMMUNICATION, + TOOL_PROCESS_ERROR_CANCELLED, + TOOL_PROCESS_ERROR_INVALID_RESULT +} ToolProcessError; +``` + +Fonction : + +```c +GQuark tool_process_error_quark(void); +``` + +### Règle importante + +Un programme qui se lance correctement puis retourne : + +```text +exit 1 +exit 2 +exit 127 +``` + +ne constitue pas une erreur de l’API. + +Dans ce cas : + +```text +tool_process_run() retourne TRUE +``` + +et le code de sortie est conservé dans `ToolProcessResult`. + +La fonction ne retourne `FALSE` que lorsque le module n’a pas pu mener l’exécution jusqu’à l’obtention d’un résultat exploitable. + +--- + +## API publique attendue + +L’API exacte peut être ajustée si une meilleure conception est justifiée, mais elle doit couvrir les opérations suivantes. + +### Exécution + +```c +gboolean tool_process_run( + const char *executable_path, + const char *const arguments[], + const char *working_directory, + GCancellable *cancellable, + ToolProcessResult **out_result, + GError **error +); +``` + +### Paramètres + +#### executable_path + +Chemin de l’exécutable. + +Le module doit accepter un chemin absolu détecté par `ToolRegistry`. + +Le chemin doit être : + +- non `NULL` ; +- non vide. + +Le module ne doit pas reconstruire une commande textuelle. + +#### arguments + +Tableau terminé par `NULL`. + +Ce tableau contient uniquement les arguments placés après `argv[0]`. + +Exemple : + +```c +const char *arguments[] = +{ + "+short", + "example.org", + NULL +}; +``` + +`arguments` peut être `NULL`, ce qui signifie aucun argument supplémentaire. + +Le module doit construire en interne un vecteur de ce type : + +```text +argv[0] = executable_path +argv[1] = arguments[0] +argv[2] = arguments[1] +... +argv[n] = NULL +``` + +Chaque argument doit rester un élément distinct. + +#### working_directory + +Dossier de travail facultatif. + +- `NULL` signifie utiliser le dossier courant hérité ; +- une chaîne non vide demande l’utilisation de ce dossier ; +- une chaîne vide est invalide. + +Utiliser `GSubprocessLauncher` lorsqu’un dossier de travail est fourni. + +#### cancellable + +Objet d’annulation facultatif. + +- `NULL` signifie que l’appel n’est pas annulable ; +- un objet déjà annulé doit provoquer un échec avec : + - `TOOL_PROCESS_ERROR_CANCELLED`. + +#### out_result + +Doit être non `NULL`. + +Avant l’appel : + +```c +*out_result == NULL +``` + +En cas de succès : + +```c +*out_result != NULL +``` + +En cas d’échec : + +```c +*out_result == NULL +``` + +#### error + +Respecter la convention GLib : + +```c +error == NULL || *error == NULL +``` + +--- + +## Utilisation de GSubprocess + +Créer le processus avec les drapeaux : + +```c +G_SUBPROCESS_FLAGS_STDOUT_PIPE +G_SUBPROCESS_FLAGS_STDERR_PIPE +``` + +La capture recommandée est : + +```c +g_subprocess_communicate() +``` + +Cette API permet de récupérer des `GBytes` et évite de supposer que les sorties sont valides en UTF-8. + +Le module ne doit pas utiliser exclusivement : + +```c +g_subprocess_communicate_utf8() +``` + +car une sortie brute peut contenir des octets non UTF-8. + +--- + +## Données envoyées à stdin + +Dans ce ticket, aucun contenu n’est envoyé à l’entrée standard. + +La communication doit transmettre : + +```c +stdin_buf = NULL +``` + +Une gestion explicite de stdin pourra être ajoutée plus tard. + +--- + +## Construction du vecteur d’arguments + +L’implémentation peut utiliser : + +```c +GPtrArray +``` + +Exemple conceptuel : + +```c +GPtrArray *argv_builder = NULL; + +argv_builder = g_ptr_array_new(); + +g_ptr_array_add( + argv_builder, + (gpointer) executable_path +); + +/* Ajout de chaque argument séparément. */ + +g_ptr_array_add( + argv_builder, + NULL +); +``` + +Le module doit décider clairement s’il duplique les chaînes ou s’il les emprunte uniquement pendant l’appel. + +Comme `g_subprocess_launcher_spawnv()` consomme le tableau pendant l’appel de création, les chaînes peuvent être empruntées si leur durée de vie couvre cet appel. + +Aucune fonction ne doit concaténer les arguments. + +--- + +## Résultat d’exécution + +### Cycle de vie + +```c +void tool_process_result_free( + ToolProcessResult *result +); +``` + +La fonction doit accepter `NULL`. + +### Sortie standard + +```c +GBytes *tool_process_result_ref_stdout( + const ToolProcessResult *result +); +``` + +Retourne une nouvelle référence. + +L’appelant doit la libérer avec : + +```c +g_bytes_unref() +``` + +### Sortie d’erreur + +```c +GBytes *tool_process_result_ref_stderr( + const ToolProcessResult *result +); +``` + +Retourne une nouvelle référence. + +### Fin normale + +```c +gboolean tool_process_result_exited_normally( + const ToolProcessResult *result +); +``` + +### Code de sortie + +```c +int tool_process_result_get_exit_status( + const ToolProcessResult *result +); +``` + +Règles recommandées : + +- retourne le code réel si le programme s’est terminé normalement ; +- retourne `-1` si le résultat est `NULL` ou si le processus ne s’est pas terminé normalement. + +### Terminaison par signal + +```c +gboolean tool_process_result_was_signaled( + const ToolProcessResult *result +); +``` + +```c +int tool_process_result_get_termination_signal( + const ToolProcessResult *result +); +``` + +Règles recommandées : + +- retourne le signal réel si disponible ; +- retourne `0` dans les autres cas. + +### Succès fonctionnel + +Ajouter une fonction pratique : + +```c +gboolean tool_process_result_is_success( + const ToolProcessResult *result +); +``` + +Elle retourne `TRUE` uniquement lorsque : + +```text +exited_normally == TRUE +exit_status == 0 +``` + +Cette fonction ne doit pas confondre le succès de l’API et le succès du programme lancé. + +--- + +## Gestion des sorties vides + +Même lorsqu’un programme n’écrit rien, le résultat doit rester exploitable. + +Les choix acceptables sont : + +```text +GBytes vide +``` + +ou : + +```text +NULL documenté comme sortie vide +``` + +La solution recommandée est de conserver un `GBytes` vide afin de simplifier les appelants. + +Les deux flux doivent être indépendants. + +--- + +## Gestion de l’annulation + +Lorsqu’un `GCancellable` est annulé pendant : + +```c +g_subprocess_communicate() +``` + +le module doit : + +1. interrompre l’attente ; +2. forcer l’arrêt du processus si nécessaire avec : + ```c + g_subprocess_force_exit() + ``` +3. attendre ou finaliser proprement l’objet `GSubprocess` selon le comportement GLib ; +4. ne retourner aucun résultat partiel ; +5. retourner `FALSE` ; +6. produire : + ```text + TOOL_PROCESS_ERROR_CANCELLED + ``` + +Le module ne doit pas laisser de processus enfant actif après l’annulation. + +--- + +## Nettoyage obligatoire + +Tous les chemins d’erreur doivent libérer : + +- le tableau d’arguments ; +- le `GSubprocessLauncher` ; +- le `GSubprocess` ; +- les `GBytes` temporaires ; +- le résultat partiellement construit ; +- toute erreur intermédiaire. + +L’implémentation doit utiliser lorsque cela améliore la lisibilité : + +```c +g_autoptr() +g_clear_object() +g_clear_pointer() +``` + +Le style doit rester cohérent avec le reste du projet. + +--- + +## Comportement attendu + +### Exécutable introuvable + +```text +retour : FALSE +résultat : NULL +erreur : TOOL_PROCESS_ERROR_SPAWN +``` + +### Exécutable valide, code zéro + +```text +retour : TRUE +résultat : non NULL +is_success : TRUE +exit_status : 0 +``` + +### Exécutable valide, code non nul + +```text +retour : TRUE +résultat : non NULL +is_success : FALSE +exit_status : code réel +``` + +### Annulation + +```text +retour : FALSE +résultat : NULL +erreur : TOOL_PROCESS_ERROR_CANCELLED +``` + +### Sortie invalide UTF-8 + +```text +retour : TRUE +stdout brut conservé dans GBytes +``` + +--- + +## Tests unitaires obligatoires + +Les tests doivent créer leurs propres faux exécutables temporaires. + +Ils ne doivent pas dépendre de : + +```text +dig +curl +openssl +whois +``` + +Ils peuvent utiliser des scripts temporaires avec un shebang, car le projet cible Linux. + +Le code du module ne doit cependant jamais lancer un shell explicitement. + +--- + +### 1. Arguments invalides + +Tester au minimum : + +- `executable_path == NULL` ; +- chemin vide ; +- `out_result == NULL` ; +- `*out_result != NULL` ; +- dossier de travail vide ; +- `GError` déjà initialisé si cette convention est vérifiée par le projet. + +Résultat attendu : + +```text +TOOL_PROCESS_ERROR_INVALID_ARGUMENT +``` + +--- + +### 2. Exécution sans argument + +Créer un faux outil qui affiche un texte fixe. + +Vérifier : + +- retour `TRUE` ; +- fin normale ; +- code zéro ; +- sortie capturée ; +- `stderr` vide ; +- `is_success == TRUE`. + +--- + +### 3. Transmission exacte des arguments + +Le faux outil doit afficher chaque argument reçu sur une ligne séparée. + +Tester des arguments comme : + +```text +example.org +nom avec espaces +$(touch /tmp/labfy-injection) +; echo danger +* +``` + +Vérifier que les valeurs sont restituées littéralement. + +Le test doit également vérifier qu’aucun fichier d’injection n’a été créé. + +Ce test prouve qu’aucun shell n’interprète les arguments. + +--- + +### 4. Capture séparée de stdout et stderr + +Le faux outil écrit : + +```text +message-out +``` + +dans `stdout`, et : + +```text +message-err +``` + +dans `stderr`. + +Vérifier que les deux sorties ne sont pas mélangées. + +--- + +### 5. Code de sortie non nul + +Le faux outil doit : + +```text +exit 7 +``` + +Vérifier : + +- `tool_process_run() == TRUE` ; +- fin normale ; +- code de sortie `7` ; +- `is_success == FALSE`. + +--- + +### 6. Exécutable inexistant + +Utiliser un chemin temporaire inexistant. + +Vérifier : + +- retour `FALSE` ; +- résultat `NULL` ; +- erreur `TOOL_PROCESS_ERROR_SPAWN`. + +--- + +### 7. Dossier de travail + +Créer un dossier temporaire et un outil affichant son répertoire courant. + +Exécuter avec `working_directory`. + +Vérifier que la sortie correspond au chemin demandé. + +--- + +### 8. Annulation avant le lancement + +Créer un `GCancellable`, l’annuler avant l’appel, puis exécuter le faux outil. + +Vérifier : + +- retour `FALSE` ; +- résultat `NULL` ; +- erreur `TOOL_PROCESS_ERROR_CANCELLED`. + +--- + +### 9. Annulation pendant l’exécution + +Créer un faux outil long, par exemple : + +```sh +#!/bin/sh +sleep 10 +``` + +Lancer l’annulation depuis un second thread après un court délai. + +Vérifier : + +- retour `FALSE` ; +- erreur d’annulation ; +- aucun résultat ; +- durée du test nettement inférieure à dix secondes ; +- aucun processus enfant laissé actif. + +Le test ne doit pas dépendre d’une boucle principale GTK. + +--- + +### 10. Sortie vide + +Créer un outil qui ne produit aucune sortie. + +Vérifier que les accesseurs de sortie restent sûrs. + +--- + +### 11. Octets non UTF-8 + +Créer un outil qui écrit au moins un octet non UTF-8 dans `stdout`. + +Vérifier : + +- retour `TRUE` ; +- récupération des octets avec `GBytes` ; +- taille exacte ; +- contenu exact. + +Ce test confirme que le module n’utilise pas une API limitée au texte UTF-8. + +--- + +### 12. Terminaison par signal + +Sur Linux, créer un faux outil qui se termine lui-même par un signal. + +Vérifier lorsque la plateforme le permet : + +- `exited_normally == FALSE` ; +- `was_signaled == TRUE` ; +- signal non nul ; +- `exit_status == -1` ; +- `is_success == FALSE`. + +Le test peut être conditionné avec les macros de plateforme appropriées. + +--- + +## Noms de tests suggérés + +```text +/tool_process/invalid_arguments +/tool_process/no_arguments +/tool_process/literal_arguments +/tool_process/stdout_stderr +/tool_process/nonzero_exit +/tool_process/missing_executable +/tool_process/working_directory +/tool_process/cancelled_before_start +/tool_process/cancelled_during_run +/tool_process/empty_output +/tool_process/non_utf8_output +/tool_process/signaled +``` + +--- + +## Fixtures de test + +Une fixture peut contenir : + +```c +typedef struct +{ + char *temporary_directory; + char *tool_path; +} ToolProcessFixture; +``` + +Le `setup` doit : + +- créer un dossier temporaire ; +- préparer les chemins. + +Le `teardown` doit : + +- supprimer tous les scripts ; +- supprimer les fichiers produits par les tests ; +- supprimer le dossier ; +- libérer toutes les chaînes. + +Chaque test doit rester isolé. + +--- + +## Makefile + +Ajouter : + +```make +TEST_TOOL_PROCESS := tests/test_tool_process +``` + +Règle attendue : + +```make +$(TEST_TOOL_PROCESS): \ + tests/test_tool_process.c \ + src/core/tool_process.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +``` + +Ajouter la cible dans : + +```make +test +``` + +et dans : + +```make +clean +``` + +`TEST_LDFLAGS` contient déjà `gio-2.0`, nécessaire pour `GSubprocess`. + +--- + +## Vérifications manuelles + +```bash +make clean +make +make tests/test_tool_process +./tests/test_tool_process +make test +``` + +Aucun avertissement de compilation ne doit être accepté. + +--- + +## Vérification mémoire + +Exécuter : + +```bash +G_DEBUG=gc-friendly \ +G_SLICE=always-malloc \ +valgrind \ + --leak-check=full \ + --show-leak-kinds=all \ + ./tests/test_tool_process +``` + +Vérifier particulièrement les chemins : + +- lancement impossible ; +- code de sortie non nul ; +- annulation ; +- sortie vide ; +- erreur de communication. + +--- + +## Vérification des processus enfants + +Pendant le test d’annulation, contrôler si nécessaire qu’aucun faux outil ne reste actif après la fin du test. + +Le module ne doit créer ni zombie ni processus abandonné. + +--- + +## Critères d’acceptation + +Le ticket est validé lorsque : + +- le module compile en C17 strict ; +- il n’a aucune dépendance GTK ; +- il utilise `GSubprocess` ou `GSubprocessLauncher` ; +- il n’utilise aucun shell ; +- l’exécutable et les arguments sont transmis séparément ; +- `stdout` et `stderr` sont capturés séparément ; +- les sorties sont conservées dans des `GBytes` ; +- un code de sortie non nul produit tout de même un résultat ; +- l’échec de lancement produit une erreur ; +- le dossier de travail facultatif fonctionne ; +- l’annulation avant le lancement fonctionne ; +- l’annulation pendant l’exécution arrête le processus ; +- aucune sortie partielle n’est retournée après annulation ; +- les arguments contenant des caractères de shell restent littéraux ; +- les octets non UTF-8 sont conservés ; +- la terminaison par signal est représentée ; +- aucun processus enfant ne reste actif ; +- aucune fuite mémoire liée au module n’est détectée ; +- tous les tests passent ; +- le test est intégré à `make test` et `make clean`. + +--- + +## Exemple d’utilisation attendu + +```c +ToolProcessResult *result = NULL; +GBytes *stdout_bytes = NULL; +GError *error = NULL; + +const char *arguments[] = +{ + "+short", + "example.org", + NULL +}; + +if (!tool_process_run( + "/usr/bin/dig", + arguments, + NULL, + NULL, + &result, + &error + )) +{ + g_warning( + "Impossible d’exécuter dig : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + g_clear_error( + &error + ); + + return; +} + +stdout_bytes = tool_process_result_ref_stdout( + result +); + +if (!tool_process_result_is_success( + result + )) +{ + g_warning( + "dig a retourné le code %d", + tool_process_result_get_exit_status( + result + ) + ); +} + +g_bytes_unref( + stdout_bytes +); + +tool_process_result_free( + result +); +``` + +L’exemple illustre le comportement attendu. Il ne constitue pas une obligation d’organisation interne. + +--- + +## Intégration future + +Après validation de ce ticket : + +1. création d’une tâche `BackgroundTask` exécutant `ToolProcess` ; +2. interrogation de la version des outils enregistrés ; +3. premier adaptateur OSINT, probablement DNS ; +4. conservation de la commande structurée : + - exécutable ; + - arguments ; + - date ; + - code de sortie ; + - sortie brute ; + - erreur brute ; +5. enregistrement de la provenance ; +6. affichage des résultats dans le panneau de travail ; +7. gestion ultérieure d’une durée maximale ; +8. ajout des dépendances facultatives aux installateurs Ubuntu. + diff --git a/include/core/tool_process.h b/include/core/tool_process.h new file mode 100644 index 0000000..fcea3dd --- /dev/null +++ b/include/core/tool_process.h @@ -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 +#include + +G_BEGIN_DECLS + +/** + * @brief Erreurs produites pendant l’exécution d’un outil externe. + */ +typedef enum +{ + /** + * Un argument transmis à la fonction est invalide. + */ + TOOL_PROCESS_ERROR_INVALID_ARGUMENT, + + /** + * Le processus n’a pas pu être créé ou lancé. + */ + TOOL_PROCESS_ERROR_SPAWN, + + /** + * La communication avec le processus a échoué. + */ + TOOL_PROCESS_ERROR_COMMUNICATION, + + /** + * L’exécution a été annulée. + */ + TOOL_PROCESS_ERROR_CANCELLED, + + /** + * Le processus s’est terminé sans produire un résultat exploitable. + */ + TOOL_PROCESS_ERROR_INVALID_RESULT +} ToolProcessError; + +/** + * @brief Domaine d’erreur de ToolProcess. + */ +#define TOOL_PROCESS_ERROR \ + tool_process_error_quark() + +/** + * @brief Résultat opaque d’une exécution externe. + */ +typedef struct ToolProcessResult ToolProcessResult; + +/** + * @brief Retourne le domaine d’erreur de ToolProcess. + * + * @return Quark GLib du domaine d’erreur. + */ +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 l’exécutable. + * @param arguments Arguments supplémentaires terminés par NULL, ou NULL. + * @param working_directory Dossier de travail, ou NULL. + * @param cancellable Objet d’annulation facultatif. + * @param out_result Emplacement recevant le résultat. + * @param error Emplacement facultatif pour l’erreur. + * + * @return TRUE lorsqu’un résultat exploitable a été 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 d’exé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 d’erreur. + * + * 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 s’est 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 s’est pas terminé + * normalement. + */ +int tool_process_result_get_exit_status( + const ToolProcessResult *result +); + +/** + * @brief Indique si le processus a été terminé par un signal. + * + * @param result Résultat consulté. + * + * @return TRUE si le processus a été 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 n’est disponible. + */ +int tool_process_result_get_termination_signal( + const ToolProcessResult *result +); + +/** + * @brief Indique si le programme s’est 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 diff --git a/labfy-investigation b/labfy-investigation index ddbf124..a960d38 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/tool_process.c b/src/core/tool_process.c new file mode 100644 index 0000000..bdee5a3 --- /dev/null +++ b/src/core/tool_process.c @@ -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 d’une 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 qu’une 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 l’exé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 d’une erreur interne. + * + * @param error Emplacement facultatif de l’erreur. + * @param error_code Code ToolProcess. + * @param context_message Description de l’opé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 l’arrêt d’un 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 + ); + + /* + * L’attente 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 l’opé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, + "L’exécution de l’outil 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 d’arguments n’a 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 n’a 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 n’interprè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 l’outil 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 d’erreur, 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, + "L’exécution de l’outil a été annulée", + local_error + ); + } + else + { + tool_process_set_wrapped_error( + error, + TOOL_PROCESS_ERROR_COMMUNICATION, + "La communication avec l’outil 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 été 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 s’est 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; +} diff --git a/tests/test_tool_process b/tests/test_tool_process new file mode 100755 index 0000000..fdb4d27 Binary files /dev/null and b/tests/test_tool_process differ diff --git a/tests/test_tool_process.c b/tests/test_tool_process.c new file mode 100644 index 0000000..8fb7168 --- /dev/null +++ b/tests/test_tool_process.c @@ -0,0 +1,1397 @@ +/****************************************************************************** + * @file test_tool_process.c + * @brief Tests unitaires de l’exécution sécurisée des outils externes. + ******************************************************************************/ + +#include "core/tool_process.h" + +#include +#include + +#include +#include + +#include + +/** + * @struct ToolProcessFixture + * @brief Environnement temporaire utilisé par les tests. + */ +typedef struct +{ + char *temporary_directory; + char *tool_path; + char *injection_path; +} ToolProcessFixture; + +/** + * @struct ToolProcessCancellationData + * @brief Données transmises au thread chargé d'annuler une exécution. + */ +typedef struct +{ + GCancellable *cancellable; + guint64 delay_microseconds; +} ToolProcessCancellationData; + +/** + * @brief Annule une opération après un court délai. + * + * @param user_data Pointeur vers ToolProcessCancellationData. + * + * @return Toujours NULL. + */ +static gpointer test_tool_process_cancel_after_delay( + gpointer user_data +) +{ + ToolProcessCancellationData *cancellation_data = + user_data; + + if (cancellation_data == NULL || + cancellation_data->cancellable == NULL) + { + return NULL; + } + + g_usleep( + cancellation_data->delay_microseconds + ); + + g_cancellable_cancel( + cancellation_data->cancellable + ); + + return NULL; +} + +/** + * @brief Crée un script exécutable utilisé comme faux outil. + * + * @param fixture Fixture du test. + * @param script_content Contenu complet du script. + */ +static void test_tool_process_write_script( + ToolProcessFixture *fixture, + const char *script_content +) +{ + GError *error = NULL; + gboolean write_success = FALSE; + int chmod_result = 0; + + g_assert_nonnull( + fixture + ); + + g_assert_nonnull( + fixture->tool_path + ); + + g_assert_nonnull( + script_content + ); + + write_success = g_file_set_contents( + fixture->tool_path, + script_content, + -1, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + write_success + ); + + chmod_result = g_chmod( + fixture->tool_path, + S_IRUSR | + S_IWUSR | + S_IXUSR + ); + + g_assert_cmpint( + chmod_result, + ==, + 0 + ); +} + +/** + * @brief Vérifie qu’un GBytes contient exactement le texte attendu. + * + * La fonction ne suppose pas que les données sont terminées par '\0'. + * + * @param bytes Données à vérifier. + * @param expected_text Texte attendu. + */ +static void test_tool_process_assert_bytes_equal_text( + GBytes *bytes, + const char *expected_text +) +{ + gconstpointer bytes_data = NULL; + gsize bytes_size = 0; + gsize expected_size = 0; + + g_assert_nonnull( + bytes + ); + + g_assert_nonnull( + expected_text + ); + + bytes_data = g_bytes_get_data( + bytes, + &bytes_size + ); + + expected_size = strlen( + expected_text + ); + + g_assert_cmpuint( + bytes_size, + ==, + expected_size + ); + + g_assert_cmpmem( + bytes_data, + bytes_size, + expected_text, + expected_size + ); +} + +/** + * @brief Prépare le répertoire temporaire d’un test. + * + * @param fixture Fixture à initialiser. + * @param user_data Données inutilisées. + */ +static void test_tool_process_fixture_setup( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + GError *error = NULL; + + (void) user_data; + + fixture->temporary_directory = + g_dir_make_tmp( + "labfy-tool-process-XXXXXX", + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + fixture->temporary_directory + ); + + fixture->tool_path = g_build_filename( + fixture->temporary_directory, + "fake_tool", + NULL + ); + + fixture->injection_path = g_build_filename( + fixture->temporary_directory, + "injection_created", + NULL + ); +} + +/** + * @brief Supprime les fichiers et le dossier temporaire. + * + * @param fixture Fixture à nettoyer. + * @param user_data Données inutilisées. + */ +static void test_tool_process_fixture_teardown( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + (void) user_data; + + if (fixture->tool_path != NULL) + { + g_remove( + fixture->tool_path + ); + } + + if (fixture->injection_path != NULL) + { + g_remove( + fixture->injection_path + ); + } + + if (fixture->temporary_directory != NULL) + { + g_rmdir( + fixture->temporary_directory + ); + } + + g_clear_pointer( + &fixture->tool_path, + g_free + ); + + g_clear_pointer( + &fixture->injection_path, + g_free + ); + + g_clear_pointer( + &fixture->temporary_directory, + g_free + ); +} + +/** + * @brief Vérifie le refus des arguments invalides. + */ +static void test_tool_process_invalid_arguments( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + ToolProcessResult *occupied_result = NULL; + + GError *error = NULL; + + (void) user_data; + + g_assert_false( + tool_process_run( + NULL, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_INVALID_ARGUMENT + ); + + g_assert_null( + result + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_process_run( + "", + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + NULL, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + /* + * out_result doit pointer vers NULL avant l’appel. + * + * Ce pointeur sert uniquement de marqueur et n’est jamais déréférencé. + */ + occupied_result = + (ToolProcessResult *) fixture; + + g_assert_false( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &occupied_result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_assert_false( + tool_process_run( + fixture->tool_path, + NULL, + "", + NULL, + &result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie l’exécution d’un outil sans argument. + */ +static void test_tool_process_no_arguments( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "printf 'outil-execute'\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + result + ); + + g_assert_true( + tool_process_result_exited_normally( + result + ) + ); + + g_assert_false( + tool_process_result_was_signaled( + result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + result + ), + ==, + 0 + ); + + g_assert_cmpint( + tool_process_result_get_termination_signal( + result + ), + ==, + 0 + ); + + g_assert_true( + tool_process_result_is_success( + result + ) + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + stderr_bytes = + tool_process_result_ref_stderr( + result + ); + + test_tool_process_assert_bytes_equal_text( + stdout_bytes, + "outil-execute" + ); + + test_tool_process_assert_bytes_equal_text( + stderr_bytes, + "" + ); + + g_bytes_unref( + stdout_bytes + ); + + g_bytes_unref( + stderr_bytes + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie que les arguments ne sont pas interprétés par un shell. + */ +static void test_tool_process_literal_arguments( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GBytes *stdout_bytes = NULL; + + char *injection_argument = NULL; + char *expected_output = NULL; + + const char *arguments[6]; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "for argument in \"$@\"\n" + "do\n" + " printf '%s\\n' \"$argument\"\n" + "done\n" + ); + + injection_argument = g_strdup_printf( + "$(touch %s)", + fixture->injection_path + ); + + arguments[0] = "example.org"; + arguments[1] = "nom avec espaces"; + arguments[2] = injection_argument; + arguments[3] = "; echo danger"; + arguments[4] = "*"; + arguments[5] = NULL; + + expected_output = g_strdup_printf( + "example.org\n" + "nom avec espaces\n" + "%s\n" + "; echo danger\n" + "*\n", + injection_argument + ); + + g_assert_false( + g_file_test( + fixture->injection_path, + G_FILE_TEST_EXISTS + ) + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + arguments, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_true( + tool_process_result_is_success( + result + ) + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + test_tool_process_assert_bytes_equal_text( + stdout_bytes, + expected_output + ); + + /* + * Si un shell avait interprété l’argument, ce fichier existerait. + */ + g_assert_false( + g_file_test( + fixture->injection_path, + G_FILE_TEST_EXISTS + ) + ); + + g_bytes_unref( + stdout_bytes + ); + + tool_process_result_free( + result + ); + + g_free( + expected_output + ); + + g_free( + injection_argument + ); +} + +/** + * @brief Vérifie la séparation de stdout et stderr. + */ +static void test_tool_process_stdout_stderr( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "printf 'message-out'\n" + "printf 'message-err' >&2\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + stderr_bytes = + tool_process_result_ref_stderr( + result + ); + + test_tool_process_assert_bytes_equal_text( + stdout_bytes, + "message-out" + ); + + test_tool_process_assert_bytes_equal_text( + stderr_bytes, + "message-err" + ); + + g_bytes_unref( + stdout_bytes + ); + + g_bytes_unref( + stderr_bytes + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie qu’un code non nul produit tout de même un résultat. + */ +static void test_tool_process_nonzero_exit( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "printf 'echec-fonctionnel' >&2\n" + "exit 7\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + result + ); + + g_assert_true( + tool_process_result_exited_normally( + result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + result + ), + ==, + 7 + ); + + g_assert_false( + tool_process_result_is_success( + result + ) + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie l’erreur produite par un exécutable inexistant. + */ +static void test_tool_process_missing_executable( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + char *missing_path = NULL; + GError *error = NULL; + + (void) user_data; + + missing_path = g_build_filename( + fixture->temporary_directory, + "tool_that_does_not_exist", + NULL + ); + + g_assert_false( + tool_process_run( + missing_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_SPAWN + ); + + g_assert_null( + result + ); + + g_clear_error( + &error + ); + + g_free( + missing_path + ); +} + +/** + * @brief Vérifie l'utilisation du dossier de travail demandé. + */ +static void test_tool_process_working_directory( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GBytes *stdout_bytes = NULL; + + char *canonical_directory = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "printf '%s' \"$PWD\"\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + fixture->temporary_directory, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + result + ); + + canonical_directory = g_canonicalize_filename( + fixture->temporary_directory, + NULL + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + test_tool_process_assert_bytes_equal_text( + stdout_bytes, + canonical_directory + ); + + g_bytes_unref( + stdout_bytes + ); + + g_free( + canonical_directory + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie une annulation demandée avant le lancement. + */ +static void test_tool_process_cancelled_before_start( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GCancellable *cancellable = NULL; + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "exit 0\n" + ); + + cancellable = g_cancellable_new(); + + g_cancellable_cancel( + cancellable + ); + + g_assert_false( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + cancellable, + &result, + &error + ) + ); + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_CANCELLED + ); + + g_assert_null( + result + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); +} + +/** + * @brief Vérifie l'annulation d'un processus en cours. + */ +static void test_tool_process_cancelled_during_run( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + + GCancellable *cancellable = NULL; + GThread *cancellation_thread = NULL; + + ToolProcessCancellationData cancellation_data; + + gint64 start_time = 0; + gint64 end_time = 0; + gint64 elapsed_microseconds = 0; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "while :\n" + "do\n" + " :\n" + "done\n" + ); + + cancellable = g_cancellable_new(); + + cancellation_data.cancellable = + cancellable; + + cancellation_data.delay_microseconds = + 200000; + + cancellation_thread = g_thread_new( + "tool-process-canceller", + test_tool_process_cancel_after_delay, + &cancellation_data + ); + + g_assert_nonnull( + cancellation_thread + ); + + start_time = g_get_monotonic_time(); + + g_assert_false( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + cancellable, + &result, + &error + ) + ); + + end_time = g_get_monotonic_time(); + + g_thread_join( + cancellation_thread + ); + + elapsed_microseconds = + end_time - start_time; + + g_assert_error( + error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_CANCELLED + ); + + g_assert_null( + result + ); + + /* + * L'exécution doit être interrompue bien avant plusieurs secondes. + */ + g_assert_cmpint( + elapsed_microseconds, + <, + 3000000 + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); +} + +/** + * @brief Vérifie qu'une exécution sans sortie produit des GBytes vides. + */ +static void test_tool_process_empty_output( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "exit 0\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + stderr_bytes = + tool_process_result_ref_stderr( + result + ); + + g_assert_nonnull( + stdout_bytes + ); + + g_assert_nonnull( + stderr_bytes + ); + + g_assert_cmpuint( + g_bytes_get_size( + stdout_bytes + ), + ==, + 0 + ); + + g_assert_cmpuint( + g_bytes_get_size( + stderr_bytes + ), + ==, + 0 + ); + + g_bytes_unref( + stdout_bytes + ); + + g_bytes_unref( + stderr_bytes + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie la conservation d'une sortie non UTF-8. + */ +static void test_tool_process_non_utf8_output( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GBytes *stdout_bytes = NULL; + + gconstpointer stdout_data = NULL; + const guint8 *stdout_octets = NULL; + + gsize stdout_size = 0; + + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "printf '\\377'\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + stdout_bytes = + tool_process_result_ref_stdout( + result + ); + + g_assert_nonnull( + stdout_bytes + ); + + stdout_data = g_bytes_get_data( + stdout_bytes, + &stdout_size + ); + + stdout_octets = stdout_data; + + g_assert_cmpuint( + stdout_size, + ==, + 1 + ); + + g_assert_nonnull( + stdout_octets + ); + + g_assert_cmpuint( + stdout_octets[0], + ==, + 0xFF + ); + + g_bytes_unref( + stdout_bytes + ); + + tool_process_result_free( + result + ); +} + +/** + * @brief Vérifie la représentation d'une terminaison par signal. + */ +static void test_tool_process_signaled( + ToolProcessFixture *fixture, + gconstpointer user_data +) +{ + ToolProcessResult *result = NULL; + GError *error = NULL; + + (void) user_data; + + test_tool_process_write_script( + fixture, + "#!/bin/sh\n" + "kill -TERM $$\n" + ); + + g_assert_true( + tool_process_run( + fixture->tool_path, + NULL, + NULL, + NULL, + &result, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + result + ); + + g_assert_false( + tool_process_result_exited_normally( + result + ) + ); + + g_assert_true( + tool_process_result_was_signaled( + result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + result + ), + ==, + -1 + ); + + g_assert_cmpint( + tool_process_result_get_termination_signal( + result + ), + ==, + SIGTERM + ); + + g_assert_false( + tool_process_result_is_success( + result + ) + ); + + tool_process_result_free( + result + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + g_test_add( + "/tool_process/invalid_arguments", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_invalid_arguments, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/no_arguments", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_no_arguments, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/literal_arguments", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_literal_arguments, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/stdout_stderr", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_stdout_stderr, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/nonzero_exit", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_nonzero_exit, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/missing_executable", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_missing_executable, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/working_directory", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_working_directory, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/cancelled_before_start", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_cancelled_before_start, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/cancelled_during_run", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_cancelled_during_run, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/empty_output", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_empty_output, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/non_utf8_output", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_non_utf8_output, + test_tool_process_fixture_teardown + ); + + g_test_add( + "/tool_process/signaled", + ToolProcessFixture, + NULL, + test_tool_process_fixture_setup, + test_tool_process_signaled, + test_tool_process_fixture_teardown + ); + + return g_test_run(); +}