Files
ja4sentinel/internal/fingerprint/engine_test.go
toto e9e523d8a2 fix: architecture violations and pre-existing test bugs
- Remove JA4SENTINEL_LOG_LEVEL env override (architecture violation: log_level must be YAML-only)
- Add TestLoadFromEnv_LogLevelIgnored test to verify env var is ignored
- Fix yaml struct tags in api.Config/AppConfig/OutputConfig (yaml.v3 ignores json tags)
- Fix isValidIP/isValidCIDR to use net.ParseIP/net.ParseCIDR for proper validation
- Fix SLL packet parsing: use protoType from SLL header to select IPv4/IPv6 decoder
- Fix TestLoadFromFile_ExcludeSourceIPs: t.Errorf → t.Fatalf to avoid nil dereference
- Fix TestFromClientHello_NilPayload: use strings.HasPrefix for error message check
- Fix TestValidate_ExcludeSourceIPs: add required FlowTimeoutSec/PacketBufferSize defaults

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-05 09:22:29 +01:00

256 lines
6.2 KiB
Go

package fingerprint
import (
"strings"
"testing"
"ja4sentinel/api"
)
func TestFromClientHello(t *testing.T) {
tests := []struct {
name string
ch api.TLSClientHello
wantErr bool
}{
{
name: "empty payload",
ch: api.TLSClientHello{
Payload: []byte{},
},
wantErr: true,
},
{
name: "invalid payload",
ch: api.TLSClientHello{
Payload: []byte{0x00, 0x01, 0x02},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
engine := NewEngine()
_, err := engine.FromClientHello(tt.ch)
if (err != nil) != tt.wantErr {
t.Errorf("FromClientHello() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestNewEngine(t *testing.T) {
engine := NewEngine()
if engine == nil {
t.Error("NewEngine() returned nil")
}
}
func TestFromClientHello_ValidPayload(t *testing.T) {
// Use a minimal valid TLS 1.2 ClientHello with extensions
// Build a proper ClientHello using the same structure as parser tests
clientHello := buildMinimalClientHelloForTest()
ch := api.TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
Payload: clientHello,
}
engine := NewEngine()
fp, err := engine.FromClientHello(ch)
if err != nil {
t.Fatalf("FromClientHello() error = %v", err)
}
if fp == nil {
t.Fatal("FromClientHello() returned nil")
}
// Verify JA4 is populated (format: t13d... or t12d...)
if fp.JA4 == "" {
t.Error("JA4 should not be empty")
}
// JA4Hash is populated for internal use (but not serialized to LogRecord)
// It contains the hash portions of the JA4 string
if fp.JA4Hash == "" {
t.Error("JA4Hash should be populated for internal use")
}
}
// buildMinimalClientHelloForTest creates a minimal valid TLS 1.2 ClientHello
func buildMinimalClientHelloForTest() []byte {
// Cipher suites (minimal set)
cipherSuites := []byte{0x00, 0x04, 0x13, 0x01, 0x13, 0x02, 0xc0, 0x2f}
// Compression methods (null only)
compressionMethods := []byte{0x01, 0x00}
// No extensions
extensions := []byte{}
extLen := len(extensions)
// Build ClientHello handshake body
handshakeBody := []byte{
0x03, 0x03, // Version: TLS 1.2
// Random (32 bytes)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, // Session ID length: 0
}
// Add cipher suites (with length prefix)
cipherSuiteLen := len(cipherSuites)
handshakeBody = append(handshakeBody, byte(cipherSuiteLen>>8), byte(cipherSuiteLen))
handshakeBody = append(handshakeBody, cipherSuites...)
// Add compression methods (with length prefix)
handshakeBody = append(handshakeBody, compressionMethods...)
// Add extensions (with length prefix)
handshakeBody = append(handshakeBody, byte(extLen>>8), byte(extLen))
handshakeBody = append(handshakeBody, extensions...)
// Now build full handshake with type and length
handshakeLen := len(handshakeBody)
handshake := append([]byte{
0x01, // Handshake type: ClientHello
byte(handshakeLen >> 16), byte(handshakeLen >> 8), byte(handshakeLen), // Handshake length
}, handshakeBody...)
// Build TLS record
recordLen := len(handshake)
record := make([]byte, 5+recordLen)
record[0] = 0x16 // Handshake
record[1] = 0x03 // Version: TLS 1.2
record[2] = 0x03
record[3] = byte(recordLen >> 8)
record[4] = byte(recordLen)
copy(record[5:], handshake)
return record
}
// TestExtractJA4Hash tests the extractJA4Hash helper function
func TestExtractJA4Hash(t *testing.T) {
tests := []struct {
name string
ja4 string
want string
}{
{
name: "standard_ja4_format",
ja4: "t13d1516h2_8daaf6152771_02cb136f2775",
want: "8daaf6152771_02cb136f2775",
},
{
name: "ja4_with_single_underscore",
ja4: "t12d1234h1_abcdef123456",
want: "abcdef123456",
},
{
name: "ja4_no_underscore_returns_empty",
ja4: "t13d1516h2",
want: "",
},
{
name: "empty_ja4_returns_empty",
ja4: "",
want: "",
},
{
name: "underscore_at_start",
ja4: "_hash1_hash2",
want: "hash1_hash2",
},
{
name: "multiple_underscores_returns_after_first",
ja4: "base_part1_part2_part3",
want: "part1_part2_part3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractJA4Hash(tt.ja4)
if got != tt.want {
t.Errorf("extractJA4Hash(%q) = %q, want %q", tt.ja4, got, tt.want)
}
})
}
}
// TestFromClientHello_NilPayload tests error handling for nil payload
func TestFromClientHello_NilPayload(t *testing.T) {
engine := NewEngine()
ch := api.TLSClientHello{
Payload: nil,
}
_, err := engine.FromClientHello(ch)
if err == nil {
t.Error("FromClientHello() with nil payload should return error")
}
if !strings.HasPrefix(err.Error(), "empty ClientHello payload") {
t.Errorf("FromClientHello() error = %v, should start with 'empty ClientHello payload'", err)
}
}
// TestFromClientHello_JA3Hash tests that JA3Hash is correctly populated
func TestFromClientHello_JA3Hash(t *testing.T) {
clientHello := buildMinimalClientHelloForTest()
ch := api.TLSClientHello{
Payload: clientHello,
}
engine := NewEngine()
fp, err := engine.FromClientHello(ch)
if err != nil {
t.Fatalf("FromClientHello() error = %v", err)
}
// JA3Hash should be populated (MD5 hash of JA3 string)
if fp.JA3Hash == "" {
t.Error("JA3Hash should be populated")
}
// JA3 should also be populated
if fp.JA3 == "" {
t.Error("JA3 should be populated")
}
}
// TestFromClientHello_EmptyJA4Hash tests behavior when JA4 has no underscore
func TestFromClientHello_EmptyJA4Hash(t *testing.T) {
// This test verifies that even if JA4 format changes, the code handles it gracefully
engine := NewEngine()
// Use a valid ClientHello - the library should produce a proper JA4
clientHello := buildMinimalClientHelloForTest()
ch := api.TLSClientHello{
Payload: clientHello,
}
fp, err := engine.FromClientHello(ch)
if err != nil {
t.Fatalf("FromClientHello() error = %v", err)
}
// JA4 should always be populated
if fp.JA4 == "" {
t.Error("JA4 should be populated")
}
// JA4Hash may be empty if the JA4 format doesn't include underscores
// This is acceptable behavior
}