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
+329
View File
@@ -0,0 +1,329 @@
package plugins
import (
"testing"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/config"
)
/*
init_test.go - 插件系统核心逻辑测试
测试目标:GenerateCredentials 函数
价值:这个函数生成所有服务的暴力破解凭据,逻辑错误会导致:
- 漏掉有效凭据(少生成)
- 浪费时间测试重复凭据(多生成)
- {user} 占位符不生效(密码错误)
"凭据生成是暴力破解的弹药库。弹药错了,仗就打不赢。"
*/
// =============================================================================
// GenerateCredentials - 核心凭据生成逻辑
// =============================================================================
func TestGenerateCredentials_UserPassPairs_Priority(t *testing.T) {
/*
关键测试:UserPassPairs 应该优先于笛卡尔积
为什么重要:
- UserPassPairs 是用户精确指定的凭据对
- 不应该和 Userdict/Passwords 混合使用
- 避免生成大量无用凭据
Bug 场景:
- UserPassPairs + 笛卡尔积混用 → 凭据爆炸
- 忽略 UserPassPairs → 用户指定的凭据不生效
*/
// 保存原始值
cfg := common.GetGlobalConfig()
origUserPassPairs := cfg.Credentials.UserPassPairs
origUserdict := cfg.Credentials.Userdict
origPasswords := cfg.Credentials.Passwords
defer func() {
cfg.Credentials.UserPassPairs = origUserPassPairs
cfg.Credentials.Userdict = origUserdict
cfg.Credentials.Passwords = origPasswords
}()
// 设置测试数据
cfg.Credentials.UserPassPairs = []config.CredentialPair{
{Username: "admin", Password: "Admin@123"},
{Username: "root", Password: "Root@456"},
}
// 即使有 Userdict 和 Passwords,也应该被忽略
cfg.Credentials.Userdict = map[string][]string{
"mysql": {"mysql", "user1", "user2"},
}
cfg.Credentials.Passwords = []string{"pass1", "pass2", "pass3"}
result := GenerateCredentials("mysql", cfg)
// 验证:只有 2 个凭据(来自 UserPassPairs
if len(result) != 2 {
t.Errorf("Expected 2 credentials from UserPassPairs, got %d", len(result))
}
// 验证:凭据内容正确
expected := map[string]string{
"admin": "Admin@123",
"root": "Root@456",
}
for _, cred := range result {
if expectedPass, exists := expected[cred.Username]; exists {
if cred.Password != expectedPass {
t.Errorf("Username %s: expected password %s, got %s",
cred.Username, expectedPass, cred.Password)
}
} else {
t.Errorf("Unexpected username: %s", cred.Username)
}
}
t.Logf("✓ UserPassPairs 优先: 生成 %d 个精确凭据对", len(result))
}
func TestGenerateCredentials_CartesianProduct(t *testing.T) {
/*
关键测试:笛卡尔积应该正确生成 users × passwords
为什么重要:
- 笛卡尔积是默认的凭据生成方式
- 逻辑错误会导致漏掉有效凭据
Bug 场景:
- 嵌套循环顺序错误
- 重复生成凭据
- 遗漏某些组合
*/
// 保存原始值
cfg := common.GetGlobalConfig()
origUserPassPairs := cfg.Credentials.UserPassPairs
origUserdict := cfg.Credentials.Userdict
origPasswords := cfg.Credentials.Passwords
defer func() {
cfg.Credentials.UserPassPairs = origUserPassPairs
cfg.Credentials.Userdict = origUserdict
cfg.Credentials.Passwords = origPasswords
}()
// 清空 UserPassPairs,使用笛卡尔积
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
cfg.Credentials.Userdict = map[string][]string{
"ssh": {"root", "admin"},
}
cfg.Credentials.Passwords = []string{"123456", "password"}
result := GenerateCredentials("ssh", cfg)
// 验证:应该有 2 × 2 = 4 个凭据
expected := 2 * 2
if len(result) != expected {
t.Errorf("Expected %d credentials (2 users × 2 passwords), got %d", expected, len(result))
}
// 验证:所有组合都存在
expectedCombos := map[string]string{
"root:123456": "root",
"root:password": "root",
"admin:123456": "admin",
"admin:password": "admin",
}
found := make(map[string]bool)
for _, cred := range result {
combo := cred.Username + ":" + cred.Password
found[combo] = true
}
for combo := range expectedCombos {
if !found[combo] {
t.Errorf("Missing combination: %s", combo)
}
}
t.Logf("✓ 笛卡尔积正确: 2 users × 2 passwords = %d 凭据", len(result))
}
func TestGenerateCredentials_PlaceholderReplacement(t *testing.T) {
/*
关键测试:{user} 占位符应该被替换为用户名
为什么重要:
- 很多服务的默认密码是用户名(如 mysql:mysql
- {user} 占位符是实现这个需求的关键
Bug 场景:
- {user} 不替换 → 密码字面值是 "{user}"
- 替换错误 → 密码是其他用户名
*/
// 保存原始值
cfg := common.GetGlobalConfig()
origUserPassPairs := cfg.Credentials.UserPassPairs
origUserdict := cfg.Credentials.Userdict
origPasswords := cfg.Credentials.Passwords
defer func() {
cfg.Credentials.UserPassPairs = origUserPassPairs
cfg.Credentials.Userdict = origUserdict
cfg.Credentials.Passwords = origPasswords
}()
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
cfg.Credentials.Userdict = map[string][]string{
"mysql": {"root", "mysql"},
}
cfg.Credentials.Passwords = []string{"{user}", "{user}123"}
result := GenerateCredentials("mysql", cfg)
// 验证:应该有 2 × 2 = 4 个凭据
expected := 2 * 2
if len(result) != expected {
t.Errorf("Expected %d credentials, got %d", expected, len(result))
}
// 验证:{user} 被正确替换
expectedCombos := map[string]string{
"root:root": "root", // {user} → root
"root:root123": "root", // {user}123 → root123
"mysql:mysql": "mysql", // {user} → mysql
"mysql:mysql123": "mysql", // {user}123 → mysql123
}
found := make(map[string]bool)
for _, cred := range result {
combo := cred.Username + ":" + cred.Password
found[combo] = true
// 验证:密码中不应该有字面值 "{user}"
if cred.Password == "{user}" || cred.Password == "{user}123" {
t.Errorf("Placeholder not replaced: %s:%s", cred.Username, cred.Password)
}
}
for combo := range expectedCombos {
if !found[combo] {
t.Errorf("Missing combination: %s", combo)
}
}
t.Logf("✓ {user} 占位符正确替换: 生成 %d 个凭据", len(result))
}
func TestGenerateCredentials_DefaultValues(t *testing.T) {
/*
关键测试:空字典时应该使用默认值
为什么重要:
- 某些服务可能没有预定义字典
- 空字典不应该导致零凭据
Bug 场景:
- 空字典 → 零凭据 → 完全不测试
- 默认值错误 → 浪费时间测试无用凭据
*/
// 保存原始值
cfg := common.GetGlobalConfig()
origUserPassPairs := cfg.Credentials.UserPassPairs
origUserdict := cfg.Credentials.Userdict
origPasswords := cfg.Credentials.Passwords
defer func() {
cfg.Credentials.UserPassPairs = origUserPassPairs
cfg.Credentials.Userdict = origUserdict
cfg.Credentials.Passwords = origPasswords
}()
cfg.Credentials.UserPassPairs = []config.CredentialPair{}
cfg.Credentials.Userdict = map[string][]string{} // 空字典
cfg.Credentials.Passwords = []string{} // 空密码列表
result := GenerateCredentials("unknown_service", cfg)
// 验证:应该有默认凭据
// 默认用户: admin, root, administrator, user, guest, ""6个)
// 默认密码: "", admin, root, password, 1234565个)
// 预期:6 × 5 = 30 个凭据
expectedUsers := []string{"admin", "root", "administrator", "user", "guest", ""}
expectedPasswords := []string{"", "admin", "root", "password", "123456"}
expectedTotal := len(expectedUsers) * len(expectedPasswords)
if len(result) != expectedTotal {
t.Errorf("Expected %d credentials with default values, got %d", expectedTotal, len(result))
}
// 验证:默认用户和密码都被使用
usersFound := make(map[string]bool)
passwordsFound := make(map[string]bool)
for _, cred := range result {
usersFound[cred.Username] = true
passwordsFound[cred.Password] = true
}
for _, user := range expectedUsers {
if !usersFound[user] {
t.Errorf("Default user not found: %s", user)
}
}
for _, pass := range expectedPasswords {
if !passwordsFound[pass] {
t.Errorf("Default password not found: %s", pass)
}
}
t.Logf("✓ 默认值正确: %d users × %d passwords = %d 凭据",
len(expectedUsers), len(expectedPasswords), len(result))
}
func TestGenerateCredentials_EmptyUserPassPairs(t *testing.T) {
/*
关键测试:空的 UserPassPairs 应该回退到笛卡尔积
为什么重要:
- UserPassPairs = [] 和 nil 行为应该一致
- 避免特殊情况
Bug 场景:
- 空数组被当作"有值" → 生成零凭据
*/
// 保存原始值
cfg := common.GetGlobalConfig()
origUserPassPairs := cfg.Credentials.UserPassPairs
origUserdict := cfg.Credentials.Userdict
origPasswords := cfg.Credentials.Passwords
defer func() {
cfg.Credentials.UserPassPairs = origUserPassPairs
cfg.Credentials.Userdict = origUserdict
cfg.Credentials.Passwords = origPasswords
}()
cfg.Credentials.UserPassPairs = []config.CredentialPair{} // 空数组
cfg.Credentials.Userdict = map[string][]string{
"test": {"user1"},
}
cfg.Credentials.Passwords = []string{"pass1"}
result := GenerateCredentials("test", cfg)
// 验证:应该回退到笛卡尔积(1 × 1 = 1)
if len(result) != 1 {
t.Errorf("Expected 1 credential (fallback to cartesian), got %d", len(result))
}
if result[0].Username != "user1" || result[0].Password != "pass1" {
t.Errorf("Expected user1:pass1, got %s:%s", result[0].Username, result[0].Password)
}
t.Logf("✓ 空 UserPassPairs 正确回退到笛卡尔积")
}