release: version 1.0.9 - Add SNI, ALPN, TLS version extraction and architecture.yml compliance
Some checks failed
Build RPM Package / Build RPM Packages (CentOS 7, Rocky 8/9/10) (push) Has been cancelled

New features:
- Extract SNI (Server Name Indication) from TLS ClientHello
- Extract ALPN (Application-Layer Protocol Negotiation) protocols
- Detect TLS version from ClientHello using tlsfingerprint library
- Add ConnID field for TCP flow correlation
- Add SensorID field for multi-sensor deployments
- Add SynToCHMs timing field for behavioral detection
- Add AsyncBuffer configuration for output queue sizing

Architecture changes:
- Remove JA4Hash from LogRecord (JA4 format includes its own hash portions)
- Update api.TLSClientHello with new TLS metadata fields
- Update api.LogRecord with correlation, TLS, and timing fields
- Ensure 100% compliance with architecture.yml specification

Tests:
- Add unit tests for TLS extension extraction (SNI, ALPN, Version)
- Update tests for new LogRecord schema without JA4Hash
- Add tests for AsyncBuffer configuration

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Jacquin Antoine
2026-03-02 19:32:16 +01:00
parent fd162982d9
commit 965720a183
12 changed files with 854 additions and 392 deletions

View File

@ -81,11 +81,13 @@ modules:
- name: output
path: "internal/output"
description: "Sortie des résultats (JA4 + meta) vers différentes destinations."
description: "Sortie asynchrone ultra-rapide des résultats (JA4 + meta)."
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 une file d'attente interne (buffer channel) pour rendre l'écriture non-bloquante pour la capture."
- "Sérialiser le JSON le plus rapidement possible (ex: pool d'allocations, librairies optimisées comme goccy/go-json)."
- "Envoyer les enregistrements vers une ou plusieurs sorties (socket UNIX DGRAM, stdout, fichier, ...)."
- "Gérer un MultiWriter pour combiner plusieurs outputs sans modifier le reste du code."
allowed_dependencies:
- "config"
@ -215,10 +217,21 @@ api:
- { name: TCPOptions, type: "string", json_key: "tcp_meta_options" }
# Fingerprints
- { name: JA4, type: "string", json_key: "ja4" }
- { name: JA4Hash, type: "string", json_key: "ja4_hash" }
- { name: JA3, type: "string", json_key: "ja3" }
- { name: JA3Hash, type: "string", json_key: "ja3_hash" }
- { name: JA4, type: "string", json_key: "ja4", description: "Le format JA4 inclut nativement ses propres hachages (parties b et c), pas besoin de ja4_hash séparé." }
- { name: JA3, type: "string", json_key: "ja3", description: "Chaîne brute JA3 (variable)." }
- { name: JA3Hash, type: "string", json_key: "ja3_hash", description: "Hachage MD5 indispensable pour exploiter la chaîne JA3." }
# --- Corrélation & Triage ---
- { name: ConnID, type: "string", json_key: "conn_id", optional: true, description: "Identifiant unique du flux (ex: hash de src_ip:src_port-dst_ip:dst_port) pour corréler facilement plusieurs événements liés à une même session TCP." }
- { name: SensorID, type: "string", json_key: "sensor_id", optional: true, description: "Nom ou identifiant du serveur/capteur qui a généré le log. Indispensable pour du déploiement à grande échelle." }
# --- Éléments TLS (ClientHello) ---
- { name: TLSVersion, type: "string", json_key: "tls_version", optional: true, description: "Version TLS maximale supportée annoncée par le client (ex: 1.2, 1.3). Utile pour repérer les clients obsolètes." }
- { name: SNI, type: "string", json_key: "tls_sni", optional: true, description: "Server Name Indication en clair. Crucial pour détecter le domaine visé par le client (C2, DGA, etc.)." }
- { name: ALPN, type: "string", json_key: "tls_alpn", optional: true, description: "Application-Layer Protocol Negotiation (ex: h2, http/1.1). Aide à différencier le trafic web légitime d'un tunnel personnalisé." }
# --- Détection comportementale (Timing) ---
- { name: SynToCHMs, type: "*uint32", json_key: "syn_to_clienthello_ms", optional: true, description: "Temps écoulé (en millisecondes) entre l'observation du SYN et l'envoi du ClientHello complet." }
# Timestamp
- { name: Timestamp, type: "int64", json_key: "timestamp", description: "Wall-clock timestamp in nanoseconds since Unix epoch (auto-filled by NewLogRecord)." }
@ -228,6 +241,7 @@ api:
description: "Configuration dune sortie de logs."
fields:
- { name: Type, type: "string", description: "Type doutput (unix_socket, stdout, file, ...)." }
- { name: AsyncBuffer, type: "int", description: "Taille de la file d'attente avant envoi asynchrone (ex: 5000)." }
- { name: Enabled, type: "bool", description: "Active ou non cette sortie." }
- { name: Params, type: "map[string]string", description: "Paramètres spécifiques (socket_path, path, ...)." }
@ -307,7 +321,7 @@ api:
module: "output"
implements: "output.Writer"
config:
- { name: socket_path, type: "string", description: "Chemin de la socket UNIX (ex: /var/run/logcorrelator/network.socket)." }
- { name: socket_path, type: "string", description: "Chemin de la socket UNIX DGRAM (ex: /var/run/logcorrelator/network.sock)." }
- name: "output.MultiWriter"
description: "Combinaison de plusieurs Writer configurés."
@ -404,347 +418,15 @@ flow_control:
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:
- "Suivi unidirectionnel : uniquement le flux entrant du client vers la machine locale (srcIP:srcPort -> dstIP:dstPort)."
- "Pour chaque flux, 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."
- "Le flow key est au format : `srcIP:srcPort->dstIP:dstPort` (pas de suivi bidirectionnel)."
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 d'intégration dans Docker (capture TLS client + outputs)."
commands:
- "docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from ja4sentinel-test"
- 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 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."
packaging:
description: >
ja4sentinel est distribué sous forme de packages .rpm (Rocky Linux/RHEL/CentOS/AlmaLinux),
construits intégralement dans Docker avec rpmbuild. Le binaire est compilé sur Rocky Linux 9
pour une compatibilité binaire maximale avec toutes les distributions RHEL-based.
formats:
- rpm
target_distros:
rpm:
- rocky-linux-8+
- rocky-linux-9+
- rocky-linux-10+
- almalinux-8+
- almalinux-9+
- almalinux-10+
- rhel-8+
- rhel-9+
- rhel-10+
tool: rpmbuild
build_pipeline:
dockerfile: Dockerfile.package
stages:
- name: builder
description: >
Compilation du binaire Go sur Rocky Linux 9 avec CGO_ENABLED=1.
GOOS=linux GOARCH=amd64 pour un binaire compatible x86_64.
Le binaire est dynamiquement lié à libpcap pour une compatibilité maximale.
- name: rpm_builder
description: >
Image Rocky Linux 9 avec rpm-build. Setup de l'arborescence rpmbuild
(BUILD, RPMS, SOURCES, SPECS, SRPMS). Copie du spec et des sources,
puis build avec rpmbuild -bb pour el8, el9, el10.
- name: output
description: >
Image Alpine minimale contenant les packages RPM dans /packages/rpm/el{8,9,10}.
files:
binary:
source: dist/ja4sentinel
dest: /usr/bin/ja4sentinel
mode: "0755"
systemd:
source: packaging/systemd/ja4sentinel.service
dest: /usr/lib/systemd/system/ja4sentinel.service
mode: "0644"
config:
- source: packaging/systemd/config.yml
dest: /etc/ja4sentinel/config.yml.default
mode: "0640"
config_file: true
- source: packaging/systemd/config.yml
dest: /usr/share/ja4sentinel/config.yml
mode: "0640"
directories:
- path: /var/lib/ja4sentinel
mode: "0750"
- path: /var/log/ja4sentinel
mode: "0750"
- path: /var/run/logcorrelator
mode: "0750"
- path: /etc/ja4sentinel
mode: "0750"
spec_file:
path: packaging/rpm/ja4sentinel.spec
version_macro: "%{?build_version}%{!?build_version:1.0.0}"
scripts:
pre: >
Script %pre intégré dans le spec - ne crée plus d'utilisateur
car le service tourne en root pour la capture réseau.
post: >
Script %post intégré dans le spec - configure les permissions
root:root sur les directories et active le service systemd.
dependencies:
rpm:
- systemd
- libpcap >= 1.9.0
verify:
rpm:
command: docker run --rm -v $(pwd)/build/rpm:/packages rockylinux:9 sh -c "dnf install -y /packages/*.rpm"
service:
systemd:
unit_name: "ja4sentinel.service"
description: "JA4 client fingerprinting daemon"
wanted_by: "multi-user.target"
exec:
binary_path: "/usr/bin/ja4sentinel"
args:
- "--config"
- "/etc/ja4sentinel/config.yml"
user_group:
user: "root"
group: "root"
note: >
Le service tourne en root pour la capture réseau (CAP_NET_RAW, CAP_NET_ADMIN).
La création d'utilisateur ja4sentinel a été supprimée.
runtime:
working_directory: "/var/lib/ja4sentinel"
restart: "on-failure"
restart_sec: 5
watchdog_sec: 30
environment_prefix: "JA4SENTINEL_"
systemd_notify:
type: "notify"
access: "main"
protocol: "sdnotify"
signals:
- "READY - envoyé après chargement de la configuration"
- "WATCHDOG - ping périodique toutes les 15s (watchdog_sec/2)"
- "STOPPING - envoyé avant l'arrêt propre"
benefits:
- "systemd sait quand le service est vraiment prêt"
- "Détection automatique des blocages (redémarrage après 30s)"
- "Meilleure intégration avec la supervision systemd"
logging:
type: "journald"
journal_identifier: "ja4sentinel"
expectations:
- "Le binaire écrit les logs de service sur stdout/stderr."
- "Les messages doivent inclure au minimum un niveau (INFO/ERROR) et un composant."
security:
capabilities:
- "CAP_NET_RAW"
- "CAP_NET_ADMIN"
sandboxing:
- "ProtectSystem=strict"
- "ProtectHome=yes"
- "PrivateTmp=yes"
- "ProtectKernelTunables=yes"
- "ProtectKernelModules=yes"
- "ProtectControlGroups=yes"
- "RestrictRealtime=yes"
- "RestrictSUIDSGID=yes"
- "LockPersonality=yes"
- "ReadWritePaths=/var/lib/ja4sentinel /var/log/ja4sentinel"
integration_rules:
- "Le binaire doit s'arrêter proprement sur SIGTERM (systemd stop)."
- "Le module cmd_ja4sentinel gère les signaux et termine la capture proprement."
- "Les chemins (config, socket UNIX, logs) doivent être compatibles avec FHS (/etc, /var/run, /var/log)."
- "Le module cmd_ja4sentinel capture SIGTERM/SIGINT et déclenche un arrêt propre (stop capture, flush outputs, fermer socket UNIX)."
- "Le processus doit retourner un code de sortie non nul en cas d'erreur fatale au démarrage."
- "Le service utilise sdnotify pour signaler READY/WATCHDOG/STOPPING à systemd."
logging:
strategy:
description: >
ja4sentinel écrit ses logs techniques sur stdout/stderr au format JSON lines,
afin que systemd/journald puissent les collecter et les filtrer.
format: "json_lines"
fields:
- "timestamp"
- "level"
- "component" # capture, tlsparse, fingerprint, output, service, ...
- "message"
- "trace_id" # optionnel
- "conn_id" # optionnel (identifiant de flux TCP)
rules:
- "Pas décriture directe dans des fichiers de log techniques depuis le code (stdout/stderr uniquement)."
- "Les logs techniques du daemon passent par stdout/stderr (systemd/journald)."
- "Les outputs métiers (LogRecord JA4) sont gérés par le module output, vers socket UNIX et/ou fichier JSON."
json_format:
description: >
Les LogRecord métiers (JA4 + métadonnées) sont sérialisés en JSON objet plat,
avec des champs nommés explicitement pour ingestion dans ClickHouse.
rules:
- "Pas de tableaux imbriqués ni dobjets deeply nested."
- "Toutes les métadonnées IP/TCP sont flatten sous forme de champs scalaires nommés."
- "Les noms de champs suivent la convention: ip_meta_*, tcp_meta_*, ja4*."
- "Les noms de champs suivent la convention: ip_meta_*, tcp_meta_*, ja*."
- "Pas de champ ja4_hash : le format JA4 intègre déjà son propre hachage tronqué, la chaîne complète de 38 caractères suffit."
logrecord_schema:
# Exemple de mapping pour api.LogRecord (résumé)
- "conn_id"
- "sensor_id"
- "src_ip"
- "src_port"
- "dst_ip"
@ -757,8 +439,11 @@ logging:
- "tcp_meta_mss"
- "tcp_meta_window_scale"
- "tcp_meta_options" # string joinée, ex: 'MSS,SACK,TS,NOP,WS'
- "tls_version"
- "tls_sni"
- "tls_alpn"
- "syn_to_clienthello_ms"
- "ja4"
- "ja4_hash"
- "ja3"
- "ja3_hash"