This commit is contained in:
Jacquin Antoine
2026-02-25 03:15:53 +01:00
commit 5fa3c3c293

481
architecture.yml Normal file
View File

@ -0,0 +1,481 @@
version: 1
project:
name: ja4sentinel
description: >
Outil Go pour capturer le trafic réseau sur un serveur Linux,
extraire les handshakes TLS côté client, générer les signatures JA4
(via psanford/tlsfingerprint), enrichir avec des métadonnées IP/TCP,
et loguer les résultats (IP, ports, JA4, meta) vers une ou plusieurs
sorties configurables (socket UNIX, stdout, fichier, ...).
languages:
- go
goals:
- "Développement bloc par bloc avec interfaces simples et stables."
- "Focalisé sur JA4 client (le serveur est connu/local)."
- "Séparation claire des responsabilités (capture, parsing, fingerprint, output)."
- "Tests unitaires pour chaque fonction publique."
- "Tests dintégration dans des conteneurs Docker."
- "Commentaires standardisés, code évolutif avec changements minimaux."
modules:
- name: config
path: "internal/config"
description: "Chargement et validation de la configuration (fichier, env, CLI)."
responsibilities:
- "Lire le fichier de configuration (YAML par défaut)."
- "Fusionner avec les overrides env/CLI."
- "Construire une api.AppConfig cohérente."
allowed_dependencies: []
forbidden_dependencies:
- "capture"
- "tlsparse"
- "fingerprint"
- "output"
- name: capture
path: "internal/capture"
description: "Capture des paquets réseau (pcap/raw socket) sur Linux."
responsibilities:
- "Ouvrir linterface réseau configurée."
- "Appliquer les filtres (ports, BPF, protocole)."
- "Observer les flux TCP côté client vers les ports dintérêt."
- "Extraire les en-têtes IP/TCP utiles (IPMeta, TCPMeta)."
- "Convertir les paquets en objets RawPacket."
allowed_dependencies:
- "config"
- "api"
forbidden_dependencies:
- "tlsparse"
- "fingerprint"
- "output"
- name: tlsparse
path: "internal/tlsparse"
description: "Extraction des ClientHello TLS côté client à partir des paquets capturés."
responsibilities:
- "Décoder les couches IP/TCP jusquau payload TLS."
- "Identifier le ClientHello TLS du client sur les ports configurés."
- "Assembler les segments si nécessaire pour obtenir un ClientHello complet."
- "Produire des TLSClientHello enrichis avec IPMeta et TCPMeta."
allowed_dependencies:
- "config"
- "capture"
- "api"
forbidden_dependencies:
- "output"
- name: fingerprint
path: "internal/fingerprint"
description: "Génération des empreintes JA4 à partir des ClientHello TLS."
responsibilities:
- "Utiliser psanford/tlsfingerprint pour analyser le ClientHello."
- "Générer la chaîne JA4 (et éventuellement JA3) côté client."
- "Encapsuler les résultats dans un type Fingerprints."
allowed_dependencies:
- "config"
- "tlsparse"
- "api"
forbidden_dependencies:
- "capture"
- name: output
path: "internal/output"
description: "Sortie des résultats (JA4 + meta) vers différentes destinations."
responsibilities:
- "Prendre en entrée les Fingerprints et les métadonnées réseau."
- "Formater les données en enregistrements log (JSON ou autre format simple)."
- "Envoyer les enregistrements vers une ou plusieurs sorties (socket UNIX, stdout, fichier, ...)."
- "Gérer un MultiWriter pour combiner plusieurs outputs sans modifier le reste du code."
allowed_dependencies:
- "config"
- "api"
forbidden_dependencies:
- "capture"
- "tlsparse"
- "fingerprint"
- name: cmd_ja4sentinel
path: "cmd/ja4sentinel"
description: "Point dentrée de lapplication (main)."
responsibilities:
- "Charger la configuration via le module config."
- "Construire les instances des modules (capture, tlsparse, fingerprint, output)."
- "Brancher les modules entre eux selon larchitecture pipeline."
- "Gérer les signaux système (arrêt propre)."
allowed_dependencies:
- "config"
- "capture"
- "tlsparse"
- "fingerprint"
- "output"
- "api"
forbidden_dependencies: []
api:
types:
- name: "api.Config"
description: "Configuration réseau et TLS de base."
fields:
- { name: Interface, type: "string", description: "Nom de linterface réseau (ex: eth0)." }
- { name: ListenPorts, type: "[]uint16", description: "Ports TCP à surveiller (ex: [443, 8443])." }
- { name: BPFFilter, type: "string", description: "Filtre BPF optionnel pour la capture." }
- name: "api.IPMeta"
description: "Métadonnées IP pour fingerprinting de stack."
fields:
- { name: TTL, type: "uint8", description: "TTL initial observé." }
- { name: TotalLength, type: "uint16", description: "Taille totale du paquet IP." }
- { name: IPID, type: "uint16", description: "Identifiant IP du paquet." }
- { name: DF, type: "bool", description: "Flag Don't Fragment." }
- name: "api.TCPMeta"
description: "Métadonnées TCP pour fingerprinting de stack."
fields:
- { name: WindowSize, type: "uint16", description: "Fenêtre initiale TCP." }
- { name: MSS, type: "uint16", description: "Maximum Segment Size (option TCP)." }
- { name: WindowScale, type: "uint8", description: "Facteur de scaling (option TCP)." }
- { name: Options, type: "[]string", description: "Liste ordonnée des options TCP (ex: [MSS, SACK, TS])." }
- name: "api.RawPacket"
description: "Paquet brut capturé sur le réseau (vue minimale)."
fields:
- { name: Data, type: "[]byte", description: "Contenu brut du paquet." }
- { name: Timestamp, type: "int64", description: "Timestamp (nanos / epoch) de capture." }
- name: "api.TLSClientHello"
description: "Représentation dun ClientHello TLS client, avec meta IP/TCP."
fields:
- { name: SrcIP, type: "string", description: "Adresse IP source (client)." }
- { name: SrcPort, type: "uint16", description: "Port source (client)." }
- { name: DstIP, type: "string", description: "Adresse IP destination (serveur)." }
- { name: DstPort, type: "uint16", description: "Port destination (serveur)." }
- { name: Payload, type: "[]byte", description: "Bytes correspondant au ClientHello TLS." }
- { name: IPMeta, type: "api.IPMeta", description: "Métadonnées IP observées côté client." }
- { name: TCPMeta, type: "api.TCPMeta", description: "Métadonnées TCP observées côté client." }
- name: "api.Fingerprints"
description: "Empreintes TLS pour un flux client."
fields:
- { name: JA4, type: "string", description: "Signature JA4 client." }
- { name: JA4Hash, type: "string", description: "Hash JA4 client." }
- { name: JA3, type: "string", description: "Signature JA3 (optionnel, si calculée)." }
- { name: JA3Hash, type: "string", description: "Hash JA3 (optionnel)." }
- name: "api.LogRecord"
description: "Enregistrement de log final envoyé vers les outputs."
fields:
- { name: SrcIP, type: "string", description: "Adresse IP source (client)." }
- { name: SrcPort, type: "uint16", description: "Port source (client)." }
- { name: DstIP, type: "string", description: "Adresse IP destination (serveur)." }
- { name: DstPort, type: "uint16", description: "Port destination (serveur)." }
- { name: IPMeta, type: "api.IPMeta", description: "Métadonnées IP." }
- { name: TCPMeta, type: "api.TCPMeta", description: "Métadonnées TCP." }
- { name: Fingerprints, type: "api.Fingerprints", description: "Empreintes JA4/JA3 associées." }
- name: "api.OutputConfig"
description: "Configuration dune sortie de logs."
fields:
- { name: Type, type: "string", description: "Type doutput (unix_socket, stdout, file, ...)." }
- { name: Enabled, type: "bool", description: "Active ou non cette sortie." }
- { name: Params, type: "map[string]string", description: "Paramètres spécifiques (socket_path, path, ...)." }
- name: "api.AppConfig"
description: "Configuration complète de ja4sentinel."
fields:
- { name: Core, type: "api.Config", description: "Paramètres réseau + TLS." }
- { name: Outputs, type: "[]api.OutputConfig", description: "Liste des outputs configurés." }
interfaces:
- name: "config.Loader"
description: "Charge la configuration (fichier + env + CLI)."
module: "config"
methods:
- name: "Load"
params: []
returns:
- { type: "api.AppConfig" }
- { type: "error" }
- name: "capture.Capture"
description: "Source de paquets réseau bruts côté client."
module: "capture"
methods:
- name: "Run"
params:
- { name: cfg, type: "api.Config" }
- { name: out, type: "chan<- api.RawPacket" }
returns:
- { type: "error" }
notes:
- "Doit respecter les filtres (ports, BPF) définis dans la configuration."
- "Ne connaît pas le format TLS ni JA4."
- name: "tlsparse.Parser"
description: "Transforme des RawPacket en TLSClientHello (côté client uniquement)."
module: "tlsparse"
methods:
- name: "Process"
params:
- { name: pkt, type: "api.RawPacket" }
returns:
- { type: "*api.TLSClientHello" }
- { type: "error" }
notes:
- "Retourne nil si le paquet ne contient pas (ou plus) de ClientHello."
- "Pour chaque flux, sarrête une fois le ClientHello complet obtenu."
- name: "fingerprint.Engine"
description: "Génère les empreintes JA4 (et JA3 éventuellement) à partir dun ClientHello."
module: "fingerprint"
methods:
- name: "FromClientHello"
params:
- { name: ch, type: "api.TLSClientHello" }
returns:
- { type: "*api.Fingerprints" }
- { type: "error" }
notes:
- "Utilise github.com/psanford/tlsfingerprint en interne."
- "Focalisé sur le JA4 client (le côté serveur est déjà connu)."
- name: "output.Writer"
description: "Interface générique pour écrire les résultats."
module: "output"
methods:
- name: "Write"
params:
- { name: rec, type: "api.LogRecord" }
returns:
- { type: "error" }
notes:
- "Ne connaît pas la capture ni les détails de parsing TLS."
- name: "output.UnixSocketWriter"
description: "Implémentation de Writer envoyant les logs sur une socket UNIX."
module: "output"
implements: "output.Writer"
config:
- { name: socket_path, type: "string", description: "Chemin de la socket UNIX (ex: /var/run/ja4sentinel.sock)." }
- name: "output.MultiWriter"
description: "Combinaison de plusieurs Writer configurés."
module: "output"
implements: "output.Writer"
config:
- { name: writers, type: "[]output.Writer", description: "Liste de Writers concrets à appeler." }
- name: "output.Builder"
description: "Construit les Writers à partir de api.AppConfig."
module: "output"
methods:
- name: "NewFromConfig"
params:
- { name: cfg, type: "api.AppConfig" }
returns:
- { type: "output.Writer" }
- { type: "error" }
notes:
- "Doit supporter plusieurs outputs simultanés via un MultiWriter."
architecture:
style: "pipeline"
flow:
- from: "capture.Capture"
to: "tlsparse.Parser"
via: "api.RawPacket"
- from: "tlsparse.Parser"
to: "fingerprint.Engine"
via: "api.TLSClientHello"
- from: "fingerprint.Engine"
to: "output.Writer"
via: "api.LogRecord"
constraints:
- id: "client_only"
description: "On ne calcule que les empreintes JA4 côté client (pas côté serveur)."
- id: "no_back_dependencies"
description: "Pas de dépendances en arrière (output ne dépend pas de fingerprint, etc.)."
- id: "simple_messages"
description: "Les communications entre blocs utilisent uniquement les types définis dans api.*."
- id: "no_global_state"
description: "Pas de variables globales partagées entre blocs pour la logique principale."
flow_control:
connection_states:
description: "États simplifiés dun flux TCP pour minimiser la capture."
states:
- name: "NEW"
description: "Observation dun SYN client sur un port surveillé, création dun état minimal (IP/TCP meta)."
- name: "WAIT_CLIENT_HELLO"
description: "Accumulation des segments TCP nécessaires pour extraire un ClientHello complet."
- name: "JA4_DONE"
description: "JA4 calculé et logué, on arrête de suivre ce flux."
rules:
- "Pour chaque flux (srcIP, srcPort, dstIP, dstPort), on sarrête dès que JA4 + IPMeta + TCPMeta sont obtenus et logués."
- "Un timeout par flux doit être défini pour éviter de garder un état si le ClientHello narrive jamais."
testing:
policy:
description: >
Chaque fonction publique (exportée) doit avoir des tests unitaires,
et les principaux flux (capture -> tlsparse -> fingerprint -> output)
doivent être couverts par des tests dintégration automatisés.
applies_to:
- "toutes les méthodes des interfaces listées dans api.interfaces"
requirements:
- id: "test_skeletons"
description: "Pour chaque nouvelle fonction exportée, créer au minimum un squelette de test dans un fichier *_test.go."
- id: "error_cases"
description: "Les cas derreur doivent être couverts par au moins un test (inputs invalides, erreurs réseau, parsing raté, etc.)."
- id: "mock_dependencies"
description: "Les dépendances entre modules (Capture, Parser, Engine, Writer) doivent être mockables via interfaces."
levels:
- name: "unit"
description: "Tests sur chaque module isolé (config, capture, tlsparse, fingerprint, output)."
tools:
- "go test"
rules:
- "Pas de dépendance réseau réelle."
- "Les interactions entre modules sont mockées."
- name: "integration"
description: "Tests bout-à-bout dans Docker avec trafic TLS client simulé."
tools:
- "go test"
- "docker"
- "docker compose"
scenarios:
- id: "tls_client_ja4_to_outputs"
description: >
Injecter un flux TLS de test côté client,
vérifier que ja4sentinel capture les paquets client,
génère JA4, enrichit avec meta IP/TCP,
et écrit les enregistrements attendus vers les outputs configurés.
steps:
- "Démarrer un serveur TLS de test dans un conteneur."
- "Démarrer ja4sentinel dans un autre conteneur, connecté au même réseau."
- "Émettre une connexion TLS client de test."
- "Lire la socket UNIX et/ou les autres outputs et vérifier JA4, IP, ports, meta."
assertions:
- "Au moins un LogRecord est reçu."
- "Les champs JA4 ne sont pas vides."
- "Les IP/ports correspondent au flux client."
- "IPMeta et TCPMeta sont cohérents (TTL, window, options...)."
ci_cd:
goals:
- "Construire et tester ja4sentinel de façon reproductible dans des conteneurs Docker."
- "Fournir une commande unique pour lancer tous les tests (unitaires + intégration)."
docker:
images:
- name: "ja4sentinel-dev"
description: "Image de développement et de test (Go + outils réseau)."
base: "golang:1.23-alpine"
includes:
- "go toolchain"
- "libpcap et headers"
- "outils de test (go test, gotestsum, etc.)"
usage:
build_cmd: "docker build -t ja4sentinel-dev -f Dockerfile.dev ."
test_cmd: "docker run --rm -v $(pwd):/app -w /app ja4sentinel-dev make test"
- name: "ja4sentinel-runtime"
description: "Image minimale pour exécuter le binaire en production."
base: "alpine:latest"
includes:
- "binaire ja4sentinel"
- "libpcap runtime si nécessaire"
usage:
build_cmd: "docker build -t ja4sentinel-runtime -f Dockerfile ."
rules:
- "Le build de production doit partir du code testé."
- "Les tests dintégration réseau se font dans des conteneurs, pas directement sur la machine hôte."
make_targets:
- name: "build"
description: "Compile le binaire ja4sentinel pour la plateforme cible."
commands:
- "go build ./cmd/ja4sentinel"
- name: "test"
description: "Lance les tests unitaires Go."
commands:
- "go test ./..."
- name: "test-integration"
description: "Lance les tests dintégration dans Docker (capture TLS client + outputs)."
commands:
- "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit"
- name: "lint"
description: "Lance les linters (go vet, staticcheck, etc.)."
commands:
- "go vet ./..."
config_guidelines:
sources:
- "Fichier de configuration YAML (par défaut config.yml)."
- "Variables denvironnement (JA4SENTINEL_*)."
- "Flags CLI."
rules:
- "Le module config est la seule source de vérité pour les paramètres (interface, ports, filtres, outputs)."
- "Les autres modules ne lisent jamais les variables denvironnement ni les fichiers de config directement."
code_style:
comments:
goals:
- "Commentaires standardisés, lisibles, utiles pour la génération de documentation."
- "Expliquer le pourquoi et les décisions, pas répéter le code."
rules:
- id: "godoc_exported"
description: >
Chaque fonction, type ou méthode exporté (nom capitalisé) doit avoir
un commentaire de documentation respectant le style GoDoc.
examples:
- "// FromClientHello génère les empreintes JA4 à partir dun ClientHello TLS client."
- id: "why_not_what"
description: "Les commentaires doivent expliquer le pourquoi plutôt que le quoi lorsque le code est clair."
- id: "no_duplicate_code"
description: "Un commentaire ne doit pas simplement répéter ce que le code dit déjà."
- id: "update_with_code"
description: "Si le code est modifié, les commentaires adjacents doivent être revus et mis à jour."
formatting:
- "Utiliser // pour les commentaires, pas /* ... */ sauf cas exceptionnel."
- "Commentaires de doc au-dessus des déclarations, sans ligne vide."
evolution:
api_stability:
strategy: "semver_like"
goals:
- "Éviter les changements majeurs dAPI une fois le projet stabilisé."
- "Limiter les modifications à ce qui est nécessaire (fixes, extensions compatibles)."
rules:
- id: "no_breaking_changes_without_reason"
description: >
Ne pas modifier les signatures des fonctions des interfaces définies dans api.interfaces
sans raison solide et documentée.
- id: "prefer_extension_over_modification"
description: >
Pour ajouter un comportement, préférer ajouter une nouvelle fonction ou un nouveau type,
plutôt que changer une signature existante.
- id: "document_changes"
description: >
Toute modification dAPI doit être reflétée dans architecture.yml
et, si nécessaire, dans un ADR séparé.
refactoring:
goals:
- "Autoriser le refactoring interne sans changer les contrats externes."
- "Limiter limpact des changements à lintérieur dun module."
guidelines:
- "Les interfaces de api.interfaces servent de contrat stable entre modules."
- "Les changements internes (optimisations, refactoring) ne doivent pas casser ces interfaces."
- "Lorsquun changement dAPI est inévitable, marquer lancienne API comme dépréciée avant de la supprimer."
dev_tools:
usage_guidelines:
- "Les outils IA doivent lire architecture.yml avant de générer ou modifier du code."
- "Les outils de test peuvent sappuyer sur ci_cd.docker et ci_cd.make_targets pour générer des scripts / pipelines."
future_extensions:
- "Génération automatique de Dockerfile.dev et Dockerfile à partir de cette section."
- "Génération de fichiers docker-compose.test.yml pour les scénarios dintégration."