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)
442 lines
11 KiB
Go
442 lines
11 KiB
Go
package output
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
/*
|
|
buffer_test.go - ResultBuffer 高价值测试
|
|
|
|
测试重点:
|
|
1. 去重逻辑 - 不同结果类型的去重策略差异
|
|
2. 完整度评分 - 决定是否替换已有服务记录
|
|
3. 并发安全 - 多goroutine同时Add
|
|
*/
|
|
|
|
// =============================================================================
|
|
// 基本去重测试
|
|
// =============================================================================
|
|
|
|
// TestResultBuffer_HostDeduplication 测试主机去重
|
|
func TestResultBuffer_HostDeduplication(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 添加相同主机多次
|
|
for i := 0; i < 10; i++ {
|
|
buf.Add(&ScanResult{
|
|
Type: TypeHost,
|
|
Target: "192.168.1.1",
|
|
Status: "alive",
|
|
})
|
|
}
|
|
|
|
hosts, _, _, _ := buf.Summary()
|
|
if hosts != 1 {
|
|
t.Errorf("主机应去重为1个,实际 %d", hosts)
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_PortDeduplication 测试端口去重
|
|
func TestResultBuffer_PortDeduplication(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 相同IP:Port应去重
|
|
for i := 0; i < 5; i++ {
|
|
buf.Add(&ScanResult{
|
|
Type: TypePort,
|
|
Target: "192.168.1.1",
|
|
Details: map[string]interface{}{"port": 80},
|
|
})
|
|
}
|
|
|
|
// 不同端口不去重
|
|
buf.Add(&ScanResult{
|
|
Type: TypePort,
|
|
Target: "192.168.1.1",
|
|
Details: map[string]interface{}{"port": 443},
|
|
})
|
|
|
|
_, ports, _, _ := buf.Summary()
|
|
if ports != 2 {
|
|
t.Errorf("端口应有2个(80和443),实际 %d", ports)
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_ServiceDeduplication 测试服务去重
|
|
func TestResultBuffer_ServiceDeduplication(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 相同Target的服务应去重
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "http",
|
|
})
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "nginx",
|
|
})
|
|
|
|
_, _, services, _ := buf.Summary()
|
|
if services != 1 {
|
|
t.Errorf("相同Target的服务应去重为1个,实际 %d", services)
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_VulnDeduplication 测试漏洞去重
|
|
func TestResultBuffer_VulnDeduplication(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 相同Target+Status的漏洞应去重
|
|
for i := 0; i < 3; i++ {
|
|
buf.Add(&ScanResult{
|
|
Type: TypeVuln,
|
|
Target: "192.168.1.1:445",
|
|
Status: "MS17-010",
|
|
})
|
|
}
|
|
|
|
// 不同漏洞不去重
|
|
buf.Add(&ScanResult{
|
|
Type: TypeVuln,
|
|
Target: "192.168.1.1:445",
|
|
Status: "CVE-2020-0796",
|
|
})
|
|
|
|
_, _, _, vulns := buf.Summary()
|
|
if vulns != 2 {
|
|
t.Errorf("漏洞应有2个,实际 %d", vulns)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 完整度评分测试
|
|
// =============================================================================
|
|
|
|
// TestResultBuffer_CompletenessScore 测试完整度评分
|
|
func TestResultBuffer_CompletenessScore(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
tests := []struct {
|
|
name string
|
|
result *ScanResult
|
|
expectedScore int
|
|
}{
|
|
{
|
|
name: "空Details",
|
|
result: &ScanResult{Details: nil},
|
|
expectedScore: 0,
|
|
},
|
|
{
|
|
name: "只有status",
|
|
result: &ScanResult{Details: map[string]interface{}{"status": 200}},
|
|
expectedScore: 2,
|
|
},
|
|
{
|
|
name: "有server",
|
|
result: &ScanResult{Details: map[string]interface{}{"server": "nginx/1.18.0"}},
|
|
expectedScore: 2,
|
|
},
|
|
{
|
|
name: "有title",
|
|
result: &ScanResult{Details: map[string]interface{}{"title": "Welcome"}},
|
|
expectedScore: 1,
|
|
},
|
|
{
|
|
name: "有指纹-[]string",
|
|
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []string{"nginx"}}},
|
|
expectedScore: 3,
|
|
},
|
|
{
|
|
name: "有指纹-[]interface{}",
|
|
result: &ScanResult{Details: map[string]interface{}{"fingerprints": []interface{}{"apache", "php"}}},
|
|
expectedScore: 3,
|
|
},
|
|
{
|
|
name: "有banner",
|
|
result: &ScanResult{Details: map[string]interface{}{"banner": "SSH-2.0-OpenSSH"}},
|
|
expectedScore: 1,
|
|
},
|
|
{
|
|
name: "完整记录",
|
|
result: &ScanResult{
|
|
Details: map[string]interface{}{
|
|
"status": 200,
|
|
"server": "nginx",
|
|
"title": "Home",
|
|
"fingerprints": []string{"nginx", "php"},
|
|
"banner": "test",
|
|
},
|
|
},
|
|
expectedScore: 9, // 2+2+1+3+1
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
score := buf.CalculateCompleteness(tt.result)
|
|
if score != tt.expectedScore {
|
|
t.Errorf("完整度评分 = %d, 期望 %d", score, tt.expectedScore)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_ServiceUpdate 测试服务记录更新
|
|
//
|
|
// 当新记录比旧记录更完整时,应该替换
|
|
func TestResultBuffer_ServiceUpdate(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 先添加简单记录
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "http",
|
|
Details: map[string]interface{}{},
|
|
})
|
|
|
|
// 再添加更完整的记录
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "http",
|
|
Details: map[string]interface{}{
|
|
"status": 200,
|
|
"server": "nginx/1.18.0",
|
|
"title": "Welcome",
|
|
"fingerprints": []string{"nginx", "php"},
|
|
},
|
|
})
|
|
|
|
_, _, services, _ := buf.Summary()
|
|
if services != 1 {
|
|
t.Fatal("服务数量应为1")
|
|
}
|
|
|
|
// 验证是更完整的记录
|
|
if buf.ServiceResults[0].Details == nil {
|
|
t.Fatal("Details不应为nil")
|
|
}
|
|
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
|
t.Error("应保留更完整的记录")
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_ServiceNoDowngrade 测试不降级服务记录
|
|
//
|
|
// 当新记录不如旧记录完整时,不应替换
|
|
func TestResultBuffer_ServiceNoDowngrade(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 先添加完整记录
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "http",
|
|
Details: map[string]interface{}{
|
|
"status": 200,
|
|
"server": "nginx/1.18.0",
|
|
"fingerprints": []string{"nginx"},
|
|
},
|
|
})
|
|
|
|
// 再添加简单记录
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: "192.168.1.1:80",
|
|
Status: "http",
|
|
Details: map[string]interface{}{},
|
|
})
|
|
|
|
// 验证仍保留完整记录
|
|
if buf.ServiceResults[0].Details["server"] != "nginx/1.18.0" {
|
|
t.Error("不应降级到不完整的记录")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 并发安全测试
|
|
// =============================================================================
|
|
|
|
// TestResultBuffer_ConcurrentAdd 测试并发添加
|
|
func TestResultBuffer_ConcurrentAdd(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
const goroutines = 100
|
|
const resultsPerGoroutine = 100
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < resultsPerGoroutine; j++ {
|
|
// 每个goroutine添加不同类型的结果
|
|
switch j % 4 {
|
|
case 0:
|
|
buf.Add(&ScanResult{
|
|
Type: TypeHost,
|
|
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
|
})
|
|
case 1:
|
|
buf.Add(&ScanResult{
|
|
Type: TypePort,
|
|
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
|
Details: map[string]interface{}{"port": j},
|
|
})
|
|
case 2:
|
|
buf.Add(&ScanResult{
|
|
Type: TypeService,
|
|
Target: fmt.Sprintf("192.168.%d.%d:%d", id, j, j),
|
|
})
|
|
case 3:
|
|
buf.Add(&ScanResult{
|
|
Type: TypeVuln,
|
|
Target: fmt.Sprintf("192.168.%d.%d", id, j),
|
|
Status: fmt.Sprintf("CVE-%d", j),
|
|
})
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// 验证没有panic,数据完整
|
|
hosts, ports, services, vulns := buf.Summary()
|
|
total := hosts + ports + services + vulns
|
|
|
|
if total == 0 {
|
|
t.Error("并发添加后应有结果")
|
|
}
|
|
|
|
t.Logf("并发测试完成: %d hosts, %d ports, %d services, %d vulns",
|
|
hosts, ports, services, vulns)
|
|
}
|
|
|
|
// TestResultBuffer_ConcurrentSummary 测试并发获取摘要
|
|
func TestResultBuffer_ConcurrentSummary(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 预填充一些数据
|
|
for i := 0; i < 100; i++ {
|
|
buf.Add(&ScanResult{
|
|
Type: TypeHost,
|
|
Target: fmt.Sprintf("192.168.1.%d", i),
|
|
})
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(100)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
// 同时获取摘要和添加
|
|
buf.Summary()
|
|
buf.Add(&ScanResult{
|
|
Type: TypeHost,
|
|
Target: "10.0.0.1",
|
|
})
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// 没有panic即为成功
|
|
}
|
|
|
|
// =============================================================================
|
|
// 边界情况测试
|
|
// =============================================================================
|
|
|
|
// TestResultBuffer_NilResult 测试nil结果
|
|
func TestResultBuffer_NilResult(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
buf.Add(nil) // 不应panic
|
|
|
|
hosts, ports, services, vulns := buf.Summary()
|
|
if hosts+ports+services+vulns != 0 {
|
|
t.Error("添加nil后应无结果")
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_PortWithoutDetails 测试无Details的端口
|
|
func TestResultBuffer_PortWithoutDetails(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
buf.Add(&ScanResult{
|
|
Type: TypePort,
|
|
Target: "192.168.1.1",
|
|
Details: nil,
|
|
})
|
|
|
|
_, ports, _, _ := buf.Summary()
|
|
if ports != 1 {
|
|
t.Error("无Details的端口也应被添加")
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_Clear 测试清空
|
|
func TestResultBuffer_Clear(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 添加各类结果
|
|
buf.Add(&ScanResult{Type: TypeHost, Target: "192.168.1.1"})
|
|
buf.Add(&ScanResult{Type: TypePort, Target: "192.168.1.1", Details: map[string]interface{}{"port": 80}})
|
|
buf.Add(&ScanResult{Type: TypeService, Target: "192.168.1.1:80"})
|
|
buf.Add(&ScanResult{Type: TypeVuln, Target: "192.168.1.1", Status: "CVE-2021-1234"})
|
|
|
|
// 清空
|
|
buf.Clear()
|
|
|
|
hosts, ports, services, vulns := buf.Summary()
|
|
if hosts+ports+services+vulns != 0 {
|
|
t.Error("Clear后应无结果")
|
|
}
|
|
|
|
// 验证可以继续添加
|
|
buf.Add(&ScanResult{Type: TypeHost, Target: "10.0.0.1"})
|
|
hosts, _, _, _ = buf.Summary()
|
|
if hosts != 1 {
|
|
t.Error("Clear后应能继续添加")
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_EmptyFingerprints 测试空指纹数组
|
|
func TestResultBuffer_EmptyFingerprints(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
// 空字符串数组
|
|
score1 := buf.CalculateCompleteness(&ScanResult{
|
|
Details: map[string]interface{}{"fingerprints": []string{}},
|
|
})
|
|
if score1 != 0 {
|
|
t.Errorf("空指纹数组不应加分,实际 %d", score1)
|
|
}
|
|
|
|
// 空interface数组
|
|
score2 := buf.CalculateCompleteness(&ScanResult{
|
|
Details: map[string]interface{}{"fingerprints": []interface{}{}},
|
|
})
|
|
if score2 != 0 {
|
|
t.Errorf("空interface数组不应加分,实际 %d", score2)
|
|
}
|
|
}
|
|
|
|
// TestResultBuffer_StatusZero 测试status为0
|
|
func TestResultBuffer_StatusZero(t *testing.T) {
|
|
buf := NewResultBuffer()
|
|
|
|
score := buf.CalculateCompleteness(&ScanResult{
|
|
Details: map[string]interface{}{"status": 0},
|
|
})
|
|
if score != 0 {
|
|
t.Errorf("status为0不应加分,实际 %d", score)
|
|
}
|
|
}
|