482 lines
21 KiB
YAML
482 lines
21 KiB
YAML
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."
|
||
|