package checker import ( "context" "crypto/tls" "encoding/json" "strings" "testing" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // stubObs is a minimal ObservationGetter that serves a pre-built TLSData // payload and ignores related lookups. It is local to this file rather than // promoted to a shared helper to keep the rule tests self-contained. type stubObs struct{ data TLSData } func (s stubObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error { if key != ObservationKeyTLSProbes { return nil } raw, _ := json.Marshal(s.data) return json.Unmarshal(raw, dest) } func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return nil, nil } func newProbeWithEnum(versions ...EnumVersion) TLSProbe { return TLSProbe{ Host: "example.test", Port: 443, Endpoint: "example.test:443", Type: "tls", TLSHandshakeOK: true, TLSVersionNum: tls.VersionTLS13, Enum: &TLSEnumeration{Versions: versions}, } } func TestVersionEnumerationRule_Skipped_NoEnum(t *testing.T) { obs := stubObs{data: TLSData{ Probes: map[string]TLSProbe{"a": {Host: "x", Port: 443, Endpoint: "x:443", Type: "tls", TLSHandshakeOK: true}}, CollectedAt: time.Now(), }} got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil) if len(got) != 1 || got[0].Code != "tls.enum.versions.skipped" { t.Fatalf("want a single skipped state, got %+v", got) } } func TestVersionEnumerationRule_OK_OnlyModern(t *testing.T) { obs := stubObs{data: TLSData{ Probes: map[string]TLSProbe{ "a": newProbeWithEnum( EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2"}, EnumVersion{Version: tls.VersionTLS13, Name: "TLS 1.3"}, ), }, }} got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil) if len(got) != 1 || got[0].Status != sdk.StatusOK || got[0].Code != "tls.enum.versions.ok" { t.Fatalf("want a single OK state, got %+v", got) } } func TestVersionEnumerationRule_LegacyAccepted(t *testing.T) { obs := stubObs{data: TLSData{ Probes: map[string]TLSProbe{ "a": newProbeWithEnum( EnumVersion{Version: tls.VersionTLS10, Name: "TLS 1.0"}, EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2"}, ), }, }} got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil) if len(got) != 1 || got[0].Status != sdk.StatusWarn || got[0].Code != "tls.enum.versions.legacy_accepted" { t.Fatalf("want a single warn state, got %+v", got) } if !strings.Contains(got[0].Message, "TLS 1.0") { t.Fatalf("warn message should mention the legacy version, got %q", got[0].Message) } } func TestClassifyCipher(t *testing.T) { cases := map[string]string{ "TLS_RSA_WITH_NULL_SHA": "NULL", "TLS_DH_anon_WITH_AES_128_CBC_SHA": "anonymous", "TLS_RSA_EXPORT_WITH_RC4_40_MD5": "EXPORT", "TLS_ECDHE_RSA_WITH_RC4_128_SHA": "RC4", "TLS_RSA_WITH_3DES_EDE_CBC_SHA": "3DES/DES", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": "", "TLS_AES_256_GCM_SHA384": "", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": "", } for name, want := range cases { if got := classifyCipher(name); got != want { t.Errorf("classifyCipher(%q) = %q, want %q", name, got, want) } } } func TestWeakCipherRule_Detects(t *testing.T) { obs := stubObs{data: TLSData{ Probes: map[string]TLSProbe{ "a": newProbeWithEnum( EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2", Ciphers: []EnumCipher{ {ID: 0xC02F, Name: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, {ID: 0x000A, Name: "TLS_RSA_WITH_3DES_EDE_CBC_SHA"}, {ID: 0x0005, Name: "TLS_RSA_WITH_RC4_128_SHA"}, }}, ), }, }} got := (&weakCipherRule{}).Evaluate(context.Background(), obs, nil) if len(got) != 1 || got[0].Status != sdk.StatusWarn || got[0].Code != "tls.enum.ciphers.weak_accepted" { t.Fatalf("want a single weak warn state, got %+v", got) } if !strings.Contains(got[0].Message, "RC4") || !strings.Contains(got[0].Message, "3DES/DES") { t.Fatalf("warn message should list the broken categories, got %q", got[0].Message) } } func TestWeakCipherRule_OK_OnlyModern(t *testing.T) { obs := stubObs{data: TLSData{ Probes: map[string]TLSProbe{ "a": newProbeWithEnum( EnumVersion{Version: tls.VersionTLS13, Name: "TLS 1.3", Ciphers: []EnumCipher{ {ID: 0x1301, Name: "TLS_AES_128_GCM_SHA256"}, }}, ), }, }} got := (&weakCipherRule{}).Evaluate(context.Background(), obs, nil) if len(got) != 1 || got[0].Status != sdk.StatusOK || got[0].Code != "tls.enum.ciphers.ok" { t.Fatalf("want a single OK state, got %+v", got) } }