labfy-investigation/docs/tickets/closed/TICKET-037.md
2026-07-17 15:42:30 +02:00

20 KiB
Raw Blame History

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 dexécuter un outil externe de manière contrôlée avec GSubprocess, sans passer par un shell.

Le module devra :

  • recevoir le chemin dun exécutable ;
  • recevoir une liste darguments séparés ;
  • lancer directement le programme ;
  • capturer intégralement stdout et stderr ;
  • conserver le code de sortie ;
  • distinguer une fin normale dune terminaison par signal ;
  • permettre lannulation 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 dexécuter ces outils sans introduire de vulnérabilité liée à une commande shell.

Labfy Investigation devra progressivement lancer des outils tels que :

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 lexécutable et ses arguments sous forme de tableau.

Exemple conceptuel correct :

const char *arguments[] =
{
    "+short",
    "example.org",
    NULL
};

Exemple interdit :

char *command = g_strdup_printf(
    "dig +short %s",
    domain_name
);

system(command);

Les API suivantes sont interdites dans le code de production :

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 darguments.


Périmètre

Ce ticket doit fournir :

  1. un module autonome tool_process ;
  2. une structure opaque ToolProcessResult ;
  3. une fonction synchrone et annulable dexé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 lorsquil est disponible ;
  9. la gestion dun 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. linté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 dun autre outil ;
  • déterminer la version dun outil ;
  • ajouter une limite de durée automatique ;
  • gérer un pool de processus ;
  • gérer une file dattente ;
  • enregistrer les sorties dans SQLite ;
  • créer une preuve ou une observation OSINT ;
  • modifier lenvironnement complet du processus ;
  • afficher la progression détaillée dune commande ;
  • ajouter un pseudo-terminal ;
  • exécuter une commande à travers un shell.

Lintégration avec BackgroundTask sera effectuée après validation de cette abstraction.


Fichiers attendus

include/core/tool_process.h
src/core/tool_process.c
tests/test_tool_process.c

Le Makefile devra compiler le module dans lapplication principale et fournir une cible :

tests/test_tool_process

Contraintes générales

  • C17.
  • GLib et GIO.
  • Aucune dépendance GTK.
  • Compilation stricte :
-Wall -Wextra -Wpedantic -Werror
  • Structure publique opaque.
  • Fonctions préfixées par :
tool_process_
tool_process_result_
  • Aucun état global mutable.
  • Aucun shell.
  • Aucun chemin doutil codé en dur dans le module.
  • Les arguments doivent être transmis séparément.
  • Les sorties doivent être conservées sous forme doctets 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

typedef struct ToolProcessResult ToolProcessResult;

La structure doit rester opaque.

Elle doit contenir au minimum :

stdout_bytes
stderr_bytes
exited_normally
exit_status
was_signaled
termination_signal

Propriété

Le résultat retourné appartient à lappelant.

Il doit être libéré avec :

void tool_process_result_free(
    ToolProcessResult *result
);

Les sorties doivent être conservées avec GBytes.


Domaine derreur

Créer :

#define TOOL_PROCESS_ERROR \
    tool_process_error_quark()

Énumération minimale :

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 :

GQuark tool_process_error_quark(void);

Règle importante

Un programme qui se lance correctement puis retourne :

exit 1
exit 2
exit 127

ne constitue pas une erreur de lAPI.

Dans ce cas :

tool_process_run() retourne TRUE

et le code de sortie est conservé dans ToolProcessResult.

La fonction ne retourne FALSE que lorsque le module na pas pu mener lexécution jusquà lobtention dun résultat exploitable.


API publique attendue

LAPI exacte peut être ajustée si une meilleure conception est justifiée, mais elle doit couvrir les opérations suivantes.

Exécution

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 lexé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 :

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 :

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 lutilisation de ce dossier ;
  • une chaîne vide est invalide.

Utiliser GSubprocessLauncher lorsquun dossier de travail est fourni.

cancellable

Objet dannulation facultatif.

  • NULL signifie que lappel nest pas annulable ;
  • un objet déjà annulé doit provoquer un échec avec :
    • TOOL_PROCESS_ERROR_CANCELLED.

out_result

Doit être non NULL.

Avant lappel :

*out_result == NULL

En cas de succès :

*out_result != NULL

En cas déchec :

*out_result == NULL

error

Respecter la convention GLib :

error == NULL || *error == NULL

Utilisation de GSubprocess

Créer le processus avec les drapeaux :

G_SUBPROCESS_FLAGS_STDOUT_PIPE
G_SUBPROCESS_FLAGS_STDERR_PIPE

La capture recommandée est :

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 :

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 nest envoyé à lentrée standard.

La communication doit transmettre :

stdin_buf = NULL

Une gestion explicite de stdin pourra être ajoutée plus tard.


Construction du vecteur darguments

Limplémentation peut utiliser :

GPtrArray

Exemple conceptuel :

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 sil duplique les chaînes ou sil les emprunte uniquement pendant lappel.

Comme g_subprocess_launcher_spawnv() consomme le tableau pendant lappel 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 dexécution

Cycle de vie

void tool_process_result_free(
    ToolProcessResult *result
);

La fonction doit accepter NULL.

Sortie standard

GBytes *tool_process_result_ref_stdout(
    const ToolProcessResult *result
);

Retourne une nouvelle référence.

Lappelant doit la libérer avec :

g_bytes_unref()

Sortie derreur

GBytes *tool_process_result_ref_stderr(
    const ToolProcessResult *result
);

Retourne une nouvelle référence.

Fin normale

gboolean tool_process_result_exited_normally(
    const ToolProcessResult *result
);

Code de sortie

int tool_process_result_get_exit_status(
    const ToolProcessResult *result
);

Règles recommandées :

  • retourne le code réel si le programme sest terminé normalement ;
  • retourne -1 si le résultat est NULL ou si le processus ne sest pas terminé normalement.

Terminaison par signal

gboolean tool_process_result_was_signaled(
    const ToolProcessResult *result
);
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 :

gboolean tool_process_result_is_success(
    const ToolProcessResult *result
);

Elle retourne TRUE uniquement lorsque :

exited_normally == TRUE
exit_status == 0

Cette fonction ne doit pas confondre le succès de lAPI et le succès du programme lancé.


Gestion des sorties vides

Même lorsquun programme nécrit rien, le résultat doit rester exploitable.

Les choix acceptables sont :

GBytes vide

ou :

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 lannulation

Lorsquun GCancellable est annulé pendant :

g_subprocess_communicate()

le module doit :

  1. interrompre lattente ;
  2. forcer larrêt du processus si nécessaire avec :
    g_subprocess_force_exit()
    
  3. attendre ou finaliser proprement lobjet GSubprocess selon le comportement GLib ;
  4. ne retourner aucun résultat partiel ;
  5. retourner FALSE ;
  6. produire :
    TOOL_PROCESS_ERROR_CANCELLED
    

Le module ne doit pas laisser de processus enfant actif après lannulation.


Nettoyage obligatoire

Tous les chemins derreur doivent libérer :

  • le tableau darguments ;
  • le GSubprocessLauncher ;
  • le GSubprocess ;
  • les GBytes temporaires ;
  • le résultat partiellement construit ;
  • toute erreur intermédiaire.

Limplémentation doit utiliser lorsque cela améliore la lisibilité :

g_autoptr()
g_clear_object()
g_clear_pointer()

Le style doit rester cohérent avec le reste du projet.


Comportement attendu

Exécutable introuvable

retour : FALSE
résultat : NULL
erreur : TOOL_PROCESS_ERROR_SPAWN

Exécutable valide, code zéro

retour : TRUE
résultat : non NULL
is_success : TRUE
exit_status : 0

Exécutable valide, code non nul

retour : TRUE
résultat : non NULL
is_success : FALSE
exit_status : code réel

Annulation

retour : FALSE
résultat : NULL
erreur : TOOL_PROCESS_ERROR_CANCELLED

Sortie invalide UTF-8

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 :

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 :

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 :

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 quaucun fichier dinjection na été créé.

Ce test prouve quaucun shell ninterprète les arguments.


4. Capture séparée de stdout et stderr

Le faux outil écrit :

message-out

dans stdout, et :

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 :

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, lannuler avant lappel, puis exécuter le faux outil.

Vérifier :

  • retour FALSE ;
  • résultat NULL ;
  • erreur TOOL_PROCESS_ERROR_CANCELLED.

9. Annulation pendant lexécution

Créer un faux outil long, par exemple :

#!/bin/sh
sleep 10

Lancer lannulation depuis un second thread après un court délai.

Vérifier :

  • retour FALSE ;
  • erreur dannulation ;
  • aucun résultat ;
  • durée du test nettement inférieure à dix secondes ;
  • aucun processus enfant laissé actif.

Le test ne doit pas dépendre dune 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 nutilise 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

/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 :

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 :

TEST_TOOL_PROCESS := tests/test_tool_process

Règle attendue :

$(TEST_TOOL_PROCESS): \
	tests/test_tool_process.c \
	src/core/tool_process.c
	$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)

Ajouter la cible dans :

test

et dans :

clean

TEST_LDFLAGS contient déjà gio-2.0, nécessaire pour GSubprocess.


Vérifications manuelles

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 :

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 dannulation, contrôler si nécessaire quaucun faux outil ne reste actif après la fin du test.

Le module ne doit créer ni zombie ni processus abandonné.


Critères dacceptation

Le ticket est validé lorsque :

  • le module compile en C17 strict ;
  • il na aucune dépendance GTK ;
  • il utilise GSubprocess ou GSubprocessLauncher ;
  • il nutilise aucun shell ;
  • lexé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 ;
  • lannulation avant le lancement fonctionne ;
  • lannulation pendant lexécution arrête le processus ;
  • aucune sortie partielle nest 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 nest détectée ;
  • tous les tests passent ;
  • le test est intégré à make test et make clean.

Exemple dutilisation attendu

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 dexé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
);

Lexemple illustre le comportement attendu. Il ne constitue pas une obligation dorganisation interne.


Intégration future

Après validation de ce ticket :

  1. création dune 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 dune durée maximale ;
  8. ajout des dépendances facultatives aux installateurs Ubuntu.