mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
improve embedded scanner runtime
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
+18
-4
@@ -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) {
|
||||||
// 检查发包限制
|
// 检查发包限制
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user