mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
test: 补充单元测试覆盖率 29.9% → 36.6%
新建 18 个测试文件,追加 30 个已有测试文件,覆盖协议解析、 错误分类、CEL 表达式求值、YAML 反序列化、字节编码等纯函数。
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
//go:build plugin_activemq || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyActiveMQErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyActiveMQErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyActiveMQErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package services
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -47,3 +48,74 @@ func TestValidateCQLQueryResponseRejectsErrors(t *testing.T) {
|
||||
t.Fatal("validateCQLQueryResponse() error = nil, want unexpected opcode error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyCassandraErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"auth error", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"bad credentials", errors.New("bad credentials"), ErrorTypeAuth},
|
||||
{"network error", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random cassandra error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyCassandraErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyCassandraErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCqlShortString(t *testing.T) {
|
||||
got := cqlShortString("AB")
|
||||
if len(got) != 4 || binary.BigEndian.Uint16(got[:2]) != 2 || string(got[2:]) != "AB" {
|
||||
t.Errorf("cqlShortString(AB) = %v", got)
|
||||
}
|
||||
empty := cqlShortString("")
|
||||
if len(empty) != 2 || binary.BigEndian.Uint16(empty) != 0 {
|
||||
t.Errorf("cqlShortString empty = %v", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCqlLongString(t *testing.T) {
|
||||
got := cqlLongString("XYZ")
|
||||
if len(got) != 7 || binary.BigEndian.Uint32(got[:4]) != 3 || string(got[4:]) != "XYZ" {
|
||||
t.Errorf("cqlLongString(XYZ) = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCqlStringMap(t *testing.T) {
|
||||
m := map[string]string{"k": "v"}
|
||||
got := cqlStringMap(m)
|
||||
if got[0] != 0x00 || got[1] != 0x01 {
|
||||
t.Errorf("count bytes wrong: %v", got[:2])
|
||||
}
|
||||
if !bytes.Contains(got, []byte("k")) || !bytes.Contains(got, []byte("v")) {
|
||||
t.Errorf("missing key/value in %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractClusterName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{"empty", nil, "unknown"},
|
||||
{"short", []byte{0x01, 0x02}, "unknown"},
|
||||
{"printable", append([]byte{0x00, 0x00, 0x00, 0x01}, []byte("TestCluster")...), "TestCluster"},
|
||||
{"binary prefix", append([]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02}, []byte("MyCluster")...), "MyCluster"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractClusterName(tt.data)
|
||||
if !strings.Contains(got, tt.want) && got != tt.want {
|
||||
t.Errorf("extractClusterName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//go:build plugin_findnet || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- hexUnicodeToString ---
|
||||
|
||||
func TestHexUnicodeToString(t *testing.T) {
|
||||
p := NewFindNetPlugin()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
src string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
src: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "UTF-16LE TEST",
|
||||
// T=0x54 E=0x45 S=0x53 T=0x54, LE pairs: 5400 4500 5300 5400
|
||||
src: "54004500530054",
|
||||
want: "TEST",
|
||||
},
|
||||
{
|
||||
name: "odd length gets padded to 4-multiple",
|
||||
// 奇数长度补0至4的倍数:"540045005300540" → "5400450053005400" → "TEST"
|
||||
src: "540045005300540", // 15 hex chars
|
||||
want: "TEST",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.hexUnicodeToString(tc.src)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- isValidHostname ---
|
||||
|
||||
func TestIsValidHostname(t *testing.T) {
|
||||
p := NewFindNetPlugin()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{name: "empty", input: "", want: false},
|
||||
{name: "valid hostname", input: "test-pc", want: true},
|
||||
{name: "single char", input: "a", want: false}, // regex requires at least 2 chars (start+middle+end)
|
||||
{name: "too long", input: strings.Repeat("a", 256), want: false},
|
||||
{name: "valid alphanumeric", input: "PC01", want: true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.isValidHostname(tc.input)
|
||||
if got != tc.want {
|
||||
t.Errorf("isValidHostname(%q) = %v, want %v", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- isValidNetworkAddress ---
|
||||
|
||||
func TestIsValidNetworkAddress(t *testing.T) {
|
||||
p := NewFindNetPlugin()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want bool
|
||||
}{
|
||||
{name: "IPv4", input: "192.168.1.1", want: true},
|
||||
{name: "IPv6 loopback", input: "::1", want: true},
|
||||
{name: "valid hostname fallback", input: "test-host", want: true},
|
||||
{name: "invalid", input: "not_an_ip!!!", want: false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.isValidNetworkAddress(tc.input)
|
||||
if got != tc.want {
|
||||
t.Errorf("isValidNetworkAddress(%q) = %v, want %v", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- cleanAndValidateAddress ---
|
||||
|
||||
func TestCleanAndValidateAddress(t *testing.T) {
|
||||
p := NewFindNetPlugin()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "valid IPv4 bytes",
|
||||
data: []byte("192.168.1.100"),
|
||||
want: "192.168.1.100",
|
||||
},
|
||||
{
|
||||
name: "bytes with unprintable chars around valid IP",
|
||||
data: append([]byte{0x00, 0x01}, append([]byte("10.0.0.1"), 0x00)...),
|
||||
want: "10.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "invalid data returns empty",
|
||||
data: []byte{0x00, 0x01, 0x02, 0x03},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.cleanAndValidateAddress(tc.data)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- NetworkInfo.Summary ---
|
||||
|
||||
func TestNetworkInfoSummary(t *testing.T) {
|
||||
t.Run("invalid returns discovery failed text", func(t *testing.T) {
|
||||
ni := &NetworkInfo{Valid: false}
|
||||
got := ni.Summary()
|
||||
if got == "" {
|
||||
t.Error("expected non-empty text for invalid NetworkInfo")
|
||||
}
|
||||
// 内容是 i18n key,只验证非空即可
|
||||
})
|
||||
|
||||
t.Run("valid with hostname and IPv4", func(t *testing.T) {
|
||||
ni := &NetworkInfo{
|
||||
Valid: true,
|
||||
Hostname: "PC01",
|
||||
IPv4Addrs: []string{"192.168.1.1", "10.0.0.1"},
|
||||
}
|
||||
got := ni.Summary()
|
||||
if got == "" {
|
||||
t.Error("expected non-empty summary")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- parseNetworkInfo ---
|
||||
|
||||
func TestParseNetworkInfo(t *testing.T) {
|
||||
p := NewFindNetPlugin()
|
||||
|
||||
t.Run("empty data returns invalid", func(t *testing.T) {
|
||||
info := p.parseNetworkInfo([]byte{})
|
||||
if info.Valid {
|
||||
t.Error("expected Valid=false for empty data")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("data without valid hostname or IP returns invalid", func(t *testing.T) {
|
||||
// 全零数据,hostname 解析出空字符串,不会 Valid
|
||||
info := p.parseNetworkInfo(make([]byte, 64))
|
||||
if info.Valid {
|
||||
t.Error("expected Valid=false for zero data")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build plugin_ftp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyFTPErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"530 login incorrect", errors.New("530 login incorrect"), ErrorTypeAuth},
|
||||
{"530 not logged in", errors.New("530 not logged in"), ErrorTypeAuth},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"too many connections", errors.New("421 there are too many connections"), ErrorTypeNetwork},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"random error", errors.New("random error"), ErrorTypeUnknown},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyFTPErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyFTPErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package services
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
@@ -61,3 +62,25 @@ func TestKafkaRecvRejectsShortResponse(t *testing.T) {
|
||||
t.Fatal("kafkaRecv() error = nil, want invalid length error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyKafkaErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"sasl auth failed", errors.New("sasl authentication failed"), ErrorTypeAuth},
|
||||
{"unauthorized", errors.New("unauthorized"), ErrorTypeAuth},
|
||||
{"broker not available", errors.New("broker not available"), ErrorTypeNetwork},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random kafka error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyKafkaErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyKafkaErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -28,3 +29,25 @@ func TestLDAPDNFormatsEscapeUsernameValue(t *testing.T) {
|
||||
t.Fatalf("escaped DN = %q", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyLDAPErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"invalid credentials", errors.New("invalid credentials"), ErrorTypeAuth},
|
||||
{"bind failed", errors.New("bind failed"), ErrorTypeAuth},
|
||||
{"ldap connection lost", errors.New("ldap: connection lost"), ErrorTypeNetwork},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random ldap error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyLDAPErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyLDAPErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -128,3 +129,25 @@ func TestBuildMongoSCRAMClientFinalBuildsProof(t *testing.T) {
|
||||
t.Fatalf("client final = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyMongoDBErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"bad auth", errors.New("bad auth"), ErrorTypeAuth},
|
||||
{"dial tcp", errors.New("dial tcp connection refused"), ErrorTypeNetwork},
|
||||
{"eof", errors.New("eof"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random mongodb error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyMongoDBErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyMongoDBErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build plugin_mssql || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyMSSQLErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"login failed", errors.New("login failed"), ErrorTypeAuth},
|
||||
{"account locked", errors.New("account locked"), ErrorTypeAuth},
|
||||
{"context deadline exceeded", errors.New("context deadline exceeded"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyMSSQLErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyMSSQLErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -85,3 +86,51 @@ func TestMySQLConnStringRejectsUnsupportedUsernameDelimiters(t *testing.T) {
|
||||
t.Fatal("mySQLConnString() error = nil, want unsupported delimiter error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLConnStringRejectsAtSign(t *testing.T) {
|
||||
info := &common.HostInfo{Host: "127.0.0.1", Port: 3306}
|
||||
if _, err := mySQLConnString("user@host", "pass", info, time.Second); err == nil {
|
||||
t.Fatal("mySQLConnString() error = nil, want unsupported delimiter error for @")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLConnStringRejectsSlash(t *testing.T) {
|
||||
info := &common.HostInfo{Host: "127.0.0.1", Port: 3306}
|
||||
if _, err := mySQLConnString("user/name", "pass", info, time.Second); err == nil {
|
||||
t.Fatal("mySQLConnString() error = nil, want unsupported delimiter error for /")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLConnStringValidUser(t *testing.T) {
|
||||
info := &common.HostInfo{Host: "127.0.0.1", Port: 3306}
|
||||
dsn, err := mySQLConnString("root", "password", info, 3*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("mySQLConnString() error = %v", err)
|
||||
}
|
||||
if dsn == "" {
|
||||
t.Fatal("mySQLConnString() returned empty DSN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyMySQLErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"access denied for user", errors.New("access denied for user"), ErrorTypeAuth},
|
||||
{"host is not allowed", errors.New("host is not allowed"), ErrorTypeAuth},
|
||||
{"too many connections", errors.New("too many connections"), ErrorTypeNetwork},
|
||||
{"can't connect to mysql server", errors.New("can't connect to mysql server"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyMySQLErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyMySQLErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -32,3 +33,24 @@ func TestNeo4jUnauthorizedRequiresNeo4jBody(t *testing.T) {
|
||||
t.Fatalf("testUnauthorizedAccess reported generic 200 as Neo4j: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyNeo4jErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"401 unauthorized", errors.New("401 unauthorized"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random neo4j error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyNeo4jErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyNeo4jErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,3 +38,192 @@ func appendNTLMAVPair(dst []byte, id uint16, value string) []byte {
|
||||
}
|
||||
return append(dst, buf...)
|
||||
}
|
||||
|
||||
// --- NetBIOSInfo.Summary ---
|
||||
|
||||
func TestNetBIOSInfoSummary(t *testing.T) {
|
||||
p := NewNetBIOSPlugin()
|
||||
_ = p // 仅用于确认插件可实例化,Summary 是值方法
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
info NetBIOSInfo
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "invalid returns empty",
|
||||
info: NetBIOSInfo{Valid: false},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "computer + domain no dot",
|
||||
info: NetBIOSInfo{Valid: true, ComputerName: "PC01", DomainName: "CORP"},
|
||||
want: "CORP\\PC01",
|
||||
},
|
||||
{
|
||||
name: "computer with dot ignores domain prefix",
|
||||
info: NetBIOSInfo{Valid: true, ComputerName: "pc01.corp.local", DomainName: "CORP"},
|
||||
want: "pc01.corp.local",
|
||||
},
|
||||
{
|
||||
name: "no computer uses server service + domain",
|
||||
info: NetBIOSInfo{Valid: true, ServerService: "SRV01", DomainName: "CORP"},
|
||||
want: "CORP\\SRV01",
|
||||
},
|
||||
{
|
||||
name: "no computer uses workstation + netbios domain",
|
||||
info: NetBIOSInfo{Valid: true, WorkstationService: "WKS01", NetBIOSDomainName: "WORKGROUP"},
|
||||
want: "WORKGROUP\\WKS01",
|
||||
},
|
||||
{
|
||||
name: "domain controller prefix",
|
||||
info: NetBIOSInfo{Valid: true, ComputerName: "DC1", DomainName: "CORP", DomainControllers: "CORP"},
|
||||
want: "DC:CORP\\DC1",
|
||||
},
|
||||
{
|
||||
name: "os version appended",
|
||||
info: NetBIOSInfo{Valid: true, ComputerName: "PC01", DomainName: "CORP", OSVersion: "Windows 10"},
|
||||
want: "CORP\\PC01 Windows 10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.info.Summary()
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- parseNetBIOSNames ---
|
||||
|
||||
func TestParseNetBIOSNames(t *testing.T) {
|
||||
p := &NetBIOSPlugin{}
|
||||
|
||||
t.Run("data too short", func(t *testing.T) {
|
||||
_, err := p.parseNetBIOSNames(make([]byte, 40))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for short data")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("numNames zero", func(t *testing.T) {
|
||||
data := make([]byte, 57) // index 56 = 0
|
||||
_, err := p.parseNetBIOSNames(data)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for zero numNames")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parses workstation and domain records", func(t *testing.T) {
|
||||
header := make([]byte, 57)
|
||||
header[56] = 2 // 2 records
|
||||
|
||||
// Record 1: WorkstationService — flagByte=0x00, nameFlags=0x04 (unique, <128)
|
||||
rec1 := make([]byte, 18)
|
||||
copy(rec1, []byte("TESTPC ")) // 15 bytes
|
||||
rec1[15] = 0x00 // flagByte = WorkstationService
|
||||
rec1[16] = 0x04 // nameFlags unique
|
||||
rec1[17] = 0x00
|
||||
|
||||
// Record 2: DomainName — flagByte=0x00, nameFlags=0x84 (group, >=128)
|
||||
rec2 := make([]byte, 18)
|
||||
copy(rec2, []byte("WORKGROUP ")) // 15 bytes
|
||||
rec2[15] = 0x00 // flagByte = DomainName for group
|
||||
rec2[16] = 0x84 // nameFlags group
|
||||
rec2[17] = 0x00
|
||||
|
||||
data := append(header, rec1...)
|
||||
data = append(data, rec2...)
|
||||
|
||||
info, err := p.parseNetBIOSNames(data)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !info.Valid {
|
||||
t.Fatal("expected Valid=true")
|
||||
}
|
||||
if info.WorkstationService != "TESTPC" {
|
||||
t.Errorf("WorkstationService = %q, want TESTPC", info.WorkstationService)
|
||||
}
|
||||
if info.DomainName != "WORKGROUP" {
|
||||
t.Errorf("DomainName = %q, want WORKGROUP", info.DomainName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- cleanOSString ---
|
||||
|
||||
func TestCleanOSString(t *testing.T) {
|
||||
p := &NetBIOSPlugin{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
data: []byte{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "plain ascii",
|
||||
data: []byte("Windows Server 2019"),
|
||||
want: "Windows Server 2019",
|
||||
},
|
||||
{
|
||||
name: "double null splits sections, first is returned",
|
||||
data: append([]byte("Windows 10\x00\x00"), []byte("Service Pack 1")...),
|
||||
want: "Windows 10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.cleanOSString(tc.data)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- parseUnicodeString (NetBIOSPlugin) ---
|
||||
|
||||
func TestNetBIOSParseUnicodeString(t *testing.T) {
|
||||
p := &NetBIOSPlugin{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
data: []byte{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "odd length returns empty",
|
||||
data: []byte{0x41},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "UTF-16LE AB",
|
||||
data: []byte{0x41, 0x00, 0x42, 0x00},
|
||||
want: "AB",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := p.parseUnicodeString(tc.data)
|
||||
if got != tc.want {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
//go:build plugin_oracle || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyOracleErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"ORA-01017 invalid username/password", errors.New("ORA-01017: invalid username/password"), ErrorTypeAuth},
|
||||
{"TNS-12541 no listener", errors.New("TNS-12541 no listener"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyOracleErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyOracleErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -30,3 +31,25 @@ func TestPostgreSQLVulnInfoTruncatesByRune(t *testing.T) {
|
||||
t.Fatalf("postgresql truncation helper = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyPostgreSQLErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"password authentication failed", errors.New("password authentication failed"), ErrorTypeAuth},
|
||||
{"pq role", errors.New("pq: role \"foo\" does not exist"), ErrorTypeAuth},
|
||||
{"dial tcp", errors.New("dial tcp connection refused"), ErrorTypeNetwork},
|
||||
{"eof", errors.New("eof"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random pg error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyPostgreSQLErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyPostgreSQLErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package services
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -60,3 +61,24 @@ func (r *chunkedByteReader) Read(p []byte) (int, error) {
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func TestClassifyRabbitMQErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"401 unauthorized", errors.New("401 unauthorized"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random rabbitmq error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyRabbitMQErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyRabbitMQErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build plugin_rdp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
func TestBuildBanner(t *testing.T) {
|
||||
p := &RDPPlugin{}
|
||||
fallback := i18n.GetText("rdp_remote_desktop_service")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
osInfo map[string]any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil map",
|
||||
osInfo: nil,
|
||||
want: fallback,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
osInfo: map[string]any{},
|
||||
want: fallback,
|
||||
},
|
||||
{
|
||||
name: "OsVerion and NetBIOSComputerName",
|
||||
osInfo: map[string]any{"OsVerion": "Windows 10", "NetBIOSComputerName": "DESKTOP-01"},
|
||||
want: "RDP (Windows 10, DESKTOP-01)",
|
||||
},
|
||||
{
|
||||
name: "only OsVerion",
|
||||
osInfo: map[string]any{"OsVerion": "Windows Server 2019"},
|
||||
want: "RDP (Windows Server 2019)",
|
||||
},
|
||||
{
|
||||
name: "only NetBIOSComputerName",
|
||||
osInfo: map[string]any{"NetBIOSComputerName": "MY-HOST"},
|
||||
want: "RDP (Hostname:MY-HOST)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.buildBanner(tt.osInfo)
|
||||
if got != tt.want {
|
||||
t.Errorf("buildBanner() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractStringField(t *testing.T) {
|
||||
p := &RDPPlugin{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
osInfo map[string]any
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "key exists and is string",
|
||||
osInfo: map[string]any{"foo": "bar"},
|
||||
key: "foo",
|
||||
want: "bar",
|
||||
},
|
||||
{
|
||||
name: "key exists but not string",
|
||||
osInfo: map[string]any{"foo": 42},
|
||||
key: "foo",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "key does not exist",
|
||||
osInfo: map[string]any{"foo": "bar"},
|
||||
key: "missing",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "nil map",
|
||||
osInfo: nil,
|
||||
key: "foo",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.extractStringField(tt.osInfo, tt.key)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractStringField(%q) = %q, want %q", tt.key, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -32,3 +33,24 @@ func (c *redisReplyTestConn) RemoteAddr() net.Addr { return nil }
|
||||
func (c *redisReplyTestConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *redisReplyTestConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *redisReplyTestConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
func TestClassifyRedisErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"wrongpass", errors.New("wrongpass invalid password"), ErrorTypeAuth},
|
||||
{"noauth", errors.New("noauth authentication required"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random redis error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyRedisErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyRedisErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
@@ -37,3 +38,24 @@ func TestReadRsyncLineHandlesChunkedReads(t *testing.T) {
|
||||
t.Fatalf("readRsyncLine() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyRsyncErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"access denied", errors.New("access denied"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random rsync error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyRsyncErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyRsyncErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
@@ -49,3 +50,466 @@ func TestReadSMBMessageHandlesChunkedReads(t *testing.T) {
|
||||
t.Fatalf("readSMBMessage() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- parseUnicodeString ----
|
||||
|
||||
func TestParseUnicodeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{"empty", []byte{}, ""},
|
||||
{"odd length", []byte{0x41}, ""},
|
||||
{"null terminated", []byte{0x41, 0x00, 0x00, 0x00}, "A"},
|
||||
{"ascii", []byte{0x41, 0x00, 0x42, 0x00, 0x43, 0x00}, "ABC"},
|
||||
{"chinese", []byte{0x2d, 0x4e, 0x87, 0x65}, "中文"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseUnicodeString(tt.data); got != tt.want {
|
||||
t.Errorf("parseUnicodeString() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---- bytesToUint16 / bytesToUint32 ----
|
||||
|
||||
func TestBytesToUint16(t *testing.T) {
|
||||
if got := bytesToUint16([]byte{}); got != 0 {
|
||||
t.Errorf("short data: got %d", got)
|
||||
}
|
||||
if got := bytesToUint16([]byte{0x01}); got != 0 {
|
||||
t.Errorf("single byte: got %d", got)
|
||||
}
|
||||
if got := bytesToUint16([]byte{0x34, 0x12}); got != 0x1234 {
|
||||
t.Errorf("LE decode: got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToUint32(t *testing.T) {
|
||||
if got := bytesToUint32([]byte{}); got != 0 {
|
||||
t.Errorf("empty: got %d", got)
|
||||
}
|
||||
if got := bytesToUint32([]byte{0x01, 0x02, 0x03}); got != 0 {
|
||||
t.Errorf("short: got %d", got)
|
||||
}
|
||||
if got := bytesToUint32([]byte{0x78, 0x56, 0x34, 0x12}); got != 0x12345678 {
|
||||
t.Errorf("LE decode: got 0x%08x", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- trimSMBString ----
|
||||
|
||||
func TestTrimSMBString(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"hello\x00", "hello"},
|
||||
{"\x00hello\x00", "hello"},
|
||||
{" hello ", "hello"},
|
||||
{"\x00", ""},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := trimSMBString(tt.input); got != tt.want {
|
||||
t.Errorf("trimSMBString(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- parseNTLMFlags ----
|
||||
|
||||
func TestParseNTLMFlags(t *testing.T) {
|
||||
// 无标志
|
||||
if got := parseNTLMFlags(0); len(got) != 0 {
|
||||
t.Errorf("zero flags: want empty, got %v", got)
|
||||
}
|
||||
|
||||
// 单标志 NEGOTIATE_UNICODE
|
||||
flags := parseNTLMFlags(0x00000001)
|
||||
if len(flags) != 1 || flags[0] != "NEGOTIATE_UNICODE" {
|
||||
t.Errorf("single flag: got %v", flags)
|
||||
}
|
||||
|
||||
// 多标志 NEGOTIATE_OEM | NEGOTIATE_NTLM
|
||||
multi := parseNTLMFlags(0x00000002 | 0x00000200)
|
||||
if len(multi) != 2 {
|
||||
t.Errorf("multi flags: want 2, got %d: %v", len(multi), multi)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- parseOSVersion ----
|
||||
|
||||
func TestParseOSVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
check func(s string) bool
|
||||
}{
|
||||
{
|
||||
"Windows 10",
|
||||
[]byte{10, 0, 0x00, 0x47, 0, 0, 0, 0}, // build 18176 < 22000
|
||||
func(s string) bool { return s != "" && contains(s, "Windows 10") },
|
||||
},
|
||||
{
|
||||
"Windows 11",
|
||||
[]byte{10, 0, 0x00, 0x5B, 0, 0, 0, 0}, // build 23296 >= 22000
|
||||
func(s string) bool { return contains(s, "Windows 11") },
|
||||
},
|
||||
{
|
||||
"Windows 7",
|
||||
[]byte{6, 1, 0x00, 0x09, 0, 0, 0, 0},
|
||||
func(s string) bool { return contains(s, "Windows 7") },
|
||||
},
|
||||
{
|
||||
"Windows XP",
|
||||
[]byte{5, 1, 0x00, 0x0A, 0, 0, 0, 0},
|
||||
func(s string) bool { return contains(s, "Windows XP") },
|
||||
},
|
||||
{
|
||||
"Windows 2000",
|
||||
[]byte{5, 0, 0x00, 0x07, 0, 0, 0, 0},
|
||||
func(s string) bool { return contains(s, "Windows 2000") },
|
||||
},
|
||||
{
|
||||
"unknown",
|
||||
[]byte{4, 0, 0x00, 0x01, 0, 0, 0, 0},
|
||||
func(s string) bool { return contains(s, "Windows 4.0") },
|
||||
},
|
||||
{
|
||||
"too short",
|
||||
[]byte{10, 0},
|
||||
func(s string) bool { return s == "" },
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseOSVersion(tt.data, info)
|
||||
if !tt.check(info.OSVersion) {
|
||||
t.Errorf("OSVersion = %q", info.OSVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
|
||||
func() bool {
|
||||
for i := 0; i <= len(s)-len(sub); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}())
|
||||
}
|
||||
|
||||
// ---- parseTargetInfo ----
|
||||
|
||||
func TestParseTargetInfo(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseTargetInfo([]byte{}, info)
|
||||
if info.ComputerName != "" || info.DomainName != "" {
|
||||
t.Error("expected empty fields")
|
||||
}
|
||||
})
|
||||
|
||||
makeAVPair := func(avId uint16, value []byte) []byte {
|
||||
b := []byte{
|
||||
byte(avId), byte(avId >> 8),
|
||||
byte(len(value)), byte(len(value) >> 8),
|
||||
}
|
||||
b = append(b, value...)
|
||||
// terminator
|
||||
b = append(b, 0x00, 0x00, 0x00, 0x00)
|
||||
return b
|
||||
}
|
||||
|
||||
encodeUTF16LE := func(s string) []byte {
|
||||
var b []byte
|
||||
for _, r := range s {
|
||||
b = append(b, byte(r), byte(uint16(r)>>8))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
t.Run("MsvAvNbComputerName", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseTargetInfo(makeAVPair(0x0001, encodeUTF16LE("MYPC")), info)
|
||||
if info.ComputerName != "MYPC" {
|
||||
t.Errorf("ComputerName = %q", info.ComputerName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MsvAvNbDomainName", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseTargetInfo(makeAVPair(0x0002, encodeUTF16LE("DOMAIN")), info)
|
||||
if info.DomainName != "DOMAIN" {
|
||||
t.Errorf("DomainName = %q", info.DomainName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MsvAvDnsComputerName_fallback", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseTargetInfo(makeAVPair(0x0003, encodeUTF16LE("dns.host")), info)
|
||||
if info.ComputerName != "dns.host" {
|
||||
t.Errorf("ComputerName = %q", info.ComputerName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("terminator only", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseTargetInfo([]byte{0x00, 0x00, 0x00, 0x00}, info)
|
||||
if info.ComputerName != "" || info.DomainName != "" {
|
||||
t.Error("expected empty fields")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- parseNTLMChallenge ----
|
||||
|
||||
// buildNTLMChallengePacket 构建测试用 NTLM Challenge 包。
|
||||
// targetName 和 targetInfo 均为 UTF-16LE 编码字节。
|
||||
// flags 应包含 0x02000000 (NEGOTIATE_VERSION) 才会有 version 字段。
|
||||
func buildNTLMChallengePacket(targetName []byte, flags uint32, targetInfo []byte, version []byte) []byte {
|
||||
// 固定头:signature(8) + msgType(4) + targetLen(2) + targetMaxLen(2) + targetOffset(4)
|
||||
// + flags(4) + challenge(8) + reserved(8) + targetInfoLen(2) + targetInfoMaxLen(2) + targetInfoOffset(4)
|
||||
// + version(8, optional) + payload
|
||||
headerSize := 56 // 8+4+2+2+4+4+8+8+2+2+4+8 (version always included here)
|
||||
targetOffset := uint32(headerSize)
|
||||
targetInfoOffset := targetOffset + uint32(len(targetName))
|
||||
|
||||
buf := make([]byte, 0, headerSize+len(targetName)+len(targetInfo))
|
||||
|
||||
// signature
|
||||
buf = append(buf, []byte("NTLMSSP\x00")...)
|
||||
// messageType = 2
|
||||
buf = append(buf, 0x02, 0x00, 0x00, 0x00)
|
||||
// targetLength
|
||||
buf = append(buf, byte(len(targetName)), byte(len(targetName)>>8))
|
||||
// targetMaxLength
|
||||
buf = append(buf, byte(len(targetName)), byte(len(targetName)>>8))
|
||||
// targetOffset
|
||||
buf = append(buf, byte(targetOffset), byte(targetOffset>>8), byte(targetOffset>>16), byte(targetOffset>>24))
|
||||
// flags
|
||||
buf = append(buf, byte(flags), byte(flags>>8), byte(flags>>16), byte(flags>>24))
|
||||
// challenge (8 bytes)
|
||||
buf = append(buf, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
|
||||
// reserved (8 bytes)
|
||||
buf = append(buf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
// targetInfoLength
|
||||
buf = append(buf, byte(len(targetInfo)), byte(len(targetInfo)>>8))
|
||||
// targetInfoMaxLength
|
||||
buf = append(buf, byte(len(targetInfo)), byte(len(targetInfo)>>8))
|
||||
// targetInfoOffset
|
||||
buf = append(buf, byte(targetInfoOffset), byte(targetInfoOffset>>8), byte(targetInfoOffset>>16), byte(targetInfoOffset>>24))
|
||||
// version (8 bytes)
|
||||
if len(version) == 8 {
|
||||
buf = append(buf, version...)
|
||||
} else {
|
||||
buf = append(buf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
}
|
||||
// payload
|
||||
buf = append(buf, targetName...)
|
||||
buf = append(buf, targetInfo...)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func TestParseNTLMChallenge(t *testing.T) {
|
||||
t.Run("too short", func(t *testing.T) {
|
||||
info := &SMBTarget{}
|
||||
parseNTLMChallenge(make([]byte, 10), info)
|
||||
if info.DomainName != "" {
|
||||
t.Error("expected no domain")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad signature", func(t *testing.T) {
|
||||
data := make([]byte, 64)
|
||||
copy(data, "BADMAGIC")
|
||||
info := &SMBTarget{}
|
||||
parseNTLMChallenge(data, info)
|
||||
if info.DomainName != "" {
|
||||
t.Error("expected no domain")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong message type", func(t *testing.T) {
|
||||
data := make([]byte, 64)
|
||||
copy(data, "NTLMSSP\x00")
|
||||
data[8] = 0x01 // messageType = 1, not 2
|
||||
info := &SMBTarget{}
|
||||
parseNTLMChallenge(data, info)
|
||||
if info.DomainName != "" {
|
||||
t.Error("expected no domain for wrong message type")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid challenge with domain", func(t *testing.T) {
|
||||
encodeUTF16LE := func(s string) []byte {
|
||||
var b []byte
|
||||
for _, r := range s {
|
||||
b = append(b, byte(r), byte(uint16(r)>>8))
|
||||
}
|
||||
return b
|
||||
}
|
||||
targetName := encodeUTF16LE("WORKGROUP")
|
||||
flags := uint32(0x00000001 | 0x00000200) // UNICODE | NTLM, no VERSION flag
|
||||
data := buildNTLMChallengePacket(targetName, flags, nil, nil)
|
||||
info := &SMBTarget{}
|
||||
parseNTLMChallenge(data, info)
|
||||
if info.DomainName != "WORKGROUP" {
|
||||
t.Errorf("DomainName = %q, want WORKGROUP", info.DomainName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid challenge with targetInfo and version", func(t *testing.T) {
|
||||
encodeUTF16LE := func(s string) []byte {
|
||||
var b []byte
|
||||
for _, r := range s {
|
||||
b = append(b, byte(r), byte(uint16(r)>>8))
|
||||
}
|
||||
return b
|
||||
}
|
||||
targetName := encodeUTF16LE("CORP")
|
||||
|
||||
// AV_PAIR: MsvAvNbComputerName = "SERVER"
|
||||
computerNameBytes := encodeUTF16LE("SERVER")
|
||||
avPair := []byte{
|
||||
0x01, 0x00,
|
||||
byte(len(computerNameBytes)), byte(len(computerNameBytes) >> 8),
|
||||
}
|
||||
avPair = append(avPair, computerNameBytes...)
|
||||
avPair = append(avPair, 0x00, 0x00, 0x00, 0x00) // terminator
|
||||
|
||||
// NEGOTIATE_VERSION flag = 0x02000000
|
||||
flags := uint32(0x02000000 | 0x00000001 | 0x00000200)
|
||||
// Windows 10 build 19041
|
||||
version := []byte{10, 0, 0xA1, 0x4A, 0x00, 0x00, 0x00, 0x0F}
|
||||
data := buildNTLMChallengePacket(targetName, flags, avPair, version)
|
||||
|
||||
info := &SMBTarget{}
|
||||
parseNTLMChallenge(data, info)
|
||||
|
||||
if info.DomainName != "CORP" {
|
||||
t.Errorf("DomainName = %q, want CORP", info.DomainName)
|
||||
}
|
||||
if info.ComputerName != "SERVER" {
|
||||
t.Errorf("ComputerName = %q, want SERVER", info.ComputerName)
|
||||
}
|
||||
if info.OSVersion == "" {
|
||||
t.Error("OSVersion should not be empty")
|
||||
}
|
||||
if len(info.NTLMFlags) == 0 {
|
||||
t.Error("NTLMFlags should not be empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- classifySMBError ----
|
||||
|
||||
func TestClassifySMBError(t *testing.T) {
|
||||
t.Run("nil error", func(t *testing.T) {
|
||||
if got := classifySMBError(nil); got != ErrorTypeUnknown {
|
||||
t.Errorf("nil: got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("auth error keyword", func(t *testing.T) {
|
||||
err := fmt.Errorf("authentication failed")
|
||||
if got := classifySMBError(err); got != ErrorTypeAuth {
|
||||
t.Errorf("auth keyword: got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NT status code", func(t *testing.T) {
|
||||
err := fmt.Errorf("nt_status_logon_failure")
|
||||
if got := classifySMBError(err); got != ErrorTypeAuth {
|
||||
t.Errorf("NT status: got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("network error", func(t *testing.T) {
|
||||
err := fmt.Errorf("connection refused")
|
||||
if got := classifySMBError(err); got != ErrorTypeNetwork {
|
||||
t.Errorf("network: got %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- SMBProtocol.String() ----
|
||||
|
||||
func TestSMBProtocolString(t *testing.T) {
|
||||
tests := []struct {
|
||||
p SMBProtocol
|
||||
want string
|
||||
}{
|
||||
{SMBProtocol1, "SMBv1"},
|
||||
{SMBProtocol2, "SMBv2"},
|
||||
{SMBProtocolUnknown, "Unknown"},
|
||||
{SMBProtocol(99), "Unknown"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.p.String(); got != tt.want {
|
||||
t.Errorf("SMBProtocol(%d).String() = %q, want %q", tt.p, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- SMBTarget.Summary() ----
|
||||
|
||||
func TestSMBTargetSummary(t *testing.T) {
|
||||
t.Run("only protocol", func(t *testing.T) {
|
||||
info := &SMBTarget{Protocol: SMBProtocol2}
|
||||
if got := info.Summary(); got != "SMBv2" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("full fields", func(t *testing.T) {
|
||||
info := &SMBTarget{
|
||||
Protocol: SMBProtocol1,
|
||||
OSVersion: "Windows 10 (Build 19041)",
|
||||
ComputerName: "MYPC",
|
||||
}
|
||||
got := info.Summary()
|
||||
if !contains(got, "SMBv1") || !contains(got, "Windows 10") || !contains(got, "MYPC") {
|
||||
t.Errorf("Summary() = %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty optional fields", func(t *testing.T) {
|
||||
info := &SMBTarget{Protocol: SMBProtocolUnknown}
|
||||
if got := info.Summary(); got != "Unknown" {
|
||||
t.Errorf("got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---- buildNTLMSSPData ----
|
||||
|
||||
func TestBuildNTLMSSPData(t *testing.T) {
|
||||
flags := []byte{0x07, 0x82, 0x08, 0xA2}
|
||||
got := buildNTLMSSPData(flags)
|
||||
if len(got) == 0 {
|
||||
t.Fatal("buildNTLMSSPData returned empty")
|
||||
}
|
||||
// 长度固定(实际为158字节)
|
||||
const wantLen = 158
|
||||
if len(got) != wantLen {
|
||||
t.Errorf("len = %d, want %d", len(got), wantLen)
|
||||
}
|
||||
// flags 嵌入在偏移138处
|
||||
const flagsOffset = 138
|
||||
if got[flagsOffset] != flags[0] || got[flagsOffset+1] != flags[1] ||
|
||||
got[flagsOffset+2] != flags[2] || got[flagsOffset+3] != flags[3] {
|
||||
t.Errorf("flags not embedded correctly at offset %d: got %x %x %x %x",
|
||||
flagsOffset, got[flagsOffset], got[flagsOffset+1], got[flagsOffset+2], got[flagsOffset+3])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build plugin_smtp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifySMTPErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"535 authentication failed", errors.New("535 authentication failed"), ErrorTypeAuth},
|
||||
{"relay access denied", errors.New("relay access denied"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifySMTPErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifySMTPErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//go:build plugin_snmp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// --- buildSNMPGetRequest ---
|
||||
|
||||
func TestBuildSNMPGetRequest(t *testing.T) {
|
||||
oid := []int{1, 3, 6, 1, 2, 1, 1, 1, 0}
|
||||
|
||||
t.Run("returns non-empty bytes starting with ASN.1 SEQUENCE", func(t *testing.T) {
|
||||
pkt := buildSNMPGetRequest("public", oid)
|
||||
if len(pkt) == 0 {
|
||||
t.Fatal("expected non-empty packet")
|
||||
}
|
||||
if pkt[0] != 0x30 {
|
||||
t.Errorf("first byte = 0x%02x, want 0x30 (ASN.1 SEQUENCE)", pkt[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different communities produce different lengths", func(t *testing.T) {
|
||||
pkt1 := buildSNMPGetRequest("public", oid)
|
||||
pkt2 := buildSNMPGetRequest("longercommunitystringhere", oid)
|
||||
if len(pkt1) >= len(pkt2) {
|
||||
t.Errorf("expected longer community to produce longer packet: len(public)=%d len(long)=%d", len(pkt1), len(pkt2))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- marshalOIDWithNull ---
|
||||
|
||||
func TestMarshalOIDWithNull(t *testing.T) {
|
||||
oid := []int{1, 3, 6, 1, 2, 1, 1, 1, 0}
|
||||
result := marshalOIDWithNull(oid)
|
||||
|
||||
if len(result) == 0 {
|
||||
t.Fatal("expected non-empty bytes")
|
||||
}
|
||||
|
||||
// 应包含 OID tag (0x06) 和 NULL tag (0x05)
|
||||
foundOID := false
|
||||
foundNull := false
|
||||
for _, b := range result {
|
||||
if b == 0x06 {
|
||||
foundOID = true
|
||||
}
|
||||
if b == 0x05 {
|
||||
foundNull = true
|
||||
}
|
||||
}
|
||||
if !foundOID {
|
||||
t.Error("expected OID tag 0x06 in output")
|
||||
}
|
||||
if !foundNull {
|
||||
t.Error("expected NULL tag 0x05 in output")
|
||||
}
|
||||
}
|
||||
|
||||
// --- parseSNMPResponse ---
|
||||
|
||||
// buildTestSNMPResponse 构造最小合法 SNMPv2c GetResponse 包含 OctetString value
|
||||
func buildTestSNMPResponse(community string, value string) []byte {
|
||||
valBytes, _ := asn1.Marshal(asn1.RawValue{Class: 0, Tag: 4, Bytes: []byte(value)})
|
||||
oidBytes, _ := asn1.Marshal(asn1.ObjectIdentifier{1, 3, 6, 1, 2, 1, 1, 1, 0})
|
||||
|
||||
var vbContent []byte
|
||||
vbContent = append(vbContent, oidBytes...)
|
||||
vbContent = append(vbContent, valBytes...)
|
||||
varbind, _ := asn1.Marshal(asn1.RawValue{Class: 0, Tag: 16, IsCompound: true, Bytes: vbContent})
|
||||
varbindList, _ := asn1.Marshal(asn1.RawValue{Class: 0, Tag: 16, IsCompound: true, Bytes: varbind})
|
||||
|
||||
reqID, _ := asn1.Marshal(12345)
|
||||
errStatus, _ := asn1.Marshal(0)
|
||||
errIndex, _ := asn1.Marshal(0)
|
||||
|
||||
var pduContent []byte
|
||||
pduContent = append(pduContent, reqID...)
|
||||
pduContent = append(pduContent, errStatus...)
|
||||
pduContent = append(pduContent, errIndex...)
|
||||
pduContent = append(pduContent, varbindList...)
|
||||
|
||||
// GetResponse PDU: context-specific tag 2
|
||||
pdu, _ := asn1.Marshal(asn1.RawValue{Class: 2, Tag: 2, IsCompound: true, Bytes: pduContent})
|
||||
|
||||
version, _ := asn1.Marshal(1) // SNMPv2c
|
||||
comm, _ := asn1.Marshal([]byte(community))
|
||||
|
||||
var msgContent []byte
|
||||
msgContent = append(msgContent, version...)
|
||||
msgContent = append(msgContent, comm...)
|
||||
msgContent = append(msgContent, pdu...)
|
||||
|
||||
msg, _ := asn1.Marshal(asn1.RawValue{Class: 0, Tag: 16, IsCompound: true, Bytes: msgContent})
|
||||
return msg
|
||||
}
|
||||
|
||||
func TestParseSNMPResponse(t *testing.T) {
|
||||
t.Run("empty data returns empty", func(t *testing.T) {
|
||||
got := parseSNMPResponse([]byte{})
|
||||
if got != "" {
|
||||
t.Errorf("got %q, want empty", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid ASN.1 returns empty", func(t *testing.T) {
|
||||
got := parseSNMPResponse([]byte{0xFF, 0xFF, 0xFF})
|
||||
if got != "" {
|
||||
t.Errorf("got %q, want empty", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid response returns sysDescr value", func(t *testing.T) {
|
||||
want := "Linux router 5.4.0"
|
||||
pkt := buildTestSNMPResponse("public", want)
|
||||
got := parseSNMPResponse(pkt)
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- buildCommunityList ---
|
||||
|
||||
func TestBuildCommunityList(t *testing.T) {
|
||||
p := NewSNMPPlugin()
|
||||
cfg := &common.Config{}
|
||||
|
||||
list := p.buildCommunityList(cfg)
|
||||
|
||||
if len(list) == 0 {
|
||||
t.Fatal("community list must not be empty")
|
||||
}
|
||||
|
||||
hasPublic := false
|
||||
hasPrivate := false
|
||||
for _, c := range list {
|
||||
if c == "public" {
|
||||
hasPublic = true
|
||||
}
|
||||
if c == "private" {
|
||||
hasPrivate = true
|
||||
}
|
||||
}
|
||||
if !hasPublic {
|
||||
t.Error("community list must contain 'public'")
|
||||
}
|
||||
if !hasPrivate {
|
||||
t.Error("community list must contain 'private'")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//go:build plugin_ssh || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifySSHErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"unable to authenticate", errors.New("unable to authenticate"), ErrorTypeAuth},
|
||||
{"no supported methods remain", errors.New("no supported methods remain"), ErrorTypeAuth},
|
||||
{"handshake failed", errors.New("handshake failed"), ErrorTypeThrottle},
|
||||
{"ssh disconnect", errors.New("ssh: disconnect"), ErrorTypeThrottle},
|
||||
{"max startups", errors.New("max startups"), ErrorTypeThrottle},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"random error", errors.New("random error"), ErrorTypeUnknown},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifySSHErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifySSHErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifySSHError(t *testing.T) {
|
||||
authKeywords := []string{"bad password", "invalid key"}
|
||||
throttleKeywords := []string{"rate limited", "too fast"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"custom auth keyword", errors.New("bad password provided"), ErrorTypeAuth},
|
||||
{"custom throttle keyword", errors.New("rate limited by server"), ErrorTypeThrottle},
|
||||
{"network error", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"no match", errors.New("something else"), ErrorTypeUnknown},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifySSHError(tt.err, authKeywords, throttleKeywords)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifySSHError(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
@@ -15,3 +16,24 @@ func TestTelnetExtractEvidenceTruncatesByRune(t *testing.T) {
|
||||
t.Fatalf("extractEvidence() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyTelnetErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil", nil, ErrorTypeUnknown},
|
||||
{"login failed", errors.New("login failed"), ErrorTypeAuth},
|
||||
{"credentials rejected", errors.New("credentials rejected"), ErrorTypeAuth},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
{"unknown", errors.New("random telnet error"), ErrorTypeUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := classifyTelnetErrorType(tt.err); got != tt.want {
|
||||
t.Errorf("classifyTelnetErrorType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build plugin_vnc || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClassifyVNCErrorType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want ErrorType
|
||||
}{
|
||||
{"nil error", nil, ErrorTypeUnknown},
|
||||
{"authentication failed", errors.New("authentication failed"), ErrorTypeAuth},
|
||||
{"too many authentication failures", errors.New("too many authentication failures"), ErrorTypeNetwork},
|
||||
{"connection refused", errors.New("connection refused"), ErrorTypeNetwork},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := classifyVNCErrorType(tt.err)
|
||||
if got != tt.want {
|
||||
t.Errorf("classifyVNCErrorType(%v) = %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user