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

1129 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 :
```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 lexé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 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
```text
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 :
```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 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
```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 à lappelant.
Il doit être libéré avec :
```c
void tool_process_result_free(
ToolProcessResult *result
);
```
Les sorties doivent être conservées avec `GBytes`.
---
## Domaine derreur
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 lAPI.
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 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
```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 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 :
```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 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 :
```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 nest envoyé à lentrée standard.
La communication doit transmettre :
```c
stdin_buf = NULL
```
Une gestion explicite de stdin pourra être ajoutée plus tard.
---
## Construction du vecteur darguments
Limplé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 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
```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.
Lappelant doit la libérer avec :
```c
g_bytes_unref()
```
### Sortie derreur
```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 sest terminé normalement ;
- retourne `-1` si le résultat est `NULL` ou si le processus ne sest 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 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 :
```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 lannulation
Lorsquun `GCancellable` est annulé pendant :
```c
g_subprocess_communicate()
```
le module doit :
1. interrompre lattente ;
2. forcer larrêt du processus si nécessaire avec :
```c
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 :
```text
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é :
```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 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 :
```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`, 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 :
```sh
#!/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
```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 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
```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 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.