- Add packaging section describing DEB and RPM builds with fpm - Document Dockerfile.package multi-stage build pipeline - List files, directories, maintainer scripts, and dependencies - Add verification commands for both package types Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
743 lines
31 KiB
YAML
743 lines
31 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: logging
|
||
path: "internal/logging"
|
||
description: "Logs structurés JSON pour le service (stdout/stderr)."
|
||
responsibilities:
|
||
- "Fournir une fabrique de loggers (LoggerFactory)."
|
||
- "Émettre des logs au format JSON lines sur stdout."
|
||
- "Supporter les niveaux : debug, info, warn, error."
|
||
- "Inclure timestamp, niveau, composant, message et détails optionnels."
|
||
allowed_dependencies:
|
||
- "api"
|
||
forbidden_dependencies:
|
||
- "config"
|
||
- "capture"
|
||
- "tlsparse"
|
||
- "fingerprint"
|
||
- "output"
|
||
|
||
- 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, logging)."
|
||
- "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"
|
||
- "logging"
|
||
forbidden_dependencies: []
|
||
|
||
api:
|
||
types:
|
||
- name: "api.ServiceLog"
|
||
description: "Log interne du service ja4sentinel (diagnostic)."
|
||
fields:
|
||
- { name: Level, type: "string", description: "niveau: debug, info, warn, error." }
|
||
- { name: Component, type: "string", description: "module concerné (capture, tlsparse, ...)." }
|
||
- { name: Message, type: "string", description: "texte du log." }
|
||
- { name: Details, type: "map[string]string", description: "infos additionnelles (erreurs, IDs...)." }
|
||
- { name: Timestamp, type: "int64", description: "Timestamp en nanosecondes (auto-rempli par le logger)." }
|
||
- { name: TraceID, type: "string", description: "ID de tracing distribué (optionnel)." }
|
||
- { name: ConnID, type: "string", description: "Identifiant de flux TCP (optionnel)." }
|
||
|
||
- 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: FlowTimeoutSec, type: "int", description: "Timeout en secondes pour l'extraction du handshake TLS (défaut: 30)." }
|
||
- { name: PacketBufferSize,type: "int", description: "Taille du buffer du canal de paquets (défaut: 1000). Pour les environnements à fort trafic." }
|
||
|
||
- 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, sérialisé en JSON objet plat."
|
||
json_object: true
|
||
fields:
|
||
- { name: SrcIP, type: "string", json_key: "src_ip" }
|
||
- { name: SrcPort, type: "uint16", json_key: "src_port" }
|
||
- { name: DstIP, type: "string", json_key: "dst_ip" }
|
||
- { name: DstPort, type: "uint16", json_key: "dst_port" }
|
||
|
||
# IPMeta flatten
|
||
- { name: IPTTL, type: "uint8", json_key: "ip_meta_ttl" }
|
||
- { name: IPTotalLen, type: "uint16", json_key: "ip_meta_total_length" }
|
||
- { name: IPID, type: "uint16", json_key: "ip_meta_id" }
|
||
- { name: IPDF, type: "bool", json_key: "ip_meta_df" }
|
||
|
||
# TCPMeta flatten
|
||
- { name: TCPWindow, type: "uint16", json_key: "tcp_meta_window_size" }
|
||
- { name: TCPMSS, type: "*uint16", json_key: "tcp_meta_mss", optional: true, description: "Pointeur (nil si non présent, 0 si absent)." }
|
||
- { name: TCPWScale, type: "*uint8", json_key: "tcp_meta_window_scale", optional: true, description: "Pointeur (nil si non présent, 0 si absent)." }
|
||
- { 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" }
|
||
|
||
# Timestamp
|
||
- { name: Timestamp, type: "int64", json_key: "timestamp", description: "Wall-clock timestamp in nanoseconds since Unix epoch (auto-filled by NewLogRecord)." }
|
||
|
||
|
||
- 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."
|
||
|
||
- name: "logging.LoggerFactory"
|
||
description: "Fabrique de loggers structurés JSON."
|
||
module: "logging"
|
||
methods:
|
||
- name: "NewLogger"
|
||
params:
|
||
- { name: level, type: "string" }
|
||
returns:
|
||
- { type: "api.Logger" }
|
||
- name: "NewDefaultLogger"
|
||
params: []
|
||
returns:
|
||
- { type: "api.Logger" }
|
||
notes:
|
||
- "Les logs sont émis en JSON lines sur stdout pour systemd/journald."
|
||
|
||
- name: "api.Logger"
|
||
description: "Interface de logging pour tous les modules."
|
||
module: "logging"
|
||
methods:
|
||
- name: "Debug"
|
||
params:
|
||
- { name: component, type: "string" }
|
||
- { name: message, type: "string" }
|
||
- { name: details, type: "map[string]string" }
|
||
- name: "Info"
|
||
params:
|
||
- { name: component, type: "string" }
|
||
- { name: message, type: "string" }
|
||
- { name: details, type: "map[string]string" }
|
||
- name: "Warn"
|
||
params:
|
||
- { name: component, type: "string" }
|
||
- { name: message, type: "string" }
|
||
- { name: details, type: "map[string]string" }
|
||
- name: "Error"
|
||
params:
|
||
- { name: component, type: "string" }
|
||
- { name: message, type: "string" }
|
||
- { name: details, type: "map[string]string" }
|
||
notes:
|
||
- "Tous les logs passent par stdout/stderr (pas de fichiers directs)."
|
||
|
||
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:
|
||
- "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 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."
|
||
|
||
packaging:
|
||
description: >
|
||
ja4sentinel est distribué sous forme de packages .deb (Debian/Ubuntu) et
|
||
.rpm (Rocky Linux/RHEL/CentOS), construits intégralement dans Docker avec fpm.
|
||
formats:
|
||
- deb
|
||
- rpm
|
||
target_distros:
|
||
deb:
|
||
- debian-12+
|
||
- ubuntu-22.04+
|
||
rpm:
|
||
- rocky-linux-8+
|
||
- rocky-linux-9+
|
||
- rhel-8+
|
||
- rhel-9+
|
||
tool: fpm
|
||
build_pipeline:
|
||
dockerfile: Dockerfile.package
|
||
stages:
|
||
- name: builder
|
||
description: >
|
||
Compilation du binaire Go avec CGO_ENABLED=1 pour libpcap.
|
||
GOOS=linux GOARCH=amd64 pour un binaire statique.
|
||
- name: package_builder
|
||
description: >
|
||
Installation de fpm, rpm, dpkg-dev. Création de l'arborescence
|
||
et exécution de fpm pour générer DEB et RPM.
|
||
- name: output
|
||
description: >
|
||
Image Alpine minimale contenant les packages dans /packages/deb et /packages/rpm.
|
||
files:
|
||
binary:
|
||
source: dist/ja4sentinel-linux-amd64
|
||
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/ja4sentinel
|
||
mode: "0750"
|
||
- path: /etc/ja4sentinel
|
||
mode: "0750"
|
||
maintainer_scripts:
|
||
deb:
|
||
postinst: packaging/deb/postinst
|
||
prerm: packaging/deb/prerm
|
||
postrm: packaging/deb/postrm
|
||
rpm:
|
||
post: packaging/deb/postinst
|
||
preun: packaging/deb/prerm
|
||
postun: packaging/deb/postrm
|
||
dependencies:
|
||
deb:
|
||
- systemd
|
||
- libpcap0.8
|
||
rpm:
|
||
- systemd
|
||
- libpcap >= 1.9.0
|
||
verify:
|
||
deb:
|
||
command: docker run --rm -v $(pwd)/build/deb:/packages debian:latest sh -c "apt-get update && apt-get install -y /packages/*.deb"
|
||
rpm:
|
||
command: docker run --rm -v $(pwd)/build/rpm:/packages rockylinux:8 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/local/bin/ja4sentinel"
|
||
args:
|
||
- "--config"
|
||
- "/etc/ja4sentinel/config.yml"
|
||
user_group:
|
||
user: "ja4sentinel"
|
||
group: "ja4sentinel"
|
||
runtime:
|
||
working_directory: "/var/lib/ja4sentinel"
|
||
pid_file: "/run/ja4sentinel.pid"
|
||
restart: "on-failure"
|
||
restart_sec: 5
|
||
environment_prefix: "JA4SENTINEL_"
|
||
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:
|
||
- "NoNewPrivileges=yes"
|
||
- "ProtectSystem=full"
|
||
- "ProtectHome=true"
|
||
- "PrivateTmp=true"
|
||
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."
|
||
|
||
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 d’objets 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*."
|
||
logrecord_schema:
|
||
# Exemple de mapping pour api.LogRecord (résumé)
|
||
- "src_ip"
|
||
- "src_port"
|
||
- "dst_ip"
|
||
- "dst_port"
|
||
- "ip_meta_ttl"
|
||
- "ip_meta_total_length"
|
||
- "ip_meta_id"
|
||
- "ip_meta_df"
|
||
- "tcp_meta_window_size"
|
||
- "tcp_meta_mss"
|
||
- "tcp_meta_window_scale"
|
||
- "tcp_meta_options" # string joinée, ex: 'MSS,SACK,TS,NOP,WS'
|
||
- "ja4"
|
||
- "ja4_hash"
|
||
- "ja3"
|
||
- "ja3_hash"
|
||
|