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 state *common.State } // NewBaseScanStrategy 创建基础扫描策略 func NewBaseScanStrategy(name string, filterType PluginFilterType) *BaseScanStrategy { return &BaseScanStrategy{ strategyName: name, filterType: filterType, } } // SetState 注入 session state(用于 per-session 服务缓存) func (b *BaseScanStrategy) SetState(state *common.State) { b.state = state } // 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 } // 显式指定插件时,尊重调用方选择,不再强制使用插件默认端口过滤。 if isCustomMode { return b.isPluginPassesFilterType(pluginName, isCustomMode, config) } // -full 模式下,web 插件对所有开放端口生效(跳过 IsMarkedWebService 检查) if config.POC.Full && b.isWebPlugin(pluginName) { return b.isPluginPassesFilterType(pluginName, isCustomMode, config) } // 检查端口匹配和过滤器类型 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) isUDPPlugin(pluginName string) bool { return plugins.IsUDP(pluginName) } 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 IsMarkedWebServiceWithState(b.state, targetHost, targetPort) } pluginPorts := b.getPluginPorts(pluginName) // 无端口限制的插件适用于所有端口 if len(pluginPorts) == 0 { return true } // 有端口限制的插件:检查端口匹配 if targetPort > 0 { for _, port := range pluginPorts { if port == targetPort { return true } } } // 端口不匹配时,按指纹识别结果匹配 // 例:8881 端口上识别到 ssh 服务 → ssh 插件应该执行 if targetHost != "" && targetPort > 0 { if info, ok := GetCachedServiceInfoWithState(b.state, targetHost, targetPort); ok && info != nil { if strings.EqualFold(info.Name, pluginName) { return true } } } return false } func (b *BaseScanStrategy) isPluginApplicableToPort(pluginName string, targetPort int) bool { if b.isWebPlugin(pluginName) { return true } return b.isPluginApplicableToPortWithHost(pluginName, "", targetPort) } // isPluginPassesFilterType 检查插件是否通过过滤器类型检查 func (b *BaseScanStrategy) isPluginPassesFilterType(pluginName string, isCustomMode bool, config *common.Config) bool { // UDP 插件有独立分发路径,不参与 TCP 端口匹配流水线 if b.isUDPPlugin(pluginName) { return false } // 自定义模式下强制运行所有明确指定的插件 if isCustomMode { return true } // 应用过滤器类型检查 switch b.filterType { case FilterLocal: // 本地扫描策略:只允许本地插件且必须通过-local参数明确指定 if b.isLocalPlugin(pluginName) { return b.isLocalPluginExplicitlySpecified(pluginName, config) } return false case FilterService: // 服务扫描策略:排除本地插件和UDP插件(UDP有独立分发路径) return !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName) case FilterWeb: // Web扫描策略:只允许Web插件 return b.isWebPlugin(pluginName) default: // 无过滤器:本地插件需要明确指定,其他插件都允许 if b.isLocalPlugin(pluginName) { return b.isLocalPluginExplicitlySpecified(pluginName, config) } return true } } // LogPluginInfo 默认不输出插件信息(service 默认端口模式有意保持安静,减少干扰)。 // 子类 LocalScanStrategy / ServiceScanStrategy 按需重写。 func (b *BaseScanStrategy) LogPluginInfo(config *common.Config, session *common.ScanSession) { } // formatPluginList 格式化插件列表(超过5个时精简显示) func formatPluginList(plugins []string) string { if len(plugins) <= 5 { return strings.Join(plugins, ", ") } return i18n.Tr("plugin_list_summary", strings.Join(plugins[:5], ", "), len(plugins)) } // ValidateConfiguration 验证扫描配置 func (b *BaseScanStrategy) ValidateConfiguration() error { return nil } // LogScanStart 输出扫描开始信息(已精简,仅在非服务扫描模式下显示) func (b *BaseScanStrategy) LogScanStart(session *common.ScanSession) { // 服务扫描模式下不显示(插件信息已足够说明) // 仅在本地/Web等特殊模式下显示 switch b.filterType { case FilterLocal: session.LogInfo(i18n.GetText("start_local_scan")) case FilterWeb: session.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: // 服务扫描策略:排除本地插件和UDP插件,保留TCP服务插件 for _, pluginName := range allPlugins { if !b.isLocalPlugin(pluginName) && !b.isUDPPlugin(pluginName) { filteredPlugins = append(filteredPlugins, pluginName) } } case FilterWeb: // Web扫描策略:只返回Web插件 for _, pluginName := range allPlugins { if b.isWebPlugin(pluginName) { filteredPlugins = append(filteredPlugins, pluginName) } } default: // 无过滤器:返回所有插件 filteredPlugins = allPlugins } orderWebPlugins(filteredPlugins) return filteredPlugins } func orderWebPlugins(pluginNames []string) { sort.SliceStable(pluginNames, func(i, j int) bool { return webPluginOrder(pluginNames[i]) < webPluginOrder(pluginNames[j]) }) } func webPluginOrder(pluginName string) int { switch pluginName { case "webtitle": return 0 case "webpoc": return 2 default: return 1 } } // 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 }