mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
451 lines
13 KiB
Go
451 lines
13 KiB
Go
package common
|
||
|
||
import (
|
||
"reflect"
|
||
"testing"
|
||
"time"
|
||
|
||
fscanconfig "github.com/shadow1ng/fscan/common/config"
|
||
)
|
||
|
||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||
fv := &FlagVars{
|
||
Password: "root admin,pass0",
|
||
AddPasswords: "pass1 pass2,pass3\tpass4",
|
||
}
|
||
|
||
got, err := parsePasswords(fv)
|
||
if err != nil {
|
||
t.Fatalf("parsePasswords error = %v", err)
|
||
}
|
||
// -pwd 逗号分隔,空格保留;-pwda 逗号/空格/tab 分隔
|
||
want := []string{"root admin", "pass0", "pass1", "pass2", "pass3", "pass4"}
|
||
if !reflect.DeepEqual(got, want) {
|
||
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
|
||
}
|
||
}
|
||
|
||
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")
|
||
}
|
||
}
|
||
|
||
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 返回的切片与源共享底层数组")
|
||
}
|
||
}
|