From 5fa3c3c293f970aa225e075304f150b6e5c37221 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Wed, 25 Feb 2026 03:15:53 +0100 Subject: [PATCH] init --- architecture.yml | 481 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 architecture.yml diff --git a/architecture.yml b/architecture.yml new file mode 100644 index 0000000..0a8d96d --- /dev/null +++ b/architecture.yml @@ -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 d’inté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 l’interface réseau configurée." + - "Appliquer les filtres (ports, BPF, protocole)." + - "Observer les flux TCP côté client vers les ports d’inté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 jusqu’au 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 d’entrée de l’application (main)." + responsibilities: + - "Charger la configuration via le module config." + - "Construire les instances des modules (capture, tlsparse, fingerprint, output)." + - "Brancher les modules entre eux selon l’architecture 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 l’interface 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 d’un 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 d’une sortie de logs." + fields: + - { name: Type, type: "string", description: "Type d’output (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, s’arrête une fois le ClientHello complet obtenu." + + - name: "fingerprint.Engine" + description: "Génère les empreintes JA4 (et JA3 éventuellement) à partir d’un 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 d’un flux TCP pour minimiser la capture." + states: + - name: "NEW" + description: "Observation d’un SYN client sur un port surveillé, création d’un é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 s’arrê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 n’arrive 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 d’inté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 d’erreur 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 d’inté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 d’inté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 d’environnement (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 d’environnement 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 d’un 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 d’API 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 d’API 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 l’impact des changements à l’intérieur d’un module." + guidelines: + - "Les interfaces de api.interfaces servent de contrat stable entre modules." + - "Les changements internes (optimisations, refactoring) ne doivent pas casser ces interfaces." + - "Lorsqu’un changement d’API est inévitable, marquer l’ancienne 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 s’appuyer 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 d’intégration." +