Files

550 lines
16 KiB
Go
Raw Permalink Normal View History

2026-06-01 02:06:15 +08:00
package common
import (
2026-10-01 12:46:29 +08:00
"os"
"path/filepath"
2026-06-01 02:06:15 +08:00
"reflect"
"testing"
"time"
fscanconfig "github.com/shadow1ng/fscan/common/config"
2026-06-01 02:06:15 +08:00
)
2026-10-01 12:46:29 +08:00
func TestBuildConfigAdditionalCredentials(t *testing.T) {
original := NewConfig().Credentials
dir := t.TempDir()
usersFile := filepath.Join(dir, "users.txt")
passwordsFile := filepath.Join(dir, "passwords.txt")
if err := os.WriteFile(usersFile, []byte("file-user\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(passwordsFile, []byte("file-password\n"), 0600); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
flags FlagVars
users []string
passwords []string
appendUsers bool
}{
{name: "defaults"},
{
name: "append to defaults",
flags: FlagVars{
AddUsers: "extra-user,extra-user",
AddPasswords: "extra-password extra-password",
},
users: []string{"extra-user"},
passwords: append(append([]string(nil), fscanconfig.DefaultPasswords...), "extra-password"),
appendUsers: true,
},
{
name: "default password is not duplicated",
flags: FlagVars{AddPasswords: "123456,123456"},
},
{
name: "append to command line overrides",
flags: FlagVars{
Username: "custom-user", Password: "primary password",
AddUsers: "extra-user,custom-user", AddPasswords: "extra-password extra-password",
},
users: []string{"custom-user", "extra-user"},
passwords: []string{"primary password", "extra-password"},
},
{
name: "append to file overrides",
flags: FlagVars{
UsersFile: usersFile, PasswordsFile: passwordsFile,
AddUsers: "extra-user,file-user", AddPasswords: "extra-password file-password",
},
users: []string{"file-user", "extra-user"},
passwords: []string{"file-password", "extra-password"},
},
{
name: "command line and files still replace defaults",
flags: FlagVars{
Username: "custom-user", Password: "primary password",
UsersFile: usersFile, PasswordsFile: passwordsFile,
AddUsers: "extra-user", AddPasswords: "extra-password",
},
users: []string{"custom-user", "file-user", "extra-user"},
passwords: []string{"primary password", "file-password", "extra-password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, _, err := BuildConfig(&tt.flags, &HostInfo{})
if err != nil {
t.Fatal(err)
}
wantPasswords := tt.passwords
if wantPasswords == nil {
wantPasswords = append([]string(nil), fscanconfig.DefaultPasswords...)
}
if !reflect.DeepEqual(cfg.Credentials.Passwords, wantPasswords) {
t.Errorf("passwords = %v, want %v", cfg.Credentials.Passwords, wantPasswords)
}
for service, defaults := range fscanconfig.DefaultUserDict {
wantUsers := tt.users
if wantUsers == nil || tt.appendUsers {
wantUsers = append(append([]string{}, defaults...), wantUsers...)
}
if !reflect.DeepEqual(cfg.Credentials.Userdict[service], wantUsers) {
t.Errorf("%s users = %v, want %v", service, cfg.Credentials.Userdict[service], wantUsers)
}
}
})
}
defaults, _, err := BuildConfig(&FlagVars{}, &HostInfo{})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(defaults.Credentials.Passwords, original.Passwords) ||
!reflect.DeepEqual(defaults.Credentials.Userdict, original.Userdict) {
t.Fatal("additional credentials leaked into a later config")
}
}
2026-06-01 03:32:13 +08:00
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
2026-06-01 02:06:15 +08:00
fv := &FlagVars{
2026-06-13 22:35:48 +08:00
Password: "root admin,pass0",
2026-06-01 02:06:15 +08:00
AddPasswords: "pass1 pass2,pass3\tpass4",
}
2026-06-01 03:32:13 +08:00
got, err := parsePasswords(fv)
if err != nil {
t.Fatalf("parsePasswords error = %v", err)
}
2026-06-13 22:35:48 +08:00
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
2026-06-01 02:06:15 +08:00
if !reflect.DeepEqual(got, want) {
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
}
}
2026-06-01 03:32:13 +08:00
func TestBuildConfigReturnsUserFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing users file")
}
}
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing passwords file")
}
}
func TestBuildConfigReturnsURLFileError(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for missing urls file")
}
}
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
if err == nil {
t.Fatal("BuildConfig should fail for invalid hash value")
}
}
func TestBuildConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg, _, err := BuildConfig(&FlagVars{Username: "custom-user"}, &HostInfo{})
if err != nil {
t.Fatalf("BuildConfig error = %v", err)
}
defaultSSHUsers := fscanconfig.DefaultUserDict["ssh"]
if len(defaultSSHUsers) == 1 && defaultSSHUsers[0] == "custom-user" {
t.Fatal("BuildConfig mutated DefaultUserDict")
}
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("Config userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("Config passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("Config port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("Config default map shares backing storage with DefaultProbeMap")
}
}
func TestParseUserPassPairsKeepsAdditionalCredentialFlags(t *testing.T) {
tests := []struct {
name string
fv *FlagVars
}{
{
name: "additional passwords",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddPasswords: "extra",
},
},
{
name: "additional users",
fv: &FlagVars{
Username: "root",
Password: "primary",
AddUsers: "admin",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairs, err := parseUserPassPairs(tt.fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 0 {
t.Fatalf("parseUserPassPairs returned exact pairs %#v; additional credential flags would be ignored", pairs)
}
})
}
}
func TestNewConfigDefaultsAreIndependentCopies(t *testing.T) {
cfg := NewConfig()
cfg.Credentials.Userdict["ssh"][0] = "mutated-user"
if fscanconfig.DefaultUserDict["ssh"][0] == "mutated-user" {
t.Fatal("NewConfig userdict shares backing storage with DefaultUserDict")
}
cfg.Credentials.Passwords[0] = "mutated-password"
if fscanconfig.DefaultPasswords[0] == "mutated-password" {
t.Fatal("NewConfig passwords share backing storage with DefaultPasswords")
}
port := 80
cfg.PortMap[port][0] = "mutated-probe"
if fscanconfig.DefaultPortMap[port][0] == "mutated-probe" {
t.Fatal("NewConfig port map shares backing storage with DefaultPortMap")
}
cfg.DefaultMap[0] = "mutated-default-probe"
if fscanconfig.DefaultProbeMap[0] == "mutated-default-probe" {
t.Fatal("NewConfig default map shares backing storage with DefaultProbeMap")
}
}
2026-06-01 03:32:13 +08:00
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
fv := &FlagVars{Ports: "22"}
info := &HostInfo{Host: "127.0.0.1:8080"}
cfg := BuildConfigFromFlags(fv)
state := NewState()
if err := parseTargets(fv, info, cfg, state); err != nil {
t.Fatalf("parseTargets error = %v", err)
}
if info.Host != "" {
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
}
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
t.Fatalf("hostPorts = %#v, want host:port target", got)
}
}
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
got := normalizeURL("HTTPS://example.com")
if got != "HTTPS://example.com" {
t.Fatalf("normalizeURL() = %q", got)
}
}
func TestNormalizeURLBracketsIPv6Literals(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "bare ipv6 without scheme", in: "2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bracketed ipv6 without scheme", in: "[2001:db8::1]", want: "http://[2001:db8::1]"},
{name: "bare ipv6 with scheme", in: "http://2001:db8::1", want: "http://[2001:db8::1]"},
{name: "bare ipv6 path without scheme", in: "2001:db8::1/admin", want: "http://[2001:db8::1]/admin"},
{name: "bare ipv6 query without scheme", in: "2001:db8::1?debug=1", want: "http://[2001:db8::1]?debug=1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeURL(tt.in); got != tt.want {
t.Fatalf("normalizeURL(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
// TestModuleTimeout 测试模块超时计算
func TestModuleTimeout(t *testing.T) {
tests := []struct {
name string
timeout time.Duration
want time.Duration
}{
{"超时大于下限", 10 * time.Second, 10 * time.Second},
{"超时等于下限", 3 * time.Second, 3 * time.Second},
{"超时小于下限", 1 * time.Second, 3 * time.Second},
{"零超时", 0, 3 * time.Second},
{"负超时", -1 * time.Second, 3 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := NewConfig()
cfg.Timeout = tt.timeout
got := cfg.ModuleTimeout()
if got != tt.want {
t.Errorf("ModuleTimeout() = %v, want %v", got, tt.want)
}
})
}
}
// TestParseUserPassPairsExactMatch 测试精确单用户单密码路径
func TestParseUserPassPairsExactMatch(t *testing.T) {
fv := &FlagVars{
Username: "admin",
Password: "secret",
}
pairs, err := parseUserPassPairs(fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 1 {
t.Fatalf("期望 1 个 pair, 实际 %d", len(pairs))
}
if pairs[0].Username != "admin" || pairs[0].Password != "secret" {
t.Errorf("pair = %+v, want {admin secret}", pairs[0])
}
}
// TestParseUserPassPairsMultiUserSkips 测试多用户时不生成精确 pair
func TestParseUserPassPairsMultiUserSkips(t *testing.T) {
fv := &FlagVars{
Username: "admin,root",
Password: "pass",
}
pairs, err := parseUserPassPairs(fv)
if err != nil {
t.Fatalf("parseUserPassPairs error = %v", err)
}
if len(pairs) != 0 {
t.Fatalf("多用户场景不应生成精确 pair, 实际 %d 个", len(pairs))
}
}
// TestParseURLsEmpty 测试空输入返回空列表
func TestParseURLsEmpty(t *testing.T) {
fv := &FlagVars{}
urls, err := parseURLs(fv)
if err != nil {
t.Fatalf("parseURLs error = %v", err)
}
if len(urls) != 0 {
t.Fatalf("空输入应返回空 url 列表, 实际 %v", urls)
}
}
// TestParseURLsCommaSeparated 测试逗号分隔多 URL
func TestParseURLsCommaSeparated(t *testing.T) {
fv := &FlagVars{
TargetURL: "http://a.com,http://b.com,http://a.com", // 含重复
}
urls, err := parseURLs(fv)
if err != nil {
t.Fatalf("parseURLs error = %v", err)
}
if len(urls) != 2 {
t.Fatalf("去重后应有 2 个 url, 实际 %d: %v", len(urls), urls)
}
}
// TestParseURLsMissingFile 测试缺失文件返回错误
func TestParseURLsMissingFile(t *testing.T) {
fv := &FlagVars{URLsFile: "nonexistent-urls.txt"}
_, err := parseURLs(fv)
if err == nil {
t.Fatal("缺失文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// parseHashes
// ---------------------------------------------------------------------------
// TestParseHashesEmpty 空输入返回空结果
func TestParseHashesEmpty(t *testing.T) {
fv := &FlagVars{}
vals, bytes, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 0 || len(bytes) != 0 {
t.Fatalf("空输入应返回空结果, vals=%v bytes=%v", vals, bytes)
}
}
// TestParseHashesValidNTLM 纯 32 字符 hex hash
func TestParseHashesValidNTLM(t *testing.T) {
hash := "aabbccddeeff00112233445566778899"
fv := &FlagVars{HashValue: hash}
vals, hashBytes, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 1 || vals[0] != hash {
t.Fatalf("vals = %v, want [%s]", vals, hash)
}
if len(hashBytes) != 1 || len(hashBytes[0]) != 16 {
t.Fatalf("hashBytes length wrong: %v", hashBytes)
}
}
// TestParseHashesLMNTFormat LM:NT 格式,提取 NT 部分
func TestParseHashesLMNTFormat(t *testing.T) {
lm := "aad3b435b51404eeaad3b435b51404ee"
nt := "31d6cfe0d16ae931b73c59d7e0c089c0"
fv := &FlagVars{HashValue: lm + ":" + nt}
vals, _, err := parseHashes(fv)
if err != nil {
t.Fatalf("parseHashes error = %v", err)
}
if len(vals) != 1 || vals[0] != nt {
t.Fatalf("vals = %v, want [%s]", vals, nt)
}
}
// TestParseHashesInvalidLength hash 长度不是 32 → error
func TestParseHashesInvalidLength(t *testing.T) {
fv := &FlagVars{HashValue: "tooshort"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("hash 长度不足应返回错误")
}
}
// TestParseHashesInvalidHex 32 字符但含非 hex 字符 → error
func TestParseHashesInvalidHex(t *testing.T) {
fv := &FlagVars{HashValue: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("非 hex 字符应返回错误")
}
}
// TestParseHashesMissingFile hash 文件不存在 → error
func TestParseHashesMissingFile(t *testing.T) {
fv := &FlagVars{HashFile: "nonexistent-hashes.txt"}
_, _, err := parseHashes(fv)
if err == nil {
t.Fatal("缺失 hash 文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// parseUsernames
// ---------------------------------------------------------------------------
// TestParseUsernamesEmpty 空输入返回空结果
func TestParseUsernamesEmpty(t *testing.T) {
fv := &FlagVars{}
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
if len(got) != 0 {
t.Fatalf("空输入应返回空, got %v", got)
}
}
// TestParseUsernamesCommaSeparated 逗号分隔多用户
func TestParseUsernamesCommaSeparated(t *testing.T) {
fv := &FlagVars{Username: "admin, root, admin"} // 含重复和空格
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
want := []string{"admin", "root"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
// TestParseUsernamesAddUsers AddUsers 追加去重
func TestParseUsernamesAddUsers(t *testing.T) {
fv := &FlagVars{
Username: "admin",
AddUsers: "root,admin", // admin 重复
}
got, err := parseUsernames(fv)
if err != nil {
t.Fatalf("parseUsernames error = %v", err)
}
want := []string{"admin", "root"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
// TestParseUsernamesMissingFile 缺失用户文件 → error
func TestParseUsernamesMissingFile(t *testing.T) {
fv := &FlagVars{UsersFile: "nonexistent-users.txt"}
_, err := parseUsernames(fv)
if err == nil {
t.Fatal("缺失用户文件应返回错误")
}
}
// ---------------------------------------------------------------------------
// cloneStringSlice
// ---------------------------------------------------------------------------
// TestCloneStringSliceNil nil 输入返回 nil
func TestCloneStringSliceNil(t *testing.T) {
got := cloneStringSlice(nil)
if got != nil {
t.Fatalf("nil 输入应返回 nil, got %v", got)
}
}
// TestCloneStringSliceEmpty 空切片:append 无元素结果为 nil,len 为 0
func TestCloneStringSliceEmpty(t *testing.T) {
got := cloneStringSlice([]string{})
if len(got) != 0 {
t.Fatalf("got len %d, want 0", len(got))
}
}
// TestCloneStringSliceCopiesValues 正常切片:值正确且独立
func TestCloneStringSliceCopiesValues(t *testing.T) {
src := []string{"a", "b", "c"}
got := cloneStringSlice(src)
if !reflect.DeepEqual(got, src) {
t.Fatalf("got %v, want %v", got, src)
}
// 修改 clone 不影响原始
got[0] = "mutated"
if src[0] != "a" {
t.Fatal("cloneStringSlice 返回的切片与源共享底层数组")
}
}