feat: add external tool catalog and version detection

This commit is contained in:
grayTerminal-sh 2026-07-17 22:51:22 +02:00
parent 2ffbdec888
commit f68c4f88d1
8 changed files with 3674 additions and 2 deletions

View file

@ -45,6 +45,7 @@ TEST_TASK_MANAGER := tests/test_task_manager
TEST_TOOL_REGISTRY := tests/test_tool_registry
TEST_TOOL_PROCESS := tests/test_tool_process
TEST_TOOL_TASK := tests/test_tool_task
TEST_TOOL_CATALOG := tests/test_tool_catalog
all: $(TARGET)
@ -171,6 +172,13 @@ $(TEST_TOOL_TASK): \
src/core/background_task.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_TOOL_CATALOG): \
tests/test_tool_catalog.c \
src/core/tool_catalog.c \
src/core/tool_registry.c \
src/core/tool_process.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
test: \
$(TEST_NODE) \
$(TEST_TREE_MODEL) \
@ -187,7 +195,8 @@ test: \
$(TEST_TASK_MANAGER) \
$(TEST_TOOL_REGISTRY) \
$(TEST_TOOL_PROCESS) \
$(TEST_TOOL_TASK)
$(TEST_TOOL_TASK) \
$(TEST_TOOL_CATALOG)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -205,6 +214,7 @@ test: \
@$(TEST_TOOL_REGISTRY)
@$(TEST_TOOL_PROCESS)
@$(TEST_TOOL_TASK)
@$(TEST_TOOL_CATALOG)
@echo "Tous les tests sont valides."
%.o: %.c
@ -230,6 +240,7 @@ clean:
$(TEST_TASK_MANAGER) \
$(TEST_TOOL_REGISTRY) \
$(TEST_TOOL_PROCESS) \
$(TEST_TOOL_TASK)
$(TEST_TOOL_TASK) \
$(TEST_TOOL_CATALOG)
.PHONY: clean run test

View file

@ -0,0 +1,515 @@
# TICKET-039 — Catalogue initial des outils externes et détection de leurs versions
## Statut
À faire
## Priorité
Haute
## Objectif
Créer le catalogue initial des outils externes utilisés par Labfy Investigation et fournir une détection fiable de leur version.
Ce ticket complète les modules déjà validés :
```text
ToolRegistry
ToolProcess
ToolTask
```
Le catalogue décrit les outils connus par lapplication. Le registre décrit leur état sur la machine courante.
```text
ToolCatalog
description statique des outils connus
ToolRegistry
disponibilité, chemin résolu, version détectée
ToolProcess
exécution sécurisée de la commande de version
```
## Contexte
`ToolRegistry` permet déjà denregistrer un outil, de rechercher son exécutable dans le `PATH`, de conserver son chemin résolu et sa version détectée, et de distinguer les états `UNKNOWN`, `AVAILABLE` et `MISSING`.
Il manque une source centrale définissant :
- les outils reconnus par lapplication ;
- leur identifiant interne ;
- leur nom affiché ;
- leur exécutable ;
- leur importance ;
- les arguments permettant de demander leur version ;
- la manière de normaliser la sortie obtenue.
Sans catalogue central, chaque fonctionnalité finirait par enregistrer elle-même ses dépendances.
## Modules attendus
```text
include/core/tool_catalog.h
src/core/tool_catalog.c
tests/test_tool_catalog.c
```
Le ticket ne doit pas modifier lAPI publique de `ToolRegistry`, sauf nécessité démontrée pendant limplémentation.
## Responsabilités de ToolCatalog
Le module doit :
1. conserver une liste statique et immuable des outils connus ;
2. garantir lunicité de leurs identifiants ;
3. exposer leurs informations descriptives ;
4. enregistrer lensemble du catalogue dans un `ToolRegistry` ;
5. définir les arguments de détection de version ;
6. exécuter ces arguments avec `ToolProcess` ;
7. normaliser la sortie de version ;
8. retourner la version détectée sans modifier directement le registre ;
9. gérer lannulation ;
10. rester indépendant de GTK.
Le module ne doit pas installer de dépendance, modifier le `PATH`, utiliser un shell, analyser un résultat OSINT ni modifier linterface graphique.
## Catalogue initial
| Identifiant interne | Nom affiché | Exécutable | Importance | Arguments de version |
|---|---|---|---|---|
| `dns.dig` | `dig` | `dig` | optionnel | `-v` |
| `dns.host` | `host` | `host` | optionnel | `-V` |
| `network.whois` | `whois` | `whois` | optionnel | `--version` |
| `http.curl` | `curl` | `curl` | optionnel | `--version` |
| `tls.openssl` | `OpenSSL` | `openssl` | optionnel | `version` |
Tous ces outils sont optionnels au niveau de lapplication entière.
Ne pas ajouter encore `nslookup`, `traceroute`, `jq`, `file`, `exiftool`, `nmap`, `subfinder`, `amass`, les outils Python ou les outils installés depuis GitHub.
## Structure ToolCatalogEntry
Créer une structure opaque :
```c
typedef struct ToolCatalogEntry ToolCatalogEntry;
```
Elle conserve au minimum :
```text
identifier
display_name
executable_name
requirement
version_arguments
```
Les données du catalogue sont statiques et immuables. Elles ne doivent jamais être libérées par lappelant.
## Domaine derreur
```c
#define TOOL_CATALOG_ERROR \
tool_catalog_error_quark()
```
```c
typedef enum
{
TOOL_CATALOG_ERROR_INVALID_ARGUMENT,
TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND,
TOOL_CATALOG_ERROR_REGISTRATION,
TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED,
TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED,
TOOL_CATALOG_ERROR_TOOL_MISSING,
TOOL_CATALOG_ERROR_INVALID_TOOL_STATE,
TOOL_CATALOG_ERROR_PROCESS,
TOOL_CATALOG_ERROR_VERSION_COMMAND,
TOOL_CATALOG_ERROR_VERSION_OUTPUT,
TOOL_CATALOG_ERROR_CANCELLED
} ToolCatalogError;
```
```c
GQuark tool_catalog_error_quark(void);
```
## API publique proposée
### Consultation
```c
gsize tool_catalog_get_count(void);
```
```c
const ToolCatalogEntry *tool_catalog_get_entry(
gsize index
);
```
```c
const ToolCatalogEntry *tool_catalog_find(
const char *identifier
);
```
### Accesseurs
```c
const char *tool_catalog_entry_get_identifier(
const ToolCatalogEntry *entry
);
```
```c
const char *tool_catalog_entry_get_display_name(
const ToolCatalogEntry *entry
);
```
```c
const char *tool_catalog_entry_get_executable_name(
const ToolCatalogEntry *entry
);
```
```c
ToolRequirement tool_catalog_entry_get_requirement(
const ToolCatalogEntry *entry
);
```
```c
gsize tool_catalog_entry_get_version_argument_count(
const ToolCatalogEntry *entry
);
```
```c
const char *tool_catalog_entry_get_version_argument(
const ToolCatalogEntry *entry,
gsize index
);
```
### Enregistrement
```c
gboolean tool_catalog_register_defaults(
ToolRegistry *tool_registry,
GError **error
);
```
### Détection dune version
```c
gboolean tool_catalog_detect_version(
const ToolRegistry *tool_registry,
const char *identifier,
GCancellable *cancellable,
char **out_version,
GError **error
);
```
`out_version` reçoit une chaîne nouvellement allouée, à libérer avec `g_free()`.
La fonction ne doit pas appeler `tool_registry_set_version()`.
## Règles denregistrement
`tool_catalog_register_defaults()` doit vérifier avant toute insertion quaucun identifiant du catalogue nexiste déjà dans le registre.
En cas de doublon :
- aucune entrée ne doit être ajoutée ;
- la fonction retourne `FALSE` ;
- lerreur vaut `TOOL_CATALOG_ERROR_REGISTRATION`.
Cette prévalidation évite une insertion partielle, car `ToolRegistry` ne fournit pas de suppression.
## Validation de la détection
La fonction doit refuser :
- un registre `NULL` ;
- un identifiant `NULL` ou vide ;
- un `out_version` égal à `NULL` ;
- un `out_version` pointant déjà vers une chaîne ;
- un `GError` déjà initialisé.
Au début dun appel valide :
```c
*out_version = NULL;
```
Cas derreur :
```text
entrée absente du catalogue → TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND
outil absent du registre → TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED
état UNKNOWN → TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED
état MISSING → TOOL_CATALOG_ERROR_TOOL_MISSING
AVAILABLE sans chemin résolu → TOOL_CATALOG_ERROR_INVALID_TOOL_STATE
```
## Exécution de la commande de version
Utiliser exclusivement :
```c
tool_process_run()
```
avec :
```text
executable_path = chemin résolu du ToolRegistry
arguments = arguments statiques du ToolCatalogEntry
working_directory = NULL
cancellable = paramètre reçu
```
Interdictions :
- `/bin/sh -c` ;
- `system()` ;
- `popen()` ;
- concaténation dune ligne de commande ;
- redirections interprétées ;
- ajout ou transformation des arguments.
## Annulation
Si `ToolProcess` retourne `TOOL_PROCESS_ERROR_CANCELLED`, la fonction doit produire :
```text
G_IO_ERROR
G_IO_ERROR_CANCELLED
```
Aucune version partielle ne doit être retournée.
## Code de sortie
Pour une commande de version, un code de sortie non nul est un échec :
```text
TOOL_CATALOG_ERROR_VERSION_COMMAND
```
Une terminaison par signal produit la même catégorie derreur.
La sortie ne doit pas être interprétée lorsque le processus na pas terminé normalement avec le code zéro.
## Sélection de la sortie
Règle générique :
1. prendre la première ligne non vide de stdout ;
2. si stdout ne contient rien dexploitable, essayer stderr ;
3. si aucun flux ne contient de texte exploitable, échouer.
Cette règle évite une logique spéciale par outil pendant ce premier ticket.
## Normalisation
Le catalogue ne doit pas extraire seulement un numéro sémantique.
La version conservée est la première ligne descriptive exploitable, par exemple :
```text
DiG 9.20.8
curl 8.14.1 (x86_64-pc-linux-gnu)
OpenSSL 3.5.1 1 Jul 2025
```
Algorithme :
1. obtenir les octets du flux ;
2. inspecter au maximum 4096 octets par flux ;
3. convertir les octets invalides avec `g_utf8_make_valid()` ;
4. séparer les lignes ;
5. ignorer les lignes vides ;
6. choisir la première ligne non vide ;
7. retirer espaces et tabulations en début et fin ;
8. retirer `\r` et `\n` ;
9. refuser un résultat vide ;
10. retourner une nouvelle chaîne.
## Propriété des données
Les entrées et chaînes du catalogue sont empruntées et statiques.
La chaîne placée dans `*out_version` appartient à lappelant et doit être libérée avec `g_free()`.
`ToolCatalog` ne devient jamais propriétaire du registre.
## Contraintes de thread
Le catalogue statique est immuable.
`tool_catalog_detect_version()` :
- ne modifie pas le catalogue ;
- ne modifie pas le registre ;
- copie le chemin nécessaire avant lexécution ;
- ne conserve aucun pointeur vers `ToolInfo` après la préparation.
Le stockage avec `tool_registry_set_version()` reste la responsabilité du propriétaire du registre, idéalement dans le thread principal.
## Tests unitaires obligatoires
Les tests utilisent uniquement de faux exécutables temporaires.
Ils ne doivent pas dépendre des outils réellement installés.
### Tests du catalogue
1. nombre exact dentrées ;
2. contenu exact des cinq entrées ;
3. index invalide ;
4. recherche par identifiant ;
5. identifiant inconnu ;
6. unicité des identifiants ;
7. arguments de version ;
8. enregistrement complet dans un registre vide ;
9. état initial `UNKNOWN` ;
10. prévalidation dun doublon sans insertion partielle.
### Tests de détection
11. arguments invalides ;
12. entrée inconnue ;
13. outil non enregistré ;
14. outil non vérifié ;
15. outil absent ;
16. version sur stdout ;
17. version sur stderr ;
18. première ligne non vide ;
19. normalisation des espaces et fins de ligne ;
20. sortie non UTF-8 ;
21. sortie vide ;
22. code de sortie non nul ;
23. terminaison par signal ;
24. annulation ;
25. indépendance du registre ;
26. plusieurs détections successives.
## Noms de tests suggérés
```text
/tool_catalog/entries
/tool_catalog/invalid_index
/tool_catalog/find
/tool_catalog/unique_entries
/tool_catalog/register_defaults
/tool_catalog/register_duplicate
/tool_catalog/detect_invalid_arguments
/tool_catalog/detect_unknown_entry
/tool_catalog/detect_unregistered_tool
/tool_catalog/detect_unchecked_tool
/tool_catalog/detect_missing_tool
/tool_catalog/version_stdout
/tool_catalog/version_stderr
/tool_catalog/version_first_nonempty_line
/tool_catalog/version_normalization
/tool_catalog/version_non_utf8
/tool_catalog/version_empty
/tool_catalog/version_nonzero_exit
/tool_catalog/version_signaled
/tool_catalog/version_cancelled
/tool_catalog/version_registry_independence
/tool_catalog/version_successive_runs
```
## Makefile
```make
TEST_TOOL_CATALOG := tests/test_tool_catalog
```
```make
$(TEST_TOOL_CATALOG): \
tests/test_tool_catalog.c \
src/core/tool_catalog.c \
src/core/tool_registry.c \
src/core/tool_process.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
```
Ajouter le binaire à `make test` et `make clean`.
## Vérifications
```bash
make clean
make
make tests/test_tool_catalog
./tests/test_tool_catalog
make test
git diff --check
```
## Vérification mémoire
```bash
G_DEBUG=gc-friendly \
G_SLICE=always-malloc \
valgrind \
--leak-check=full \
--show-leak-kinds=all \
./tests/test_tool_catalog
```
## Critères dacceptation
Le ticket est validé lorsque :
- le catalogue contient exactement les cinq outils prévus ;
- toutes les entrées sont immuables ;
- tous les identifiants sont uniques ;
- le catalogue senregistre dans un registre vide ;
- un doublon empêche toute insertion partielle ;
- la détection utilise `ToolProcess` ;
- aucun shell nest utilisé ;
- stdout et stderr sont pris en charge ;
- la première ligne non vide est normalisée ;
- une sortie invalide devient un UTF-8 valide ;
- linspection est limitée à 4096 octets par flux ;
- un code non nul ou un signal est refusé ;
- lannulation produit `G_IO_ERROR_CANCELLED` ;
- la fonction ne modifie pas directement le registre ;
- les versions successives sont indépendantes ;
- les tests ne dépendent daucun outil installé ;
- tous les tests passent ;
- aucune fuite mémoire nest détectée ;
- le test est intégré au `Makefile`.
## Démonstration finale
Après validation :
1. créer un `ToolRegistry` ;
2. appeler `tool_catalog_register_defaults()` ;
3. appeler `tool_registry_refresh()` ;
4. afficher les outils présents et absents ;
5. détecter la version dun outil disponible ;
6. stocker explicitement la version avec `tool_registry_set_version()`.
Cette démonstration restera hors de linterface GTK principale.
## Suite prévue
```text
#040 — Initialisation des dépendances et analyse asynchrone au démarrage
```
Ce ticket devra créer le registre global de lapplication, enregistrer le catalogue, rafraîchir les disponibilités hors du thread GTK, détecter les versions et publier une tâche dans `TaskManager`.

257
include/core/tool_catalog.h Normal file
View file

@ -0,0 +1,257 @@
/******************************************************************************
* @file tool_catalog.h
* @brief Catalogue statique des outils externes connus.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_TOOL_CATALOG_H
#define LABFY_INVESTIGATION_TOOL_CATALOG_H
#include "core/tool_registry.h"
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief Erreurs produites par ToolCatalog.
*/
typedef enum
{
/**
* Un argument transmis au module est invalide.
*/
TOOL_CATALOG_ERROR_INVALID_ARGUMENT,
/**
* L'identifiant demandé n'existe pas dans le catalogue.
*/
TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND,
/**
* Le catalogue n'a pas pu être enregistré dans le registre.
*/
TOOL_CATALOG_ERROR_REGISTRATION,
/**
* L'outil existe dans le catalogue, mais pas dans le registre.
*/
TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED,
/**
* La disponibilité de l'outil n'a pas encore é vérifiée.
*/
TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED,
/**
* L'outil a é vérifié, mais son exécutable est absent.
*/
TOOL_CATALOG_ERROR_TOOL_MISSING,
/**
* L'outil est disponible, mais son état interne est incohérent.
*/
TOOL_CATALOG_ERROR_INVALID_TOOL_STATE,
/**
* ToolProcess n'a pas pu exécuter ou interroger l'outil.
*/
TOOL_CATALOG_ERROR_PROCESS,
/**
* La commande de version s'est terminée avec un code non nul
* ou à la suite d'un signal.
*/
TOOL_CATALOG_ERROR_VERSION_COMMAND,
/**
* La commande n'a produit aucune version exploitable.
*/
TOOL_CATALOG_ERROR_VERSION_OUTPUT
} ToolCatalogError;
/**
* @brief Domaine d'erreur du catalogue d'outils.
*/
#define TOOL_CATALOG_ERROR \
tool_catalog_error_quark()
/**
* @brief Entrée opaque et immuable du catalogue.
*
* Les entrées sont conservées dans une table statique interne.
* Elles ne doivent jamais être libérées par l'appelant.
*/
typedef struct ToolCatalogEntry ToolCatalogEntry;
/**
* @brief Retourne le domaine d'erreur du catalogue.
*
* @return Quark GLib du domaine d'erreur.
*/
GQuark tool_catalog_error_quark(void);
/**
* @brief Retourne le nombre d'outils connus du catalogue.
*
* @return Nombre d'entrées statiques.
*/
gsize tool_catalog_get_count(void);
/**
* @brief Retourne une entrée à partir de son index.
*
* Le pointeur retourné est emprunté et statique.
*
* @param index Index de l'entrée.
*
* @return Entrée correspondante, ou NULL si l'index est invalide.
*/
const ToolCatalogEntry *tool_catalog_get_entry(
gsize index
);
/**
* @brief Recherche une entrée par son identifiant interne.
*
* Le pointeur retourné est emprunté et statique.
*
* @param identifier Identifiant recherché.
*
* @return Entrée correspondante, ou NULL si elle n'existe pas.
*/
const ToolCatalogEntry *tool_catalog_find(
const char *identifier
);
/**
* @brief Retourne l'identifiant interne d'une entrée.
*
* La chaîne retournée est empruntée et statique.
*
* @param entry Entrée consultée.
*
* @return Identifiant de l'outil, ou NULL.
*/
const char *tool_catalog_entry_get_identifier(
const ToolCatalogEntry *entry
);
/**
* @brief Retourne le nom affiché d'une entrée.
*
* La chaîne retournée est empruntée et statique.
*
* @param entry Entrée consultée.
*
* @return Nom affiché, ou NULL.
*/
const char *tool_catalog_entry_get_display_name(
const ToolCatalogEntry *entry
);
/**
* @brief Retourne le nom de l'exécutable recherché dans le PATH.
*
* La chaîne retournée est empruntée et statique.
*
* @param entry Entrée consultée.
*
* @return Nom de l'exécutable, ou NULL.
*/
const char *tool_catalog_entry_get_executable_name(
const ToolCatalogEntry *entry
);
/**
* @brief Retourne l'importance de la dépendance.
*
* @param entry Entrée consultée.
*
* @return Importance de l'outil.
*/
ToolRequirement tool_catalog_entry_get_requirement(
const ToolCatalogEntry *entry
);
/**
* @brief Retourne le nombre d'arguments de la commande de version.
*
* Le chemin de l'exécutable n'est pas compté.
*
* @param entry Entrée consultée.
*
* @return Nombre d'arguments.
*/
gsize tool_catalog_entry_get_version_argument_count(
const ToolCatalogEntry *entry
);
/**
* @brief Retourne un argument de la commande de version.
*
* La chaîne retournée est empruntée et statique.
*
* @param entry Entrée consultée.
* @param index Index de l'argument.
*
* @return Argument correspondant, ou NULL si l'index est invalide.
*/
const char *tool_catalog_entry_get_version_argument(
const ToolCatalogEntry *entry,
gsize index
);
/**
* @brief Enregistre toutes les entrées du catalogue dans un registre.
*
* Avant toute insertion, la fonction vérifie qu'aucun identifiant du
* catalogue n'existe déjà dans le registre.
*
* En cas de doublon, aucune entrée n'est ajoutée.
*
* @param tool_registry Registre cible.
* @param error Emplacement facultatif pour l'erreur.
*
* @return TRUE si toutes les entrées ont é enregistrées.
*/
gboolean tool_catalog_register_defaults(
ToolRegistry *tool_registry,
GError **error
);
/**
* @brief Détecte la version d'un outil disponible.
*
* La fonction :
*
* - vérifie que l'outil existe dans le catalogue et le registre ;
* - vérifie que son exécutable est disponible ;
* - exécute sa commande de version avec ToolProcess ;
* - sélectionne la première ligne non vide de stdout ou stderr ;
* - retourne une chaîne UTF-8 normalisée.
*
* La fonction ne modifie pas la version conservée dans ToolRegistry.
*
* En cas d'annulation, l'erreur retournée appartient au domaine
* G_IO_ERROR avec le code G_IO_ERROR_CANCELLED.
*
* @param tool_registry Registre contenant l'outil détecté.
* @param identifier Identifiant de l'entrée du catalogue.
* @param cancellable Objet d'annulation facultatif.
* @param out_version Emplacement recevant une nouvelle chaîne.
* @param error Emplacement facultatif pour l'erreur.
*
* @return TRUE si une version exploitable a é détectée.
*/
gboolean tool_catalog_detect_version(
const ToolRegistry *tool_registry,
const char *identifier,
GCancellable *cancellable,
char **out_version,
GError **error
);
G_END_DECLS
#endif

Binary file not shown.

872
src/core/tool_catalog.c Normal file
View file

@ -0,0 +1,872 @@
/******************************************************************************
* @file tool_catalog.c
* @brief Catalogue statique des outils externes connus.
******************************************************************************/
#include "core/tool_catalog.h"
#include "core/tool_process.h"
#define TOOL_CATALOG_VERSION_OUTPUT_LIMIT 4096
/**
* @struct ToolCatalogEntry
* @brief Description statique d'un outil externe connu.
*/
struct ToolCatalogEntry
{
const char *identifier;
const char *display_name;
const char *executable_name;
ToolRequirement requirement;
const char *const *version_arguments;
gsize version_argument_count;
};
/*
* Arguments de détection des versions.
*
* Chaque tableau est terminé par NULL afin de pouvoir être transmis
* directement à ToolProcess.
*/
static const char *const tool_catalog_dig_version_arguments[] =
{
"-v",
NULL
};
static const char *const tool_catalog_host_version_arguments[] =
{
"-V",
NULL
};
static const char *const tool_catalog_whois_version_arguments[] =
{
"--version",
NULL
};
static const char *const tool_catalog_curl_version_arguments[] =
{
"--version",
NULL
};
static const char *const tool_catalog_openssl_version_arguments[] =
{
"version",
NULL
};
/**
* @brief Catalogue statique initial.
*/
static const ToolCatalogEntry tool_catalog_entries[] =
{
{
.identifier = "dns.dig",
.display_name = "dig",
.executable_name = "dig",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments =
tool_catalog_dig_version_arguments,
.version_argument_count = 1
},
{
.identifier = "dns.host",
.display_name = "host",
.executable_name = "host",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments =
tool_catalog_host_version_arguments,
.version_argument_count = 1
},
{
.identifier = "network.whois",
.display_name = "whois",
.executable_name = "whois",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments =
tool_catalog_whois_version_arguments,
.version_argument_count = 1
},
{
.identifier = "http.curl",
.display_name = "curl",
.executable_name = "curl",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments =
tool_catalog_curl_version_arguments,
.version_argument_count = 1
},
{
.identifier = "tls.openssl",
.display_name = "OpenSSL",
.executable_name = "openssl",
.requirement = TOOL_REQUIREMENT_OPTIONAL,
.version_arguments =
tool_catalog_openssl_version_arguments,
.version_argument_count = 1
}
};
/**
* @brief Vérifie qu'une chaîne est définie et non vide.
*/
static gboolean tool_catalog_string_is_valid(
const char *text
)
{
return text != NULL &&
text[0] != '\0';
}
/**
* @brief Encapsule une erreur provenant d'un autre module.
*/
static void tool_catalog_set_wrapped_error(
GError **error,
ToolCatalogError error_code,
const char *context_message,
const GError *cause
)
{
if (cause == NULL ||
cause->message == NULL)
{
g_set_error_literal(
error,
TOOL_CATALOG_ERROR,
error_code,
context_message
);
return;
}
g_set_error(
error,
TOOL_CATALOG_ERROR,
error_code,
"%s : %s",
context_message,
cause->message
);
}
/**
* @brief Extrait la première ligne non vide d'une sortie.
*
* Au maximum TOOL_CATALOG_VERSION_OUTPUT_LIMIT octets sont inspectés.
* Les séquences UTF-8 invalides sont remplacées.
*
* @param bytes Sortie brute du processus.
*
* @return Nouvelle chaîne normalisée, ou NULL.
*/
static char *tool_catalog_extract_first_nonempty_line(
GBytes *bytes
)
{
gconstpointer bytes_data = NULL;
gsize bytes_size = 0;
gsize inspected_size = 0;
gsize line_index = 0;
char *utf8_text = NULL;
char **lines = NULL;
char *version = NULL;
char *trimmed_line = NULL;
if (bytes == NULL)
{
return NULL;
}
bytes_data = g_bytes_get_data(
bytes,
&bytes_size
);
if (bytes_data == NULL ||
bytes_size == 0)
{
return NULL;
}
inspected_size = MIN(
bytes_size,
(gsize) TOOL_CATALOG_VERSION_OUTPUT_LIMIT
);
utf8_text = g_utf8_make_valid(
bytes_data,
(gssize) inspected_size
);
if (utf8_text == NULL)
{
return NULL;
}
lines = g_strsplit(
utf8_text,
"\n",
-1
);
if (lines == NULL)
{
g_free(
utf8_text
);
return NULL;
}
for (line_index = 0;
lines[line_index] != NULL;
line_index++)
{
/*
* g_strstrip() retire notamment :
*
* - espaces ;
* - tabulations ;
* - retours chariot ;
* - fins de ligne.
*/
trimmed_line = g_strstrip(
lines[line_index]
);
if (trimmed_line[0] == '\0')
{
continue;
}
version = g_strdup(
trimmed_line
);
break;
}
g_strfreev(
lines
);
g_free(
utf8_text
);
return version;
}
GQuark tool_catalog_error_quark(void)
{
return g_quark_from_static_string(
"labfy-investigation-tool-catalog-error"
);
}
gsize tool_catalog_get_count(void)
{
return G_N_ELEMENTS(
tool_catalog_entries
);
}
const ToolCatalogEntry *tool_catalog_get_entry(
gsize index
)
{
if (index >= tool_catalog_get_count())
{
return NULL;
}
return &tool_catalog_entries[index];
}
const ToolCatalogEntry *tool_catalog_find(
const char *identifier
)
{
gsize entry_index = 0;
if (!tool_catalog_string_is_valid(
identifier
))
{
return NULL;
}
for (entry_index = 0;
entry_index < tool_catalog_get_count();
entry_index++)
{
if (g_strcmp0(
tool_catalog_entries[entry_index].identifier,
identifier
) == 0)
{
return &tool_catalog_entries[entry_index];
}
}
return NULL;
}
const char *tool_catalog_entry_get_identifier(
const ToolCatalogEntry *entry
)
{
if (entry == NULL)
{
return NULL;
}
return entry->identifier;
}
const char *tool_catalog_entry_get_display_name(
const ToolCatalogEntry *entry
)
{
if (entry == NULL)
{
return NULL;
}
return entry->display_name;
}
const char *tool_catalog_entry_get_executable_name(
const ToolCatalogEntry *entry
)
{
if (entry == NULL)
{
return NULL;
}
return entry->executable_name;
}
ToolRequirement tool_catalog_entry_get_requirement(
const ToolCatalogEntry *entry
)
{
if (entry == NULL)
{
/*
* Valeur neutre et non bloquante pour un appel invalide.
*/
return TOOL_REQUIREMENT_OPTIONAL;
}
return entry->requirement;
}
gsize tool_catalog_entry_get_version_argument_count(
const ToolCatalogEntry *entry
)
{
if (entry == NULL)
{
return 0;
}
return entry->version_argument_count;
}
const char *tool_catalog_entry_get_version_argument(
const ToolCatalogEntry *entry,
gsize index
)
{
if (entry == NULL ||
entry->version_arguments == NULL ||
index >= entry->version_argument_count)
{
return NULL;
}
return entry->version_arguments[index];
}
gboolean tool_catalog_register_defaults(
ToolRegistry *tool_registry,
GError **error
)
{
const ToolCatalogEntry *entry = NULL;
GError *registration_error = NULL;
gsize entry_index = 0;
gboolean registration_success = FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (tool_registry == NULL)
{
g_set_error_literal(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_INVALID_ARGUMENT,
"Le registre fourni au catalogue est invalide."
);
return FALSE;
}
/*
* Prévalidation complète.
*
* Aucun outil ne doit être ajouté lorsqu'au moins un identifiant
* existe déjà dans le registre.
*/
for (entry_index = 0;
entry_index < tool_catalog_get_count();
entry_index++)
{
entry = tool_catalog_get_entry(
entry_index
);
if (entry == NULL)
{
g_set_error_literal(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_REGISTRATION,
"Le catalogue contient une entrée invalide."
);
return FALSE;
}
if (tool_registry_find(
tool_registry,
entry->identifier
) != NULL)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_REGISTRATION,
"L'outil '%s' existe déjà dans le registre.",
entry->identifier
);
return FALSE;
}
}
/*
* Aucun doublon n'a é trouvé. Les entrées peuvent maintenant
* être enregistrées.
*/
for (entry_index = 0;
entry_index < tool_catalog_get_count();
entry_index++)
{
entry = tool_catalog_get_entry(
entry_index
);
registration_success =
tool_registry_register(
tool_registry,
entry->identifier,
entry->display_name,
entry->executable_name,
entry->requirement,
&registration_error
);
if (!registration_success)
{
tool_catalog_set_wrapped_error(
error,
TOOL_CATALOG_ERROR_REGISTRATION,
"Une entrée du catalogue n'a pas pu être enregistrée",
registration_error
);
g_clear_error(
&registration_error
);
return FALSE;
}
}
return TRUE;
}
gboolean tool_catalog_detect_version(
const ToolRegistry *tool_registry,
const char *identifier,
GCancellable *cancellable,
char **out_version,
GError **error
)
{
const ToolCatalogEntry *catalog_entry = NULL;
const ToolInfo *tool_info = NULL;
ToolAvailability availability;
const char *resolved_path = NULL;
char *executable_path = NULL;
char *detected_version = NULL;
ToolProcessResult *process_result = NULL;
GBytes *stdout_bytes = NULL;
GBytes *stderr_bytes = NULL;
GError *process_error = NULL;
gboolean process_success = FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
FALSE
);
if (tool_registry == NULL ||
!tool_catalog_string_is_valid(
identifier
) ||
out_version == NULL ||
*out_version != NULL)
{
g_set_error_literal(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_INVALID_ARGUMENT,
"Les arguments de détection de version sont invalides."
);
return FALSE;
}
*out_version = NULL;
catalog_entry = tool_catalog_find(
identifier
);
if (catalog_entry == NULL)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_ENTRY_NOT_FOUND,
"L'identifiant '%s' n'existe pas dans le catalogue.",
identifier
);
return FALSE;
}
tool_info = tool_registry_find(
tool_registry,
identifier
);
if (tool_info == NULL)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_TOOL_NOT_REGISTERED,
"L'outil '%s' n'est pas enregistré dans le registre.",
identifier
);
return FALSE;
}
availability = tool_info_get_availability(
tool_info
);
if (availability ==
TOOL_AVAILABILITY_UNKNOWN)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_TOOL_NOT_CHECKED,
"La disponibilité de l'outil '%s' n'a pas été vérifiée.",
identifier
);
return FALSE;
}
if (availability ==
TOOL_AVAILABILITY_MISSING)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_TOOL_MISSING,
"L'outil '%s' est absent de la machine.",
identifier
);
return FALSE;
}
if (availability !=
TOOL_AVAILABILITY_AVAILABLE)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_INVALID_TOOL_STATE,
"L'outil '%s' possède un état de disponibilité invalide.",
identifier
);
return FALSE;
}
resolved_path = tool_info_get_resolved_path(
tool_info
);
if (!tool_catalog_string_is_valid(
resolved_path
))
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_INVALID_TOOL_STATE,
"L'outil '%s' est disponible mais ne possède aucun chemin.",
identifier
);
return FALSE;
}
/*
* La copie permet de ne plus dépendre du ToolInfo pendant
* l'exécution du processus.
*/
executable_path = g_strdup(
resolved_path
);
process_success = tool_process_run(
executable_path,
catalog_entry->version_arguments,
NULL,
cancellable,
&process_result,
&process_error
);
g_free(
executable_path
);
executable_path = NULL;
if (!process_success)
{
if (process_error != NULL &&
g_error_matches(
process_error,
TOOL_PROCESS_ERROR,
TOOL_PROCESS_ERROR_CANCELLED
))
{
g_set_error(
error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"%s",
process_error->message != NULL
? process_error->message
: "La détection de version a été annulée."
);
}
else
{
tool_catalog_set_wrapped_error(
error,
TOOL_CATALOG_ERROR_PROCESS,
"La commande de version n'a pas pu être exécutée",
process_error
);
}
g_clear_error(
&process_error
);
tool_process_result_free(
process_result
);
return FALSE;
}
if (process_result == NULL)
{
g_set_error_literal(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_PROCESS,
"La commande de version n'a produit aucun résultat."
);
return FALSE;
}
/*
* Une commande de version doit se terminer normalement avec
* le code zéro.
*/
if (!tool_process_result_exited_normally(
process_result
))
{
if (tool_process_result_was_signaled(
process_result
))
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_VERSION_COMMAND,
"La commande de version de '%s' a été terminée "
"par le signal %d.",
identifier,
tool_process_result_get_termination_signal(
process_result
)
);
}
else
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_VERSION_COMMAND,
"La commande de version de '%s' ne s'est pas "
"terminée normalement.",
identifier
);
}
tool_process_result_free(
process_result
);
return FALSE;
}
if (tool_process_result_get_exit_status(
process_result
) != 0)
{
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_VERSION_COMMAND,
"La commande de version de '%s' a retourné le code %d.",
identifier,
tool_process_result_get_exit_status(
process_result
)
);
tool_process_result_free(
process_result
);
return FALSE;
}
stdout_bytes =
tool_process_result_ref_stdout(
process_result
);
detected_version =
tool_catalog_extract_first_nonempty_line(
stdout_bytes
);
/*
* Certains outils écrivent leur version sur stderr.
*/
if (detected_version == NULL)
{
stderr_bytes =
tool_process_result_ref_stderr(
process_result
);
detected_version =
tool_catalog_extract_first_nonempty_line(
stderr_bytes
);
}
g_clear_pointer(
&stdout_bytes,
g_bytes_unref
);
g_clear_pointer(
&stderr_bytes,
g_bytes_unref
);
tool_process_result_free(
process_result
);
process_result = NULL;
if (detected_version == NULL ||
detected_version[0] == '\0')
{
g_clear_pointer(
&detected_version,
g_free
);
g_set_error(
error,
TOOL_CATALOG_ERROR,
TOOL_CATALOG_ERROR_VERSION_OUTPUT,
"L'outil '%s' n'a produit aucune version exploitable.",
identifier
);
return FALSE;
}
*out_version = detected_version;
return TRUE;
}

BIN
tests/test_tool_catalog Executable file

Binary file not shown.

2017
tests/test_tool_catalog.c Normal file

File diff suppressed because it is too large Load diff