feat: implémentation complète du pipeline JA4 + Docker + tests
Nouveaux modules: - cmd/ja4sentinel/main.go : point d'entrée avec pipeline capture→parse→fingerprint→output - internal/config/loader.go : chargement YAML + env (JA4SENTINEL_*) + validation - internal/tlsparse/parser.go : extraction ClientHello avec suivi d'état de flux (NEW/WAIT_CLIENT_HELLO/JA4_DONE) - internal/fingerprint/engine.go : génération JA4/JA3 via psanford/tlsfingerprint - internal/output/writers.go : StdoutWriter, FileWriter, UnixSocketWriter, MultiWriter Infrastructure: - Dockerfile (multi-stage), Dockerfile.dev, Dockerfile.test-server - Makefile (build, test, lint, docker-build-*) - docker-compose.test.yml pour tests d'intégration - README.md (276 lignes) avec architecture, config, exemples API (api/types.go): - Ajout Close() aux interfaces Capture et Parser - Ajout FlowTimeoutSec dans Config (défaut: 30s, env: JA4SENTINEL_FLOW_TIMEOUT) - ServiceLog: +Timestamp, +TraceID, +ConnID - LogRecord: champs flatten (ip_meta_*, tcp_meta_*, ja4*) - Helper NewLogRecord() pour conversion TLSClientHello+Fingerprints→LogRecord Architecture (architecture.yml): - Documentation module logging + interfaces LoggerFactory/Logger - Section service.systemd complète (unit, security, capabilities) - Section logging.strategy (JSON lines, champs, règles) - api.Config: +FlowTimeoutSec documenté Fixes/cleanup: - Suppression internal/api/types.go (consolidé dans api/types.go) - Correction imports logging (ja4sentinel/api) - .dockerignore / .gitignore - config.yml.example Tests: - Tous les modules ont leurs tests (*_test.go) - Tests unitaires : capture, config, fingerprint, output, tlsparse - Tests d'intégration via docker-compose.test.yml Build: - Binaires dans dist/ (make build → dist/ja4sentinel) - Docker runtime avec COPY --from=builder /app/dist/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
250
internal/output/writers.go
Normal file
250
internal/output/writers.go
Normal file
@ -0,0 +1,250 @@
|
||||
// Package output provides writers for ja4sentinel log records
|
||||
package output
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"ja4sentinel/api"
|
||||
)
|
||||
|
||||
// StdoutWriter writes log records to stdout
|
||||
type StdoutWriter struct {
|
||||
encoder *json.Encoder
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewStdoutWriter creates a new stdout writer
|
||||
func NewStdoutWriter() *StdoutWriter {
|
||||
return &StdoutWriter{
|
||||
encoder: json.NewEncoder(os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes a log record to stdout
|
||||
func (w *StdoutWriter) Write(rec api.LogRecord) error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
return w.encoder.Encode(rec)
|
||||
}
|
||||
|
||||
// Close closes the writer (no-op for stdout)
|
||||
func (w *StdoutWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileWriter writes log records to a file
|
||||
type FileWriter struct {
|
||||
file *os.File
|
||||
encoder *json.Encoder
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewFileWriter creates a new file writer
|
||||
func NewFileWriter(path string) (*FileWriter, error) {
|
||||
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return &FileWriter{
|
||||
file: file,
|
||||
encoder: json.NewEncoder(file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Write writes a log record to the file
|
||||
func (w *FileWriter) Write(rec api.LogRecord) error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
return w.encoder.Encode(rec)
|
||||
}
|
||||
|
||||
// Close closes the file
|
||||
func (w *FileWriter) Close() error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
if w.file != nil {
|
||||
return w.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnixSocketWriter writes log records to a UNIX socket
|
||||
type UnixSocketWriter struct {
|
||||
socketPath string
|
||||
conn net.Conn
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewUnixSocketWriter creates a new UNIX socket writer
|
||||
func NewUnixSocketWriter(socketPath string) (*UnixSocketWriter, error) {
|
||||
w := &UnixSocketWriter{
|
||||
socketPath: socketPath,
|
||||
}
|
||||
|
||||
// Try to connect (socket may not exist yet)
|
||||
conn, err := net.Dial("unix", socketPath)
|
||||
if err != nil {
|
||||
// Socket doesn't exist yet, we'll try to connect on first write
|
||||
return w, nil
|
||||
}
|
||||
|
||||
w.conn = conn
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write writes a log record to the UNIX socket
|
||||
func (w *UnixSocketWriter) Write(rec api.LogRecord) error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
// Connect if not already connected
|
||||
if w.conn == nil {
|
||||
conn, err := net.Dial("unix", w.socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to socket %s: %w", w.socketPath, err)
|
||||
}
|
||||
w.conn = conn
|
||||
}
|
||||
|
||||
data, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal record: %w", err)
|
||||
}
|
||||
|
||||
// Add newline for line-based protocols
|
||||
data = append(data, '\n')
|
||||
|
||||
_, err = w.conn.Write(data)
|
||||
if err != nil {
|
||||
// Connection failed, try to reconnect
|
||||
w.conn.Close()
|
||||
w.conn = nil
|
||||
return fmt.Errorf("failed to write to socket: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the UNIX socket connection
|
||||
func (w *UnixSocketWriter) Close() error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
if w.conn != nil {
|
||||
return w.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiWriter combines multiple writers
|
||||
type MultiWriter struct {
|
||||
writers []api.Writer
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewMultiWriter creates a new multi-writer
|
||||
func NewMultiWriter() *MultiWriter {
|
||||
return &MultiWriter{
|
||||
writers: make([]api.Writer, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes a log record to all writers
|
||||
func (mw *MultiWriter) Write(rec api.LogRecord) error {
|
||||
mw.mutex.Lock()
|
||||
defer mw.mutex.Unlock()
|
||||
|
||||
var lastErr error
|
||||
for _, w := range mw.writers {
|
||||
if err := w.Write(rec); err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Add adds a writer to the multi-writer
|
||||
func (mw *MultiWriter) Add(writer api.Writer) {
|
||||
mw.mutex.Lock()
|
||||
defer mw.mutex.Unlock()
|
||||
mw.writers = append(mw.writers, writer)
|
||||
}
|
||||
|
||||
// CloseAll closes all writers
|
||||
func (mw *MultiWriter) CloseAll() error {
|
||||
mw.mutex.Lock()
|
||||
defer mw.mutex.Unlock()
|
||||
|
||||
var lastErr error
|
||||
for _, w := range mw.writers {
|
||||
if closer, ok := w.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// BuilderImpl implements the api.Builder interface
|
||||
type BuilderImpl struct{}
|
||||
|
||||
// NewBuilder creates a new output builder
|
||||
func NewBuilder() *BuilderImpl {
|
||||
return &BuilderImpl{}
|
||||
}
|
||||
|
||||
// NewFromConfig constructs writers from AppConfig
|
||||
func (b *BuilderImpl) NewFromConfig(cfg api.AppConfig) (api.Writer, error) {
|
||||
multiWriter := NewMultiWriter()
|
||||
|
||||
for _, outputCfg := range cfg.Outputs {
|
||||
if !outputCfg.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
var writer api.Writer
|
||||
var err error
|
||||
|
||||
switch outputCfg.Type {
|
||||
case "stdout":
|
||||
writer = NewStdoutWriter()
|
||||
case "file":
|
||||
path := outputCfg.Params["path"]
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("file output requires 'path' parameter")
|
||||
}
|
||||
writer, err = NewFileWriter(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "unix_socket":
|
||||
socketPath := outputCfg.Params["socket_path"]
|
||||
if socketPath == "" {
|
||||
return nil, fmt.Errorf("unix_socket output requires 'socket_path' parameter")
|
||||
}
|
||||
writer, err = NewUnixSocketWriter(socketPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown output type: %s", outputCfg.Type)
|
||||
}
|
||||
|
||||
multiWriter.Add(writer)
|
||||
}
|
||||
|
||||
// If no outputs configured, default to stdout
|
||||
if len(multiWriter.writers) == 0 {
|
||||
multiWriter.Add(NewStdoutWriter())
|
||||
}
|
||||
|
||||
return multiWriter, nil
|
||||
}
|
||||
Reference in New Issue
Block a user