Files
fscan/core/scanner.go
ZacharyZcR 8312808769 refactor(logging): 统一日志前缀,删除废弃的 LogBase
- 删除 LogBase 函数,所有调用迁移到 LogInfo/LogError
- 新增 PrefixDebug ([.]) 前缀,所有日志级别现在都有前缀
- 修复日志输出缩进不一致的问题
- 删除未使用的 PrefixDefault 常量
2026-01-21 18:29:47 +08:00

355 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/output"
"github.com/shadow1ng/fscan/plugins"
"github.com/shadow1ng/fscan/webscan/lib"
)
// ScanStrategy 定义扫描策略接口
type ScanStrategy interface {
Execute(config *common.Config, state *common.State, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
GetPlugins(config *common.Config) ([]string, bool)
IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool
}
// ScanMode 扫描模式类型
type ScanMode int
const (
ScanModeService ScanMode = iota // 默认:服务扫描
ScanModeAlive // 仅存活检测
ScanModeLocal // 本地插件
ScanModeWeb // Web扫描
)
// strategyInfo 策略信息
type strategyInfo struct {
factory func() ScanStrategy
logKey string
}
var strategyRegistry = map[ScanMode]strategyInfo{
ScanModeAlive: {func() ScanStrategy { return NewAliveScanStrategy() }, "scan_mode_alive_selected"},
ScanModeLocal: {func() ScanStrategy { return NewLocalScanStrategy() }, "scan_mode_local_selected"},
ScanModeWeb: {func() ScanStrategy { return NewWebScanStrategy() }, "scan_mode_web_selected"},
ScanModeService: {func() ScanStrategy { return NewServiceScanStrategy() }, "scan_mode_service_selected"},
}
// determineScanMode 根据配置和状态确定扫描模式
func determineScanMode(config *common.Config, state *common.State) ScanMode {
switch {
case config.AliveOnly || config.Mode == "icmp":
return ScanModeAlive
case config.LocalMode:
return ScanModeLocal
case len(state.GetURLs()) > 0:
return ScanModeWeb
default:
return ScanModeService
}
}
// selectStrategy 根据扫描模式选择策略
func selectStrategy(config *common.Config, state *common.State, info common.HostInfo) ScanStrategy {
mode := determineScanMode(config, state)
if info, ok := strategyRegistry[mode]; ok {
return info.factory()
}
// 后备:默认服务扫描(理论上不会执行到这里)
return NewServiceScanStrategy()
}
// RunScan 执行整体扫描流程
func RunScan(info common.HostInfo, config *common.Config, state *common.State) {
// 初始化HTTP客户端静默无需日志
if err := lib.Inithttp(config); err != nil {
common.LogError(i18n.Tr("http_client_init_failed", err))
os.Exit(1)
}
// 选择策略
strategy := selectStrategy(config, state, info)
// 并发控制初始化
ch := make(chan struct{}, config.ThreadNum)
wg := sync.WaitGroup{}
// 执行策略
strategy.Execute(config, state, info, ch, &wg)
// 等待所有扫描完成
wg.Wait()
// 检查是否有活跃的连接需要维持
if state.IsReverseShellActive() || state.IsSocks5ProxyActive() || state.IsForwardShellActive() {
if state.IsReverseShellActive() {
common.LogInfo(i18n.GetText("active_reverse_shell"))
}
if state.IsSocks5ProxyActive() {
common.LogInfo(i18n.GetText("active_socks5_proxy"))
}
if state.IsForwardShellActive() {
common.LogInfo(i18n.GetText("active_forward_shell"))
}
common.LogInfo(i18n.GetText("press_ctrl_c_exit"))
// 优雅等待信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
common.LogInfo(i18n.GetText("received_exit_signal"))
}
// 完成扫描
finishScan(config, state)
}
// finishScan 完成扫描并输出结果
func finishScan(config *common.Config, state *common.State) {
// 确保进度条正确完成
if common.IsProgressActive() {
common.FinishProgressBar()
}
// 输出扫描完成信息
common.LogInfo(i18n.Tr("scan_task_complete", time.Since(state.GetStartTime()).Round(time.Millisecond), state.GetNum()))
// 输出性能统计 JSON如果启用
if config.Output.PerfStats {
fmt.Printf("\n[PERF_STATS_JSON]%s[/PERF_STATS_JSON]\n", state.GetPerfStatsJSON())
}
}
// ExecuteScanTasks 任务执行通用框架
func ExecuteScanTasks(config *common.Config, state *common.State, targets []common.HostInfo, strategy ScanStrategy, ch chan struct{}, wg *sync.WaitGroup) {
// 获取要执行的插件
pluginsToRun, isCustomMode := strategy.GetPlugins(config)
// 预计算任务数量用于进度条
taskCount := countApplicableTasks(targets, pluginsToRun, isCustomMode, strategy, config)
// 初始化进度条
if taskCount > 0 && config.Output.ShowProgress {
description := i18n.GetText("progress_scanning_description")
common.InitProgressBar(int64(taskCount), description)
}
// 流式执行任务,避免预构建大量任务对象
for _, target := range targets {
targetPort := target.Port
for _, pluginName := range pluginsToRun {
// 使用Exists检查避免不必要的插件实例创建
if !plugins.Exists(pluginName) {
continue
}
// 检查插件是否适用于当前目标
if strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
executeScanTask(config, state, pluginName, target, ch, wg)
}
}
}
}
// countApplicableTasks 计算适用的任务数量
func countApplicableTasks(targets []common.HostInfo, pluginsToRun []string, isCustomMode bool, strategy ScanStrategy, config *common.Config) int {
count := 0
for _, target := range targets {
targetPort := target.Port
for _, pluginName := range pluginsToRun {
// 使用Exists检查避免不必要的插件实例创建
if plugins.Exists(pluginName) &&
strategy.IsPluginApplicableByName(pluginName, target.Host, targetPort, isCustomMode, config) {
count++
}
}
}
return count
}
// executeScanTask 执行单个扫描任务
func executeScanTask(config *common.Config, state *common.State, pluginName string, target common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
wg.Add(1)
ch <- struct{}{} // 获取并发槽位
go func() {
// 开始监控插件任务
monitor := common.GetConcurrencyMonitor()
monitor.StartPluginTask()
defer func() {
// 捕获并记录任何可能的panic
if r := recover(); r != nil {
common.LogError(i18n.Tr("plugin_panic", pluginName, target.Host, target.Port, r))
}
// 更新统计和进度(任务真正完成时才更新)
state.IncrementNum()
common.UpdateProgressBar(1)
// 完成任务,释放资源
monitor.FinishPluginTask()
wg.Done()
<-ch // 释放并发槽位
}()
plugin := plugins.Get(pluginName)
if plugin != nil {
result := plugin.Scan(context.Background(), &target, config, state)
if result != nil {
if result.Success {
// 保存成功的扫描结果到文件
savePluginResult(&target, pluginName, result)
} else if result.Type == plugins.ResultTypeCredential {
// 凭据测试完成但未发现弱密码在error级别输出提示
common.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
} else if result.Error != nil {
// 其他类型的错误
common.LogError(i18n.Tr("plugin_scan_error", target.Host, target.Port, result.Error))
}
}
}
}()
}
// resultSerializer 结果序列化信息
type resultSerializer struct {
outputType output.ResultType
getStatus func(*plugins.Result, *common.HostInfo) string
fillDetail func(*plugins.Result, *common.HostInfo, map[string]interface{})
}
var resultSerializers = map[plugins.ResultType]resultSerializer{
plugins.ResultTypeCredential: {
outputType: output.TypeVuln,
getStatus: func(r *plugins.Result, _ *common.HostInfo) string {
return fmt.Sprintf("weak_credential: %s:%s", r.Username, r.Password)
},
fillDetail: func(r *plugins.Result, _ *common.HostInfo, d map[string]interface{}) {
d["service"] = r.Service
d["username"] = r.Username
d["password"] = r.Password
d["type"] = "weak_credential"
},
},
plugins.ResultTypeService: {
outputType: output.TypeService,
getStatus: func(r *plugins.Result, _ *common.HostInfo) string {
if r.Banner != "" {
return r.Banner
}
return r.Service
},
fillDetail: func(r *plugins.Result, _ *common.HostInfo, d map[string]interface{}) {
if r.Banner != "" {
d["banner"] = r.Banner
}
if r.Service != "" {
d["service"] = r.Service
}
},
},
plugins.ResultTypeVuln: {
outputType: output.TypeVuln,
getStatus: func(r *plugins.Result, _ *common.HostInfo) string {
// 优先使用VulInfo为空则回退到Banner
if r.VulInfo != "" {
return r.VulInfo
}
return r.Banner
},
fillDetail: func(r *plugins.Result, _ *common.HostInfo, d map[string]interface{}) {
// 优先使用VulInfo为空则回退到Banner
vuln := r.VulInfo
if vuln == "" {
vuln = r.Banner
}
d["vulnerability"] = vuln
d["service"] = r.Service
},
},
plugins.ResultTypeWeb: {
outputType: output.TypeService,
getStatus: func(_ *plugins.Result, _ *common.HostInfo) string { return "web" },
fillDetail: func(_ *plugins.Result, info *common.HostInfo, d map[string]interface{}) {
d["is_web"] = true
d["port"] = info.Port
},
},
}
var defaultSerializer = resultSerializer{
outputType: output.TypeService,
getStatus: func(r *plugins.Result, _ *common.HostInfo) string {
if r.Banner != "" {
return r.Banner
}
if r.Service != "" {
return r.Service
}
return "detected"
},
fillDetail: func(_ *plugins.Result, _ *common.HostInfo, _ map[string]interface{}) {},
}
// savePluginResult 保存插件扫描结果
func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.Result) {
if result == nil || !result.Success || result.Skipped {
return
}
// 获取序列化器
serializer, ok := resultSerializers[result.Type]
if !ok {
serializer = defaultSerializer
}
// 构建详情
details := map[string]interface{}{"plugin": pluginName}
serializer.fillDetail(result, info, details)
// 添加通用字段
addCommonDetails(result, details)
// 保存结果
target := info.Target()
_ = common.SaveResult(&output.ScanResult{
Time: time.Now(),
Type: serializer.outputType,
Target: target,
Status: serializer.getStatus(result, info),
Details: details,
})
}
// addCommonDetails 添加通用详情字段
func addCommonDetails(result *plugins.Result, details map[string]interface{}) {
if len(result.Fingerprints) > 0 {
details["fingerprints"] = result.Fingerprints
}
if result.Title != "" {
details["title"] = result.Title
}
if result.Status != 0 {
details["status"] = result.Status
}
if result.Server != "" {
details["server"] = result.Server
}
}