mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 19:21:52 +08:00
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
394 lines
10 KiB
Go
394 lines
10 KiB
Go
package config
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
/*
|
|
constants_test.go - 配置常量测试
|
|
|
|
测试目标:端口组、探测器配置、字典数据
|
|
价值:配置错误会导致:
|
|
- 端口组错误 → 扫描范围错误(用户遗漏目标)
|
|
- 字典错误 → 暴力破解失败(无法登录系统)
|
|
- 探测器配置错误 → 服务识别失败
|
|
|
|
"配置是数据,但数据也会有bug。端口范围错误、字典重复、
|
|
空值遗漏——这些都是真实问题。测试数据和测试代码一样重要。"
|
|
*/
|
|
|
|
// =============================================================================
|
|
// 端口组测试
|
|
// =============================================================================
|
|
|
|
// TestPortGroups_Format 测试端口组格式
|
|
//
|
|
// 验证:所有端口组字符串格式正确(可解析为端口列表)
|
|
func TestPortGroups_Format(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
portGroup string
|
|
}{
|
|
{"WebPorts", WebPorts},
|
|
{"MainPorts", MainPorts},
|
|
{"DbPorts", DbPorts},
|
|
{"ServicePorts", ServicePorts},
|
|
{"CommonPorts", CommonPorts},
|
|
{"AllPorts", AllPorts},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// 验证格式:逗号分隔的数字或范围
|
|
if tt.portGroup == "" {
|
|
t.Error("端口组不应为空")
|
|
return
|
|
}
|
|
|
|
// AllPorts是特殊格式"1-65535"
|
|
if tt.portGroup == "1-65535" {
|
|
t.Logf("✓ %s 格式正确(范围格式)", tt.name)
|
|
return
|
|
}
|
|
|
|
// 其他端口组应该是逗号分隔的数字
|
|
ports := strings.Split(tt.portGroup, ",")
|
|
if len(ports) == 0 {
|
|
t.Error("端口组应该包含至少一个端口")
|
|
return
|
|
}
|
|
|
|
// 验证每个端口都是有效数字
|
|
for i, portStr := range ports {
|
|
port, err := strconv.Atoi(strings.TrimSpace(portStr))
|
|
if err != nil {
|
|
t.Errorf("第%d个端口 '%s' 不是有效数字: %v", i+1, portStr, err)
|
|
continue
|
|
}
|
|
|
|
// 验证端口范围
|
|
if port < 1 || port > 65535 {
|
|
t.Errorf("第%d个端口 %d 超出有效范围 [1-65535]", i+1, port)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ %s 格式正确(%d个端口)", tt.name, len(ports))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPortGroups_NoEmpty 测试端口组非空
|
|
func TestPortGroups_NoEmpty(t *testing.T) {
|
|
groups := map[string]string{
|
|
"WebPorts": WebPorts,
|
|
"MainPorts": MainPorts,
|
|
"DbPorts": DbPorts,
|
|
"ServicePorts": ServicePorts,
|
|
"CommonPorts": CommonPorts,
|
|
"AllPorts": AllPorts,
|
|
}
|
|
|
|
for name, ports := range groups {
|
|
if ports == "" {
|
|
t.Errorf("%s 不应为空字符串", name)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ 所有端口组非空")
|
|
}
|
|
|
|
// TestPortGroups_NoDuplicates 测试端口组无重复
|
|
func TestPortGroups_NoDuplicates(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
portGroup string
|
|
}{
|
|
{"WebPorts", WebPorts},
|
|
{"MainPorts", MainPorts},
|
|
{"DbPorts", DbPorts},
|
|
{"ServicePorts", ServicePorts},
|
|
{"CommonPorts", CommonPorts},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.portGroup == "1-65535" {
|
|
t.Skip("范围格式无需检查重复")
|
|
return
|
|
}
|
|
|
|
ports := strings.Split(tt.portGroup, ",")
|
|
seen := make(map[string]bool)
|
|
duplicates := []string{}
|
|
|
|
for _, port := range ports {
|
|
port = strings.TrimSpace(port)
|
|
if seen[port] {
|
|
duplicates = append(duplicates, port)
|
|
}
|
|
seen[port] = true
|
|
}
|
|
|
|
if len(duplicates) > 0 {
|
|
t.Errorf("%s 包含重复端口: %v", tt.name, duplicates)
|
|
} else {
|
|
t.Logf("✓ %s 无重复端口", tt.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetPortGroups_Completeness 测试GetPortGroups完整性
|
|
//
|
|
// 验证:返回的map包含所有预定义的端口组
|
|
func TestGetPortGroups_Completeness(t *testing.T) {
|
|
groups := GetPortGroups()
|
|
|
|
expectedKeys := []string{"web", "main", "db", "service", "common", "all"}
|
|
for _, key := range expectedKeys {
|
|
if _, ok := groups[key]; !ok {
|
|
t.Errorf("GetPortGroups缺少键: %s", key)
|
|
}
|
|
}
|
|
|
|
if len(groups) != len(expectedKeys) {
|
|
t.Errorf("GetPortGroups返回%d个组,期望%d个", len(groups), len(expectedKeys))
|
|
}
|
|
|
|
t.Logf("✓ GetPortGroups包含所有%d个端口组", len(expectedKeys))
|
|
}
|
|
|
|
// TestGetPortGroups_Values 测试GetPortGroups返回正确的值
|
|
func TestGetPortGroups_Values(t *testing.T) {
|
|
groups := GetPortGroups()
|
|
|
|
tests := []struct {
|
|
key string
|
|
expected string
|
|
}{
|
|
{"web", WebPorts},
|
|
{"main", MainPorts},
|
|
{"db", DbPorts},
|
|
{"service", ServicePorts},
|
|
{"common", CommonPorts},
|
|
{"all", AllPorts},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
actual, ok := groups[tt.key]
|
|
if !ok {
|
|
t.Fatalf("GetPortGroups缺少键: %s", tt.key)
|
|
}
|
|
|
|
if actual != tt.expected {
|
|
t.Errorf("GetPortGroups[%s] 值不匹配\n期望前20字符: %s...\n实际前20字符: %s...",
|
|
tt.key, tt.expected[:20], actual[:20])
|
|
}
|
|
|
|
t.Logf("✓ %s 映射正确", tt.key)
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 探测器配置测试
|
|
// =============================================================================
|
|
|
|
// TestDefaultProbeMap_NoEmpty 测试默认探测器列表非空
|
|
func TestDefaultProbeMap_NoEmpty(t *testing.T) {
|
|
if len(DefaultProbeMap) == 0 {
|
|
t.Error("DefaultProbeMap不应为空")
|
|
return
|
|
}
|
|
|
|
// 验证每个探测器名称非空
|
|
for i, probe := range DefaultProbeMap {
|
|
if probe == "" {
|
|
t.Errorf("第%d个探测器名称为空", i+1)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ DefaultProbeMap包含%d个探测器", len(DefaultProbeMap))
|
|
}
|
|
|
|
// TestDefaultPortMap_ValidKeys 测试DefaultPortMap的键有效
|
|
func TestDefaultPortMap_ValidKeys(t *testing.T) {
|
|
invalidPorts := []int{}
|
|
|
|
for port := range DefaultPortMap {
|
|
if port < 1 || port > 65535 {
|
|
invalidPorts = append(invalidPorts, port)
|
|
}
|
|
}
|
|
|
|
if len(invalidPorts) > 0 {
|
|
t.Errorf("DefaultPortMap包含无效端口号: %v", invalidPorts)
|
|
} else {
|
|
t.Logf("✓ DefaultPortMap的%d个端口号都有效", len(DefaultPortMap))
|
|
}
|
|
}
|
|
|
|
// TestDefaultPortMap_NoEmptyValues 测试DefaultPortMap值非空
|
|
func TestDefaultPortMap_NoEmptyValues(t *testing.T) {
|
|
emptyPorts := []int{}
|
|
|
|
for port, probes := range DefaultPortMap {
|
|
if len(probes) == 0 {
|
|
emptyPorts = append(emptyPorts, port)
|
|
}
|
|
}
|
|
|
|
if len(emptyPorts) > 0 {
|
|
t.Errorf("以下端口的探测器列表为空: %v", emptyPorts)
|
|
} else {
|
|
t.Logf("✓ DefaultPortMap所有端口都有探测器")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 字典数据测试
|
|
// =============================================================================
|
|
|
|
// TestDefaultUserDict_NoEmptyKeys 测试DefaultUserDict键非空
|
|
func TestDefaultUserDict_NoEmptyKeys(t *testing.T) {
|
|
for service, users := range DefaultUserDict {
|
|
if service == "" {
|
|
t.Error("DefaultUserDict包含空服务名")
|
|
}
|
|
|
|
if len(users) == 0 {
|
|
t.Errorf("服务 '%s' 的用户列表为空", service)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ DefaultUserDict包含%d个服务", len(DefaultUserDict))
|
|
}
|
|
|
|
// TestDefaultUserDict_CommonServices 测试DefaultUserDict包含常见服务
|
|
func TestDefaultUserDict_CommonServices(t *testing.T) {
|
|
commonServices := []string{"ftp", "mysql", "mssql", "ssh", "redis", "mongodb"}
|
|
|
|
for _, service := range commonServices {
|
|
if _, ok := DefaultUserDict[service]; !ok {
|
|
t.Errorf("DefaultUserDict缺少常见服务: %s", service)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ DefaultUserDict包含所有常见服务")
|
|
}
|
|
|
|
// TestDefaultUserDict_AllowsEmptyUser 测试DefaultUserDict允许空用户名
|
|
//
|
|
// 验证:某些服务(如redis)允许空用户名
|
|
func TestDefaultUserDict_AllowsEmptyUser(t *testing.T) {
|
|
// redis服务应该包含空用户名
|
|
redisUsers, ok := DefaultUserDict["redis"]
|
|
if !ok {
|
|
t.Skip("DefaultUserDict不包含redis,跳过测试")
|
|
return
|
|
}
|
|
|
|
hasEmptyUser := false
|
|
for _, user := range redisUsers {
|
|
if user == "" {
|
|
hasEmptyUser = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasEmptyUser {
|
|
t.Error("redis用户列表应该包含空用户名(默认无认证)")
|
|
} else {
|
|
t.Logf("✓ redis用户列表正确包含空用户名")
|
|
}
|
|
}
|
|
|
|
// TestDefaultPasswords_NoEmpty 测试DefaultPasswords非空
|
|
func TestDefaultPasswords_NoEmpty(t *testing.T) {
|
|
if len(DefaultPasswords) == 0 {
|
|
t.Error("DefaultPasswords不应为空")
|
|
return
|
|
}
|
|
|
|
t.Logf("✓ DefaultPasswords包含%d个密码", len(DefaultPasswords))
|
|
}
|
|
|
|
// TestDefaultPasswords_AllowsEmptyPassword 测试DefaultPasswords允许空密码
|
|
func TestDefaultPasswords_AllowsEmptyPassword(t *testing.T) {
|
|
// 应该包含空密码(某些服务默认无密码)
|
|
hasEmptyPassword := false
|
|
for _, pass := range DefaultPasswords {
|
|
if pass == "" {
|
|
hasEmptyPassword = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasEmptyPassword {
|
|
t.Error("DefaultPasswords应该包含空密码(某些服务默认无密码)")
|
|
} else {
|
|
t.Logf("✓ DefaultPasswords正确包含空密码")
|
|
}
|
|
}
|
|
|
|
// TestDefaultPasswords_HasPlaceholder 测试DefaultPasswords包含占位符
|
|
func TestDefaultPasswords_HasPlaceholder(t *testing.T) {
|
|
// 应该包含{user}占位符(密码=用户名的场景)
|
|
hasPlaceholder := false
|
|
for _, pass := range DefaultPasswords {
|
|
if strings.Contains(pass, "{user}") {
|
|
hasPlaceholder = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasPlaceholder {
|
|
t.Error("DefaultPasswords应该包含{user}占位符(密码=用户名变体)")
|
|
} else {
|
|
t.Logf("✓ DefaultPasswords正确包含{user}占位符")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 结构体测试
|
|
// =============================================================================
|
|
|
|
// TestPocInfo_Fields 测试PocInfo结构体字段
|
|
func TestPocInfo_Fields(t *testing.T) {
|
|
poc := PocInfo{
|
|
Target: "http://example.com",
|
|
PocName: "test-poc",
|
|
}
|
|
|
|
if poc.Target != "http://example.com" {
|
|
t.Error("PocInfo.Target赋值失败")
|
|
}
|
|
|
|
if poc.PocName != "test-poc" {
|
|
t.Error("PocInfo.PocName赋值失败")
|
|
}
|
|
|
|
t.Logf("✓ PocInfo结构体正常工作")
|
|
}
|
|
|
|
// TestCredentialPair_Fields 测试CredentialPair结构体字段
|
|
func TestCredentialPair_Fields(t *testing.T) {
|
|
cred := CredentialPair{
|
|
Username: "admin",
|
|
Password: "password123",
|
|
}
|
|
|
|
if cred.Username != "admin" {
|
|
t.Error("CredentialPair.Username赋值失败")
|
|
}
|
|
|
|
if cred.Password != "password123" {
|
|
t.Error("CredentialPair.Password赋值失败")
|
|
}
|
|
|
|
t.Logf("✓ CredentialPair结构体正常工作")
|
|
}
|