improve embedded scanner runtime

This commit is contained in:
ZacharyZcR
2026-05-18 14:53:00 +08:00
parent 6bfa05cb45
commit 6605c93dd9
10 changed files with 267 additions and 81 deletions
-17
View File
@@ -17,23 +17,6 @@ func SetResultCallback(cb ResultCallback) {
resultCallback = cb resultCallback = cb
} }
// ReplaceResultCallback temporarily replaces the result callback and returns a
// restore function. This is useful for embedded callers that need to collect
// structured results without permanently stealing the callback from another
// subsystem.
func ReplaceResultCallback(cb ResultCallback) func() {
callbackMu.Lock()
previous := resultCallback
resultCallback = cb
callbackMu.Unlock()
return func() {
callbackMu.Lock()
resultCallback = previous
callbackMu.Unlock()
}
}
// NotifyResult 通知结果给回调函数 // NotifyResult 通知结果给回调函数
func NotifyResult(result interface{}) { func NotifyResult(result interface{}) {
callbackMu.RLock() callbackMu.RLock()
+15 -1
View File
@@ -8,16 +8,21 @@ import (
"sync" "sync"
"time" "time"
"github.com/shadow1ng/fscan/common/proxy"
"github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/output"
"github.com/shadow1ng/fscan/common/proxy"
) )
// ResultSink receives structured scan results for one scan session.
type ResultSink func(result *output.ScanResult) error
// ScanSession 封装单次扫描的全部上下文 // ScanSession 封装单次扫描的全部上下文
// 一次扫描一个 session,并发扫描各自独立 // 一次扫描一个 session,并发扫描各自独立
type ScanSession struct { type ScanSession struct {
Config *Config // 不可变,创建后只读 Config *Config // 不可变,创建后只读
State *State // 可变,原子操作,每会话独立 State *State // 可变,原子操作,每会话独立
Params *FlagVars // 原始参数,只读 Params *FlagVars // 原始参数,只读
ResultSink ResultSink // 可选,覆盖全局输出
// 每会话 dialer(懒初始化,取决于代理配置) // 每会话 dialer(懒初始化,取决于代理配置)
dialerOnce sync.Once dialerOnce sync.Once
@@ -34,6 +39,15 @@ func NewScanSession(config *Config, state *State, params *FlagVars) *ScanSession
} }
} }
// SaveResult saves a scan result through the session sink if present, otherwise
// falls back to the process-wide output pipeline used by the CLI.
func (s *ScanSession) SaveResult(result *output.ScanResult) error {
if s != nil && s.ResultSink != nil {
return s.ResultSink(result)
}
return SaveResult(result)
}
// DialTCP 创建 TCP 连接,内含限速检查、代理、计数 // DialTCP 创建 TCP 连接,内含限速检查、代理、计数
func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) { func (s *ScanSession) DialTCP(ctx context.Context, network, address string, timeout time.Duration) (net.Conn, error) {
// 检查发包限制 // 检查发包限制
+5
View File
@@ -81,6 +81,11 @@ func (b *BaseScanStrategy) IsPluginApplicableByName(pluginName string, targetHos
return false return false
} }
// 显式指定插件时,尊重调用方选择,不再强制使用插件默认端口过滤。
if isCustomMode {
return b.isPluginPassesFilterType(pluginName, isCustomMode, config)
}
// 检查端口匹配和过滤器类型 // 检查端口匹配和过滤器类型
return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config) return b.isPluginApplicableToPortWithHost(pluginName, targetHost, targetPort) && b.isPluginPassesFilterType(pluginName, isCustomMode, config)
} }
+4 -4
View File
@@ -55,7 +55,7 @@ func CheckLive(ctx context.Context, hostslist []string, Ping bool, session *comm
chanHosts := make(chan string, len(hostslist)) chanHosts := make(chan string, len(hostslist))
// 处理存活主机 // 处理存活主机
go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, &livewg) go handleAliveHosts(chanHosts, hostslist, Ping, &aliveHosts, &aliveHostsMu, existHosts, config, session, &livewg)
// 根据Ping参数选择检测方式 // 根据Ping参数选择检测方式
if Ping { if Ping {
@@ -130,7 +130,7 @@ func IsContain(items []string, item string) bool {
return false return false
} }
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, livewg *sync.WaitGroup) { func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, aliveHosts *[]string, aliveHostsMu *sync.Mutex, existHosts map[string]struct{}, config *common.Config, session *common.ScanSession, livewg *sync.WaitGroup) {
for ip := range chanHosts { for ip := range chanHosts {
if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) { if _, ok := existHosts[ip]; !ok && IsContain(hostslist, ip) {
existHosts[ip] = struct{}{} existHosts[ip] = struct{}{}
@@ -155,7 +155,7 @@ func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool, al
"protocol": protocol, "protocol": protocol,
}, },
} }
_ = common.SaveResult(result) _ = session.SaveResult(result)
// 保留原有的控制台输出 // 保留原有的控制台输出
if !config.Output.Silent { if !config.Output.Silent {
@@ -771,7 +771,7 @@ func runTcpProbeForHosts(ctx context.Context, hosts []string, session *common.Sc
"protocol": "TCP", "protocol": "TCP",
}, },
} }
_ = common.SaveResult(result) _ = session.SaveResult(result)
if !config.Output.Silent { if !config.Output.Silent {
common.LogInfo(i18n.Tr("host_alive", h, "TCP")) common.LogInfo(i18n.Tr("host_alive", h, "TCP"))
+5 -5
View File
@@ -487,7 +487,7 @@ func scanSinglePort(ctx context.Context, host string, port int, addr string, ada
// 步骤2:记录开放端口 // 步骤2:记录开放端口
atomic.AddInt64(count, 1) atomic.AddInt64(count, 1)
collector.Add(addr) collector.Add(addr)
saveOpenPort(host, port) saveOpenPort(session, host, port)
// 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接) // 步骤3:服务识别(Scanner负责关闭连接,包括探测中可能创建的新连接)
scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session) scanner := NewSmartPortInfoScanner(ctx, host, port, conn, timeout, config, session)
@@ -640,8 +640,8 @@ func isConnectionClosed(err error) bool {
} }
// saveOpenPort 保存开放端口结果 // saveOpenPort 保存开放端口结果
func saveOpenPort(host string, port int) { func saveOpenPort(session *common.ScanSession, host string, port int) {
_ = common.SaveResult(&output.ScanResult{ _ = session.SaveResult(&output.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: output.TypePort, Type: output.TypePort,
Target: host, Target: host,
@@ -669,7 +669,7 @@ func processServiceResult(host string, port int, addr string, serviceInfo *Servi
MarkAsWebService(host, port, serviceInfo) MarkAsWebService(host, port, serviceInfo)
} }
_ = common.SaveResult(&output.ScanResult{ _ = session.SaveResult(&output.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: output.TypeService, Type: output.TypeService,
Target: fmt.Sprintf("%s:%d", host, port), Target: fmt.Sprintf("%s:%d", host, port),
@@ -737,7 +737,7 @@ func tryHTTPFallbackDetection(host string, port int, addr string, config *common
"is_web": true, "is_web": true,
"detected_by": "http_probe", "detected_by": "http_probe",
} }
_ = common.SaveResult(&output.ScanResult{ _ = session.SaveResult(&output.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: output.TypeService, Type: output.TypeService,
Target: fmt.Sprintf("%s:%d", host, port), Target: fmt.Sprintf("%s:%d", host, port),
+3 -3
View File
@@ -281,7 +281,7 @@ func executeScanTask(ctx context.Context, session *common.ScanSession, pluginNam
if result != nil { if result != nil {
if result.Success { if result.Success {
// 保存成功的扫描结果到文件 // 保存成功的扫描结果到文件
savePluginResult(&target, pluginName, result) savePluginResult(session, &target, pluginName, result)
} else if result.Type == plugins.ResultTypeCredential { } else if result.Type == plugins.ResultTypeCredential {
// 凭据测试完成但未发现弱密码,在error级别输出提示 // 凭据测试完成但未发现弱密码,在error级别输出提示
common.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName)) common.LogError(i18n.Tr("brute_no_weak_pass", target.Host, target.Port, pluginName))
@@ -382,7 +382,7 @@ var defaultSerializer = resultSerializer{
} }
// savePluginResult 保存插件扫描结果 // savePluginResult 保存插件扫描结果
func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.Result) { func savePluginResult(session *common.ScanSession, info *common.HostInfo, pluginName string, result *plugins.Result) {
if result == nil || !result.Success || result.Skipped { if result == nil || !result.Success || result.Skipped {
return return
} }
@@ -402,7 +402,7 @@ func savePluginResult(info *common.HostInfo, pluginName string, result *plugins.
// 保存结果 // 保存结果
target := info.Target() target := info.Target()
_ = common.SaveResult(&output.ScanResult{ _ = session.SaveResult(&output.ScanResult{
Time: time.Now(), Time: time.Now(),
Type: serializer.outputType, Type: serializer.outputType,
Target: target, Target: target,
+6 -3
View File
@@ -20,6 +20,9 @@ func main() {
DisablePing: true, DisablePing: true,
DisableBrute: true, DisableBrute: true,
Plugins: []string{"ssh", "mysql", "redis"}, Plugins: []string{"ssh", "mysql", "redis"},
OnResult: func(result fscan.Result) {
fmt.Printf("%s %s %s\n", result.Type, result.Target, result.Status)
},
}) })
results, err := scanner.Scan(context.Background(), fscan.Target{ results, err := scanner.Scan(context.Background(), fscan.Target{
@@ -30,10 +33,10 @@ func main() {
panic(err) panic(err)
} }
for _, result := range results { fmt.Printf("total results: %d\n", len(results))
fmt.Printf("%s %s %s\n", result.Type, result.Target, result.Status)
}
} }
``` ```
The SDK currently reuses fscan's existing scan core and plugin registry. Calls are serialized internally because the current core still keeps process-wide runtime state. The SDK currently reuses fscan's existing scan core and plugin registry. Calls are serialized internally because the current core still keeps process-wide runtime state.
By default, the SDK runs a conservative service-oriented plugin set and blocks plugins with local side effects or active POC behavior. Set `AllowUnsafePlugins` only when the embedding system explicitly wants those capabilities.
+128 -43
View File
@@ -12,6 +12,7 @@ import (
"github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common"
commonconfig "github.com/shadow1ng/fscan/common/config" commonconfig "github.com/shadow1ng/fscan/common/config"
"github.com/shadow1ng/fscan/common/i18n" "github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/common/output"
"github.com/shadow1ng/fscan/core" "github.com/shadow1ng/fscan/core"
"github.com/shadow1ng/fscan/plugins" "github.com/shadow1ng/fscan/plugins"
@@ -22,6 +23,56 @@ import (
var scanMu sync.Mutex var scanMu sync.Mutex
var defaultSafePlugins = []string{
"activemq",
"cassandra",
"elasticsearch",
"ftp",
"kafka",
"ldap",
"memcached",
"mongodb",
"mssql",
"mysql",
"neo4j",
"netbios",
"oracle",
"postgresql",
"rabbitmq",
"rdp",
"redis",
"rsync",
"smb",
"smtp",
"ssh",
"telnet",
"vnc",
"webtitle",
}
var unsafePlugins = map[string]struct{}{
"cleaner": {},
"crontask": {},
"download": {},
"forwardshell": {},
"keylogger": {},
"ldpreload": {},
"minidump": {},
"reverseshell": {},
"socks5proxy": {},
"sshkey": {},
"systemdservice": {},
"winbits": {},
"winifeo": {},
"winlogon": {},
"winregistry": {},
"winschtask": {},
"winservice": {},
"winstartup": {},
"winwmi": {},
"webpoc": {},
}
// Scanner runs fscan from another Go process. // Scanner runs fscan from another Go process.
type Scanner struct { type Scanner struct {
config Config config Config
@@ -32,6 +83,12 @@ func NewScanner(config Config) *Scanner {
return &Scanner{config: config} return &Scanner{config: config}
} }
// DefaultSafePlugins returns the plugin set used by the SDK when Config.Plugins
// is empty. The returned slice can be modified by callers.
func DefaultSafePlugins() []string {
return append([]string(nil), defaultSafePlugins...)
}
// Scan runs the scanner for the provided targets and returns structured // Scan runs the scanner for the provided targets and returns structured
// findings. If no targets are provided, Config.Targets is used. // findings. If no targets are provided, Config.Targets is used.
func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error) { func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error) {
@@ -48,24 +105,30 @@ func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error)
scanMu.Lock() scanMu.Lock()
defer scanMu.Unlock() defer scanMu.Unlock()
previous := captureRuntime()
defer previous.restore()
var ( var (
mu sync.Mutex mu sync.Mutex
results []Result results []Result
) )
restoreCallback := common.ReplaceResultCallback(func(raw interface{}) {
if result, ok := decodeCallbackResult(raw); ok {
mu.Lock()
results = append(results, result)
mu.Unlock()
}
})
defer restoreCallback()
for _, target := range targets { for _, target := range targets {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return snapshotResults(&mu, results), err return snapshotResults(&mu, results), err
} }
if err := s.scanOne(ctx, target); err != nil { sink := func(raw *output.ScanResult) error {
if result, ok := convertOutputResult(raw); ok {
mu.Lock()
results = append(results, result)
mu.Unlock()
if s.config.OnResult != nil {
s.config.OnResult(result)
}
}
return nil
}
if err := s.scanOne(ctx, target, sink); err != nil {
return snapshotResults(&mu, results), err return snapshotResults(&mu, results), err
} }
} }
@@ -73,7 +136,7 @@ func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error)
return snapshotResults(&mu, results), ctx.Err() return snapshotResults(&mu, results), ctx.Err()
} }
func (s *Scanner) scanOne(ctx context.Context, target Target) error { func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.ResultSink) error {
fv := buildFlagVars(s.config, target) fv := buildFlagVars(s.config, target)
globalFV := common.GetFlagVars() globalFV := common.GetFlagVars()
*globalFV = *fv *globalFV = *fv
@@ -105,6 +168,7 @@ func (s *Scanner) scanOne(ctx context.Context, target Target) error {
common.InitLogger() common.InitLogger()
session := common.NewScanSession(cfg, state, globalFV) session := common.NewScanSession(cfg, state, globalFV)
session.ResultSink = sink
core.RunScan(ctx, info, session) core.RunScan(ctx, info, session)
return nil return nil
} }
@@ -121,6 +185,9 @@ func validateConfig(config Config, targets []Target) error {
if !plugins.Exists(name) { if !plugins.Exists(name) {
return fmt.Errorf("fscan: plugin %q not found", name) return fmt.Errorf("fscan: plugin %q not found", name)
} }
if !config.AllowUnsafePlugins && !isSafePlugin(name) {
return fmt.Errorf("fscan: plugin %q is not enabled for embedded safe mode", name)
}
} }
for _, target := range targets { for _, target := range targets {
if strings.TrimSpace(target.Host) == "" && strings.TrimSpace(target.URL) == "" { if strings.TrimSpace(target.Host) == "" && strings.TrimSpace(target.URL) == "" {
@@ -184,7 +251,7 @@ func buildFlagVars(config Config, target Target) *common.FlagVars {
return &common.FlagVars{ return &common.FlagVars{
Host: strings.TrimSpace(target.Host), Host: strings.TrimSpace(target.Host),
Ports: formatPorts(ports), Ports: formatPorts(ports),
ScanMode: formatPlugins(config.Plugins), ScanMode: formatPlugins(config),
ThreadNum: threadNum, ThreadNum: threadNum,
ModuleThreadNum: moduleThreads, ModuleThreadNum: moduleThreads,
TimeoutSec: timeout, TimeoutSec: timeout,
@@ -225,12 +292,16 @@ func buildFlagVars(config Config, target Target) *common.FlagVars {
} }
} }
func formatPlugins(plugins []string) string { func formatPlugins(config Config) string {
if len(plugins) == 0 { pluginNames := config.Plugins
if len(pluginNames) == 0 && !config.AllowUnsafePlugins {
pluginNames = defaultSafePlugins
}
if len(pluginNames) == 0 {
return "all" return "all"
} }
parts := make([]string, 0, len(plugins)) parts := make([]string, 0, len(pluginNames))
for _, plugin := range plugins { for _, plugin := range pluginNames {
plugin = strings.TrimSpace(plugin) plugin = strings.TrimSpace(plugin)
if plugin != "" { if plugin != "" {
parts = append(parts, plugin) parts = append(parts, plugin)
@@ -242,6 +313,20 @@ func formatPlugins(plugins []string) string {
return strings.Join(parts, ",") return strings.Join(parts, ",")
} }
func isSafePlugin(name string) bool {
name = strings.TrimSpace(name)
if name == "" {
return true
}
if plugins.HasType(name, plugins.PluginTypeLocal) {
return false
}
if _, bad := unsafePlugins[name]; bad {
return false
}
return true
}
func formatPorts(ports []int) string { func formatPorts(ports []int) string {
if len(ports) == 0 { if len(ports) == 0 {
return commonconfig.MainPorts return commonconfig.MainPorts
@@ -266,43 +351,43 @@ func secondsOrDefault(value time.Duration, fallback int) int64 {
return seconds return seconds
} }
func decodeCallbackResult(raw interface{}) (Result, bool) { func convertOutputResult(raw *output.ScanResult) (Result, bool) {
item, ok := raw.(map[string]interface{}) if raw == nil {
if !ok {
return Result{}, false return Result{}, false
} }
result := Result{ result := Result{
Type: stringValue(item["type"]), Time: raw.Time,
Target: stringValue(item["target"]), Type: string(raw.Type),
Status: stringValue(item["status"]), Target: raw.Target,
Details: mapValue(item["details"]), Status: raw.Status,
} Details: raw.Details,
if t, ok := item["time"].(time.Time); ok {
result.Time = t
} }
return result, result.Target != "" || result.Status != "" return result, result.Target != "" || result.Status != ""
} }
func stringValue(value interface{}) string {
if s, ok := value.(string); ok {
return s
}
return ""
}
func mapValue(value interface{}) map[string]interface{} {
if value == nil {
return nil
}
if m, ok := value.(map[string]interface{}); ok {
return m
}
return nil
}
func snapshotResults(mu *sync.Mutex, results []Result) []Result { func snapshotResults(mu *sync.Mutex, results []Result) []Result {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
return append([]Result(nil), results...) return append([]Result(nil), results...)
} }
type runtimeSnapshot struct {
flagVars common.FlagVars
config *common.Config
state *common.State
}
func captureRuntime() runtimeSnapshot {
return runtimeSnapshot{
flagVars: *common.GetFlagVars(),
config: common.GetGlobalConfig(),
state: common.GetGlobalState(),
}
}
func (s runtimeSnapshot) restore() {
*common.GetFlagVars() = s.flagVars
common.SetGlobalConfig(s.config)
common.SetGlobalState(s.state)
common.ResetLogger()
}
+93 -2
View File
@@ -2,9 +2,13 @@ package fscan
import ( import (
"context" "context"
"net"
"strings"
"sync/atomic"
"testing" "testing"
"time" "time"
"github.com/shadow1ng/fscan/common"
commonconfig "github.com/shadow1ng/fscan/common/config" commonconfig "github.com/shadow1ng/fscan/common/config"
) )
@@ -17,8 +21,8 @@ func TestBuildFlagVarsDefaults(t *testing.T) {
if fv.Ports != commonconfig.MainPorts { if fv.Ports != commonconfig.MainPorts {
t.Fatalf("Ports = %q, want MainPorts", fv.Ports) t.Fatalf("Ports = %q, want MainPorts", fv.Ports)
} }
if fv.ScanMode != "all" { if fv.ScanMode != formatPlugins(Config{Plugins: DefaultSafePlugins()}) {
t.Fatalf("ScanMode = %q, want all", fv.ScanMode) t.Fatalf("ScanMode = %q, want safe defaults", fv.ScanMode)
} }
if !fv.DisableSave || !fv.Silent || !fv.DisableProgress { if !fv.DisableSave || !fv.Silent || !fv.DisableProgress {
t.Fatalf("embedded defaults should disable output side effects") t.Fatalf("embedded defaults should disable output side effects")
@@ -43,6 +47,12 @@ func TestValidateConfig(t *testing.T) {
if err := validateConfig(Config{Plugins: []string{"definitely-missing"}}, []Target{{Host: "127.0.0.1"}}); err == nil { if err := validateConfig(Config{Plugins: []string{"definitely-missing"}}, []Target{{Host: "127.0.0.1"}}); err == nil {
t.Fatal("expected missing plugin error") t.Fatal("expected missing plugin error")
} }
if err := validateConfig(Config{Plugins: []string{"webpoc"}}, []Target{{URL: "http://127.0.0.1"}}); err == nil {
t.Fatal("expected unsafe plugin error")
}
if err := validateConfig(Config{Plugins: []string{"webpoc"}, AllowUnsafePlugins: true}, []Target{{URL: "http://127.0.0.1"}}); err != nil {
t.Fatalf("unsafe plugin with opt-in failed: %v", err)
}
if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", Ports: []int{70000}}}); err == nil { if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", Ports: []int{70000}}}); err == nil {
t.Fatal("expected invalid port error") t.Fatal("expected invalid port error")
} }
@@ -64,3 +74,84 @@ func TestScanHonorsCanceledContext(t *testing.T) {
t.Fatalf("Scan error = %v, want context.Canceled", err) t.Fatalf("Scan error = %v, want context.Canceled", err)
} }
} }
func TestScanCollectsResultsThroughSessionSink(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func(conn net.Conn) {
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(2 * time.Second))
_, _ = conn.Write([]byte("220 test FTP\r\n"))
buf := make([]byte, 64)
_, _ = conn.Read(buf)
}(conn)
}
}()
var callbackCalls int32
common.SetResultCallback(func(interface{}) {
atomic.AddInt32(&callbackCalls, 1)
})
defer common.ClearResultCallback()
var streamed int32
scanner := NewScanner(Config{
DisablePing: true,
DisableBrute: true,
Timeout: time.Second,
Threads: 16,
Plugins: []string{"ftp"},
OnResult: func(result Result) {
atomic.AddInt32(&streamed, 1)
},
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
port := listener.Addr().(*net.TCPAddr).Port
results, err := scanner.Scan(ctx, Target{Host: "127.0.0.1", Ports: []int{port}})
if err != nil {
t.Fatal(err)
}
if len(results) == 0 {
t.Fatal("expected SDK results")
}
if got := atomic.LoadInt32(&streamed); got != int32(len(results)) {
t.Fatalf("streamed length = %d, want %d", got, len(results))
}
if !hasResult(results, "PORT", "open", "") {
t.Fatalf("missing port result: %#v", results)
}
if !hasResult(results, "SERVICE", "FTP", "ftp") {
t.Fatalf("missing ftp plugin result: %#v", results)
}
if got := atomic.LoadInt32(&callbackCalls); got != 0 {
t.Fatalf("global callback calls = %d, want 0", got)
}
}
func hasResult(results []Result, resultType, statusText, plugin string) bool {
for _, result := range results {
if result.Type != resultType || !strings.Contains(result.Status, statusText) {
continue
}
if plugin == "" {
return true
}
if result.Details != nil && result.Details["plugin"] == plugin {
return true
}
}
return false
}
+5
View File
@@ -25,6 +25,11 @@ type Config struct {
Plugins []string Plugins []string
Ports []int Ports []int
// AllowUnsafePlugins permits plugins with local side effects or long-lived
// behavior. It is false by default for embedded endpoint use.
AllowUnsafePlugins bool
// OnResult is called for every structured result as it is discovered.
OnResult func(Result)
Timeout time.Duration Timeout time.Duration
Threads int Threads int