mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-02-09 02:09:17 +08:00
284 lines
7.9 KiB
Go
284 lines
7.9 KiB
Go
package core
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
"github.com/shadow1ng/fscan/plugins"
|
||
)
|
||
|
||
// PluginFilterType 插件过滤类型
|
||
type PluginFilterType int
|
||
|
||
const (
|
||
// FilterNone 不过滤
|
||
FilterNone PluginFilterType = iota
|
||
// FilterLocal 仅本地插件
|
||
FilterLocal
|
||
// FilterService 仅服务插件(排除本地)
|
||
FilterService
|
||
// FilterWeb 仅Web插件
|
||
FilterWeb
|
||
)
|
||
|
||
// BaseScanStrategy 扫描策略基础类
|
||
type BaseScanStrategy struct {
|
||
strategyName string
|
||
filterType PluginFilterType
|
||
}
|
||
|
||
// NewBaseScanStrategy 创建基础扫描策略
|
||
func NewBaseScanStrategy(name string, filterType PluginFilterType) *BaseScanStrategy {
|
||
return &BaseScanStrategy{
|
||
strategyName: name,
|
||
filterType: filterType,
|
||
}
|
||
}
|
||
|
||
// GetPlugins 获取插件列表
|
||
func (b *BaseScanStrategy) GetPlugins(config *common.Config) ([]string, bool) {
|
||
scanMode := config.Mode
|
||
// 如果指定了特定插件且不是"all"
|
||
if scanMode != "" && scanMode != "all" {
|
||
requestedPlugins := parsePluginList(scanMode)
|
||
if len(requestedPlugins) == 0 {
|
||
requestedPlugins = []string{scanMode}
|
||
}
|
||
|
||
// 验证插件是否存在
|
||
var validPlugins []string
|
||
var missingPlugins []string
|
||
for _, name := range requestedPlugins {
|
||
if b.pluginExists(name) {
|
||
validPlugins = append(validPlugins, name)
|
||
} else {
|
||
missingPlugins = append(missingPlugins, name)
|
||
}
|
||
}
|
||
|
||
// 警告用户显式指定的插件不存在
|
||
// 注意:使用fmt.Fprintf直接输出到stderr,确保错误消息不会被日志级别过滤
|
||
for _, name := range missingPlugins {
|
||
errMsg := i18n.Tr("scan_plugin_not_found", name)
|
||
fmt.Fprintf(os.Stderr, "[ERROR] %s\n", errMsg)
|
||
}
|
||
|
||
return validPlugins, true
|
||
}
|
||
|
||
// 未指定或使用"all":根据策略类型获取对应插件
|
||
return b.getPluginsByFilterType(), false
|
||
}
|
||
|
||
// IsPluginApplicableByName 根据插件名称判断是否适用
|
||
func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHost string, targetPort int, isCustomMode bool, config *common.Config) bool {
|
||
// 首先检查插件是否存在
|
||
if !b.pluginExists(pluginName) {
|
||
return false
|
||
}
|
||
|
||
// 检查端口匹配和过滤器类型
|
||
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
|
||
}
|
||
|
||
func (b *BaseScanStrategy) pluginExists(pluginName string) bool {
|
||
return plugins.Exists(pluginName)
|
||
}
|
||
|
||
func (b *BaseScanStrategy) getPluginPorts(pluginName string) []int {
|
||
return plugins.GetPluginPorts(pluginName)
|
||
}
|
||
|
||
func (b *BaseScanStrategy) isWebPlugin(pluginName string) bool {
|
||
return plugins.HasType(pluginName, plugins.PluginTypeWeb)
|
||
}
|
||
|
||
func (b *BaseScanStrategy) isLocalPlugin(pluginName string) bool {
|
||
return plugins.HasType(pluginName, plugins.PluginTypeLocal)
|
||
}
|
||
|
||
func (b *BaseScanStrategy) isLocalPluginExplicitlySpecified(pluginName string, config *common.Config) bool {
|
||
return config.LocalPlugin == pluginName
|
||
}
|
||
|
||
// isPluginApplicableToPortWithHost 检查插件是否适用于指定端口
|
||
func (b *BaseScanStrategy) isPluginApplicableToPortWithHost(pluginName string, targetHost string, targetPort int) bool {
|
||
if b.isWebPlugin(pluginName) {
|
||
return IsMarkedWebService(targetHost, targetPort)
|
||
}
|
||
|
||
pluginPorts := b.getPluginPorts(pluginName)
|
||
|
||
// 无端口限制的插件适用于所有端口
|
||
if len(pluginPorts) == 0 {
|
||
return true
|
||
}
|
||
|
||
// 有端口限制的插件:检查端口匹配
|
||
if targetPort > 0 {
|
||
for _, port := range pluginPorts {
|
||
if port == targetPort {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool {
|
||
return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort)
|
||
}
|
||
|
||
// isPluginPassesFilterType 检查插件是否通过过滤器类型检查
|
||
func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomMode bool, config *common.Config) bool {
|
||
// 自定义模式下强制运行所有明确指定的插件
|
||
if isCustomMode {
|
||
return true
|
||
}
|
||
|
||
// 应用过滤器类型检查
|
||
switch b.filterType {
|
||
case FilterLocal:
|
||
// 本地扫描策略:只允许本地插件且必须通过-local参数明确指定
|
||
if b.isLocalPlugin(pluginName) {
|
||
return b.isLocalPluginExplicitlySpecified(pluginName, config)
|
||
}
|
||
return false
|
||
case FilterService:
|
||
// 服务扫描策略:排除本地插件
|
||
return !b.isLocalPlugin(pluginName)
|
||
case FilterWeb:
|
||
// Web扫描策略:只允许Web插件
|
||
return b.isWebPlugin(pluginName)
|
||
default:
|
||
// 无过滤器:本地插件需要明确指定,其他插件都允许
|
||
if b.isLocalPlugin(pluginName) {
|
||
return b.isLocalPluginExplicitlySpecified(pluginName, config)
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
// LogPluginInfo 输出插件信息
|
||
func (b *BaseScanStrategy) LogPluginInfo(config *common.Config) {
|
||
allPlugins, isCustomMode := b.GetPlugins(config)
|
||
|
||
var prefix string
|
||
switch b.filterType {
|
||
case FilterLocal:
|
||
prefix = i18n.GetText("concurrency_local_plugin")
|
||
case FilterService:
|
||
prefix = i18n.GetText("concurrency_service_plugin")
|
||
case FilterWeb:
|
||
prefix = i18n.GetText("concurrency_web_plugin")
|
||
default:
|
||
prefix = i18n.GetText("concurrency_plugin")
|
||
}
|
||
|
||
// 插件信息不再输出,减少干扰
|
||
_ = allPlugins
|
||
_ = isCustomMode
|
||
_ = prefix
|
||
}
|
||
|
||
// formatPluginList 格式化插件列表(超过5个时精简显示)
|
||
func formatPluginList(plugins []string) string {
|
||
if len(plugins) <= 5 {
|
||
return strings.Join(plugins, ", ")
|
||
}
|
||
return fmt.Sprintf("%s ... 等%d个", strings.Join(plugins[:5], ", "), len(plugins))
|
||
}
|
||
|
||
// ValidateConfiguration 验证扫描配置
|
||
func (b *BaseScanStrategy) ValidateConfiguration() error {
|
||
return nil
|
||
}
|
||
|
||
// LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示)
|
||
func (b *BaseScanStrategy) LogScanStart() {
|
||
// 服务扫描模式下不显示(插件信息已足够说明)
|
||
// 仅在本地/Web等特殊模式下显示
|
||
switch b.filterType {
|
||
case FilterLocal:
|
||
common.LogInfo(i18n.GetText("start_local_scan"))
|
||
case FilterWeb:
|
||
common.LogInfo(i18n.GetText("start_web_scan"))
|
||
}
|
||
}
|
||
|
||
// getPluginsByFilterType 根据过滤器类型获取插件列表
|
||
func (b *BaseScanStrategy) getPluginsByFilterType() []string {
|
||
allPlugins := plugins.All()
|
||
var filteredPlugins []string
|
||
|
||
switch b.filterType {
|
||
case FilterLocal:
|
||
// 本地扫描策略:只返回本地插件
|
||
for _, pluginName := range allPlugins {
|
||
if b.isLocalPlugin(pluginName) {
|
||
filteredPlugins = append(filteredPlugins, pluginName)
|
||
}
|
||
}
|
||
case FilterService:
|
||
// 服务扫描策略:排除本地插件和纯Web插件,保留服务插件
|
||
for _, pluginName := range allPlugins {
|
||
if !b.isLocalPlugin(pluginName) {
|
||
filteredPlugins = append(filteredPlugins, pluginName)
|
||
}
|
||
}
|
||
case FilterWeb:
|
||
// Web扫描策略:只返回Web插件
|
||
for _, pluginName := range allPlugins {
|
||
if b.isWebPlugin(pluginName) {
|
||
filteredPlugins = append(filteredPlugins, pluginName)
|
||
}
|
||
}
|
||
// 确保 webtitle 在 webpoc 之前执行,避免指纹识别竞态
|
||
sort.Slice(filteredPlugins, func(i, j int) bool {
|
||
// webtitle 必须在 webpoc 之前
|
||
if filteredPlugins[i] == "webtitle" {
|
||
return true
|
||
}
|
||
if filteredPlugins[j] == "webtitle" {
|
||
return false
|
||
}
|
||
if filteredPlugins[i] == "webpoc" {
|
||
return false
|
||
}
|
||
if filteredPlugins[j] == "webpoc" {
|
||
return true
|
||
}
|
||
// 其他插件保持字母顺序
|
||
return filteredPlugins[i] < filteredPlugins[j]
|
||
})
|
||
default:
|
||
// 无过滤器:返回所有插件
|
||
filteredPlugins = allPlugins
|
||
}
|
||
|
||
return filteredPlugins
|
||
}
|
||
|
||
// parsePluginList 解析插件列表字符串
|
||
func parsePluginList(pluginStr string) []string {
|
||
if pluginStr == "" {
|
||
return []string{}
|
||
}
|
||
|
||
// 支持逗号分隔的插件列表
|
||
plugins := strings.Split(pluginStr, ",")
|
||
result := []string{} // 初始化为空切片而非nil
|
||
for _, plugin := range plugins {
|
||
plugin = strings.TrimSpace(plugin)
|
||
if plugin != "" {
|
||
result = append(result, plugin)
|
||
}
|
||
}
|
||
return result
|
||
}
|