feat: v2.1.0 核心重构与功能增强

## 架构重构
- 全局变量消除,迁移至 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)
This commit is contained in:
ZacharyZcR
2026-01-11 20:16:23 +08:00
parent 6b13b2e84f
commit 71b92d4408
948 changed files with 92335 additions and 24630 deletions
+620
View File
@@ -0,0 +1,620 @@
package parsers
import (
"strings"
"testing"
)
/*
validation_test.go - 参数验证解析器测试
测试目标:ValidationParser核心验证逻辑
价值:验证逻辑错误会导致:
- 用户无法启动扫描(false positive错误)
- 错误配置未被发现(false negative漏检)
- 性能问题未预警(大规模扫描超时)
"验证是用户的第一道防线。验证太严=拒绝合法输入,
验证太松=允许错误配置。必须精确测试每个规则。"
*/
// =============================================================================
// ValidationParser - 构造函数测试
// =============================================================================
// TestNewValidationParser_DefaultOptions 测试默认选项
//
// 验证:nil选项时使用默认配置
func TestNewValidationParser_DefaultOptions(t *testing.T) {
parser := NewValidationParser(nil)
if parser == nil {
t.Fatal("NewValidationParser(nil)应该返回有效parser")
}
if parser.options == nil {
t.Error("options不应为nil(应使用默认配置)")
}
// 验证默认值合理性
if parser.options.MaxErrorCount <= 0 {
t.Error("MaxErrorCount应该大于0")
}
t.Logf("✓ 默认选项测试通过(MaxErrorCount=%d)", parser.options.MaxErrorCount)
}
// TestNewValidationParser_CustomOptions 测试自定义选项
func TestNewValidationParser_CustomOptions(t *testing.T) {
options := &ValidationParserOptions{
StrictMode: true,
AllowEmpty: false,
CheckConflicts: true,
ValidateTargets: true,
ValidateNetwork: true,
MaxErrorCount: 10,
}
parser := NewValidationParser(options)
if parser.options.StrictMode != true {
t.Error("StrictMode应该为true")
}
if parser.options.MaxErrorCount != 10 {
t.Error("MaxErrorCount应该为10")
}
t.Logf("✓ 自定义选项测试通过")
}
// =============================================================================
// ValidationParser - 基础验证测试
// =============================================================================
// TestValidationParser_Parse_NoTargets 测试无目标验证
//
// 验证:无目标时返回错误(AllowEmpty=false)
func TestValidationParser_Parse_NoTargets(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
AllowEmpty: false,
MaxErrorCount: 10,
})
input := &ValidationInput{
ScanMode: "all",
HasHosts: false,
HasURLs: false,
LocalMode: false,
}
result, err := parser.Parse(input, nil, nil)
if err != nil {
t.Fatalf("Parse不应返回err: %v", err)
}
if result.Success {
t.Error("无目标时Success应该为false")
}
if len(result.Errors) == 0 {
t.Error("无目标时应该有错误")
}
// 验证错误消息
hasTargetError := false
for _, e := range result.Errors {
if strings.Contains(e.Error(), "目标") {
hasTargetError = true
break
}
}
if !hasTargetError {
t.Error("应该包含目标相关的错误消息")
}
t.Logf("✓ 无目标验证测试通过(错误数=%d)", len(result.Errors))
}
// TestValidationParser_Parse_AllowEmpty 测试允许空配置
func TestValidationParser_Parse_AllowEmpty(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
AllowEmpty: true,
MaxErrorCount: 10,
})
input := &ValidationInput{
ScanMode: "",
HasHosts: false,
HasURLs: false,
LocalMode: false,
}
result, err := parser.Parse(input, nil, nil)
if err != nil {
t.Fatalf("Parse不应返回err: %v", err)
}
// AllowEmpty=true时,无目标只警告不报错
if !result.Success {
t.Error("AllowEmpty=true时Success应该为true")
}
if len(result.Warnings) == 0 {
t.Error("应该有警告")
}
t.Logf("✓ AllowEmpty测试通过(警告数=%d)", len(result.Warnings))
}
// TestValidationParser_Parse_ValidScanModes 测试有效扫描模式
func TestValidationParser_Parse_ValidScanModes(t *testing.T) {
parser := NewValidationParser(nil)
validModes := []string{"all", "icmp", "ssh", "mysql", ""}
for _, mode := range validModes {
t.Run(mode, func(t *testing.T) {
input := &ValidationInput{
ScanMode: mode,
HasHosts: true,
}
result, err := parser.Parse(input, nil, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
if !result.Success {
t.Errorf("模式%q应该有效,但Success=false,错误: %v", mode, result.Errors)
}
t.Logf("✓ 模式%q验证通过", mode)
})
}
}
// =============================================================================
// ValidationParser - 冲突检测测试
// =============================================================================
// TestValidationParser_Parse_ConflictMultipleScanModes 测试多种扫描模式冲突
//
// 验证:同时指定多种扫描模式时报错
func TestValidationParser_Parse_ConflictMultipleScanModes(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
CheckConflicts: true,
MaxErrorCount: 10,
})
tests := []struct {
name string
input *ValidationInput
hasConflict bool
}{
{
name: "主机+URL冲突",
input: &ValidationInput{
HasHosts: true,
HasURLs: true,
},
hasConflict: true,
},
{
name: "主机+本地模式冲突",
input: &ValidationInput{
HasHosts: true,
LocalMode: true,
},
hasConflict: true,
},
{
name: "URL+本地模式冲突",
input: &ValidationInput{
HasURLs: true,
LocalMode: true,
},
hasConflict: true,
},
{
name: "三种模式同时冲突",
input: &ValidationInput{
HasHosts: true,
HasURLs: true,
LocalMode: true,
},
hasConflict: true,
},
{
name: "仅主机-无冲突",
input: &ValidationInput{
HasHosts: true,
},
hasConflict: false,
},
{
name: "仅URL-无冲突",
input: &ValidationInput{
HasURLs: true,
},
hasConflict: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parser.Parse(tt.input, nil, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
if tt.hasConflict {
if result.Success {
t.Error("有冲突时Success应该为false")
}
if len(result.Errors) == 0 {
t.Error("应该有冲突错误")
}
// 验证错误消息包含"扫描模式"
hasConflictError := false
for _, e := range result.Errors {
if strings.Contains(e.Error(), "扫描模式") {
hasConflictError = true
break
}
}
if !hasConflictError {
t.Error("应该包含扫描模式冲突的错误")
}
} else {
if !result.Success {
t.Errorf("无冲突时Success应该为true,错误: %v", result.Errors)
}
}
t.Logf("✓ %s 测试通过", tt.name)
})
}
}
// TestValidationParser_Parse_ProxyPingWarning 测试代理+Ping警告
//
// 验证:代理模式下未禁用Ping时给出警告
func TestValidationParser_Parse_ProxyPingWarning(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
CheckConflicts: true,
MaxErrorCount: 10,
})
tests := []struct {
name string
hasProxy bool
disablePing bool
wantWarning bool
}{
{
name: "代理+Ping启用-有警告",
hasProxy: true,
disablePing: false,
wantWarning: true,
},
{
name: "代理+Ping禁用-无警告",
hasProxy: true,
disablePing: true,
wantWarning: false,
},
{
name: "无代理+Ping启用-无警告",
hasProxy: false,
disablePing: false,
wantWarning: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := &ValidationInput{
HasHosts: true,
HasProxy: tt.hasProxy,
DisablePing: tt.disablePing,
}
result, err := parser.Parse(input, nil, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
hasPingWarning := false
for _, w := range result.Warnings {
if strings.Contains(w, "Ping") || strings.Contains(w, "代理") {
hasPingWarning = true
break
}
}
if tt.wantWarning && !hasPingWarning {
t.Error("应该有代理Ping警告")
}
if !tt.wantWarning && hasPingWarning {
t.Errorf("不应该有警告,实际警告: %v", result.Warnings)
}
t.Logf("✓ %s 测试通过", tt.name)
})
}
}
// =============================================================================
// ValidationParser - 逻辑验证测试
// =============================================================================
// TestValidationParser_Parse_ExcludePortsLogic 测试排除端口逻辑
//
// 验证:排除端口但未指定端口时给出警告
func TestValidationParser_Parse_ExcludePortsLogic(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
ValidateTargets: true,
MaxErrorCount: 10,
})
config := &ParsedConfig{
Targets: &TargetConfig{
Hosts: []string{"192.168.1.1"},
Ports: []int{}, // 无端口
ExcludePorts: []int{80, 443, 8080}, // 但有排除端口
},
}
input := &ValidationInput{
HasHosts: true,
HasPorts: false,
}
result, err := parser.Parse(input, config, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
// 应该有警告
hasExcludeWarning := false
for _, w := range result.Warnings {
if strings.Contains(w, "排除端口") {
hasExcludeWarning = true
break
}
}
if !hasExcludeWarning {
t.Errorf("排除端口逻辑错误时应该有警告,实际警告: %v", result.Warnings)
}
t.Logf("✓ 排除端口逻辑测试通过")
}
// =============================================================================
// ValidationParser - 性能检查测试
// =============================================================================
// TestValidationParser_Parse_PerformanceLargeTargets 测试大量目标警告
//
// 验证:目标数量过多时给出性能警告
func TestValidationParser_Parse_PerformanceLargeTargets(t *testing.T) {
parser := NewValidationParser(nil)
// 创建大量目标:1000个主机 x 1000个端口 = 100万目标
hosts := make([]string, 1000)
for i := 0; i < 1000; i++ {
hosts[i] = "192.168.1.1"
}
ports := make([]int, 1000)
for i := 0; i < 1000; i++ {
ports[i] = i + 1
}
config := &ParsedConfig{
Targets: &TargetConfig{
Hosts: hosts,
Ports: ports,
},
}
input := &ValidationInput{
HasHosts: true,
HasPorts: true,
}
result, err := parser.Parse(input, config, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
// 应该有性能警告
hasPerformanceWarning := false
for _, w := range result.Warnings {
if strings.Contains(w, "大量目标") || strings.Contains(w, "耗时") {
hasPerformanceWarning = true
break
}
}
if !hasPerformanceWarning {
t.Errorf("大量目标时应该有性能警告,实际警告: %v", result.Warnings)
}
t.Logf("✓ 大量目标性能警告测试通过")
}
// TestValidationParser_Parse_PerformanceManyPorts 测试端口数量警告
func TestValidationParser_Parse_PerformanceManyPorts(t *testing.T) {
parser := NewValidationParser(nil)
// 创建大量端口
ports := make([]int, 10000)
for i := 0; i < 10000; i++ {
ports[i] = i + 1
}
config := &ParsedConfig{
Targets: &TargetConfig{
Hosts: []string{"192.168.1.1"},
Ports: ports,
},
}
input := &ValidationInput{
HasHosts: true,
HasPorts: true,
}
result, err := parser.Parse(input, config, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
// 应该有端口数量警告
hasPortWarning := false
for _, w := range result.Warnings {
if strings.Contains(w, "端口") {
hasPortWarning = true
break
}
}
if !hasPortWarning {
t.Errorf("大量端口时应该有警告,实际警告: %v", result.Warnings)
}
t.Logf("✓ 端口数量警告测试通过")
}
// =============================================================================
// ValidationParser - 错误数量限制测试
// =============================================================================
// TestValidationParser_Parse_MaxErrorCount 测试错误数量限制
//
// 验证:错误超过MaxErrorCount时截断
func TestValidationParser_Parse_MaxErrorCount(t *testing.T) {
parser := NewValidationParser(&ValidationParserOptions{
MaxErrorCount: 3,
CheckConflicts: true,
})
// 创建多个错误:多种扫描模式冲突
input := &ValidationInput{
HasHosts: true,
HasURLs: true,
LocalMode: true,
HasProxy: true,
// 这会产生至少1个冲突错误
}
result, err := parser.Parse(input, nil, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
if len(result.Errors) > 3 {
t.Errorf("错误数量应该被限制为%d,实际%d", 3, len(result.Errors))
}
// 注意:如果实际错误数<=MaxErrorCount,不会有截断警告
// 这是正常行为,不算失败
if len(result.Errors) <= 3 {
t.Logf("✓ 错误数量限制测试通过(限制=%d,实际=%d,无需截断)", 3, len(result.Errors))
} else {
// 只有超过限制才需要截断警告
hasTruncateWarning := false
for _, w := range result.Warnings {
if strings.Contains(w, "仅显示") || strings.Contains(w, "过多") {
hasTruncateWarning = true
break
}
}
if !hasTruncateWarning {
t.Error("超过限制时应该有错误截断警告")
}
t.Logf("✓ 错误数量限制测试通过(限制=%d,截断后=%d)", 3, len(result.Errors))
}
}
// =============================================================================
// ValidationParser - nil输入测试
// =============================================================================
// TestValidationParser_Parse_NilInput 测试nil输入
//
// 验证:nil输入时返回错误
func TestValidationParser_Parse_NilInput(t *testing.T) {
parser := NewValidationParser(nil)
result, err := parser.Parse(nil, nil, nil)
if err == nil {
t.Error("nil输入应该返回错误")
}
if result != nil {
t.Error("nil输入时result应该为nil")
}
if !strings.Contains(err.Error(), "空") {
t.Errorf("错误消息应该提示空输入,实际: %v", err)
}
t.Logf("✓ nil输入测试通过")
}
// =============================================================================
// ValidationParser - 成功场景测试
// =============================================================================
// TestValidationParser_Parse_Success 测试正常验证通过
func TestValidationParser_Parse_Success(t *testing.T) {
parser := NewValidationParser(nil)
input := &ValidationInput{
ScanMode: "all",
HasHosts: true,
HasPorts: true,
DisablePing: false,
}
config := &ParsedConfig{
Targets: &TargetConfig{
Hosts: []string{"192.168.1.1"},
Ports: []int{80, 443},
},
}
result, err := parser.Parse(input, config, nil)
if err != nil {
t.Fatalf("Parse失败: %v", err)
}
if !result.Success {
t.Errorf("正常配置应该Success=true,错误: %v", result.Errors)
}
if len(result.Errors) > 0 {
t.Errorf("正常配置不应有错误: %v", result.Errors)
}
// ParseTime可能为0(如果验证非常快)
if result.ParseTime < 0 {
t.Error("ParseTime不应该为负数")
}
if result.Config == nil || result.Config.Validation == nil {
t.Error("result.Config.Validation不应为nil")
}
t.Logf("✓ 成功场景测试通过(耗时=%v)", result.ParseTime)
}