allow concurrent embedded scans

This commit is contained in:
ZacharyZcR
2026-05-18 16:11:41 +08:00
parent 3dde0c6a8e
commit 8de7570268
5 changed files with 171 additions and 53 deletions
+40 -11
View File
@@ -14,11 +14,16 @@ import (
)
var (
globalLogger *logging.Logger
loggerOnce sync.Once
globalLogger *logging.Logger
loggerOnce sync.Once
loggerMu sync.Mutex
silentLoggerRefs int
)
func getGlobalLogger() *logging.Logger {
loggerMu.Lock()
defer loggerMu.Unlock()
loggerOnce.Do(func() {
fv := GetFlagVars()
level := getLogLevelFromString(fv.LogLevel)
@@ -27,7 +32,7 @@ func getGlobalLogger() *logging.Logger {
EnableColor: !fv.NoColor,
SlowOutput: false,
ShowProgress: !fv.DisableProgress,
Silent: fv.Silent,
Silent: fv.Silent || silentLoggerRefs > 0,
StartTime: GetGlobalState().GetStartTime(),
}
if fv.Debug {
@@ -84,17 +89,41 @@ func LogError(errMsg string) { getGlobalLogger().Error(errMsg) }
// CloseLogger 关闭日志系统,释放文件资源
func CloseLogger() {
if globalLogger != nil {
globalLogger.Close()
loggerMu.Lock()
defer loggerMu.Unlock()
closeLoggerLocked()
}
// PushSilentLogger suppresses process-wide legacy log output until the returned
// restore function is called. It is reference counted so concurrent embedded
// scans can overlap safely.
func PushSilentLogger() func() {
loggerMu.Lock()
silentLoggerRefs++
resetLoggerLocked()
loggerMu.Unlock()
var once sync.Once
return func() {
once.Do(func() {
loggerMu.Lock()
if silentLoggerRefs > 0 {
silentLoggerRefs--
}
resetLoggerLocked()
loggerMu.Unlock()
})
}
}
// ResetLogger clears the process-wide logger so embedded callers can rebuild it
// after replacing runtime configuration.
func ResetLogger() {
if globalLogger != nil {
globalLogger.Close()
}
func resetLoggerLocked() {
closeLoggerLocked()
globalLogger = nil
loggerOnce = sync.Once{}
}
func closeLoggerLocked() {
if globalLogger != nil {
globalLogger.Close()
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ func main() {
}
```
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. Embedded scans build per-session runtime state and can run concurrently.
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.
+2 -2
View File
@@ -2,8 +2,8 @@
//
// The package is intentionally thin: it reuses the existing scan core and
// plugin registry, while hiding CLI flags, stdout output, and result files from
// callers. The first SDK surface is serialized internally because the current
// scan core still uses process-wide runtime state.
// callers. Embedded scans build per-session runtime state and can run
// concurrently.
//
// Embedded callers can use ValidateConfig before starting a scan, IsSafePlugin
// or ListPlugins to build plugin allow lists, and ResultType* constants instead
+5 -39
View File
@@ -21,8 +21,6 @@ import (
_ "github.com/shadow1ng/fscan/plugins/web"
)
var scanMu sync.Mutex
var defaultSafePlugins = []string{
"activemq",
"cassandra",
@@ -149,12 +147,8 @@ func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ..
ctx, cancel := context.WithCancel(ctx)
defer cancel()
scanMu.Lock()
defer scanMu.Unlock()
previous := captureRuntime()
defer previous.restore()
restoreLogger := common.PushSilentLogger()
defer restoreLogger()
var (
errMu sync.Mutex
@@ -201,13 +195,11 @@ func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ..
func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.ResultSink) error {
fv := buildFlagVars(s.config, target)
globalFV := common.GetFlagVars()
*globalFV = *fv
info := common.HostInfo{Host: strings.TrimSpace(target.Host), URL: strings.TrimSpace(target.URL)}
i18n.SetLanguage(globalFV.Language)
i18n.SetLanguage(fv.Language)
cfg, state, err := common.BuildConfig(globalFV, &info)
cfg, state, err := common.BuildConfig(fv, &info)
if err != nil {
return err
}
@@ -225,12 +217,7 @@ func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.Result
cfg.Output.DisableProgress = true
cfg.Output.ShowProgress = false
common.SetGlobalConfig(cfg)
common.SetGlobalState(state)
common.ResetLogger()
common.InitLogger()
session := common.NewScanSession(cfg, state, globalFV)
session := common.NewScanSession(cfg, state, fv)
session.ResultSink = sink
core.RunScan(ctx, info, session)
return nil
@@ -456,24 +443,3 @@ func getHandlerError(mu *sync.Mutex, err *error) error {
defer mu.Unlock()
return *err
}
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()
}
+123
View File
@@ -5,6 +5,7 @@ import (
"errors"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -252,6 +253,128 @@ func TestScanEachRequiresHandler(t *testing.T) {
}
}
func TestScanEachRunsConcurrent(t *testing.T) {
first := startFTPListener(t)
defer first.Close()
second := startFTPListener(t)
defer second.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
blocked := make(chan struct{})
release := make(chan struct{})
firstErr := make(chan error, 1)
var blockOnce sync.Once
go func() {
scanner := NewScanner(Config{
DisablePing: true,
DisableBrute: true,
Timeout: time.Second,
Threads: 16,
Plugins: []string{"ftp"},
})
port := first.Addr().(*net.TCPAddr).Port
firstErr <- scanner.ScanEach(ctx, func(result Result) error {
if result.IsPort() {
blockOnce.Do(func() { close(blocked) })
select {
case <-release:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}, Target{Host: "127.0.0.1", Ports: []int{port}})
}()
select {
case <-blocked:
case <-time.After(2 * time.Second):
t.Fatal("first scan did not reach handler")
}
secondErr := make(chan error, 1)
var secondResults []Result
go func() {
scanner := NewScanner(Config{
DisablePing: true,
DisableBrute: true,
Timeout: time.Second,
Threads: 16,
Plugins: []string{"ftp"},
})
port := second.Addr().(*net.TCPAddr).Port
secondErr <- scanner.ScanEach(ctx, func(result Result) error {
secondResults = append(secondResults, result)
return nil
}, Target{Host: "127.0.0.1", Ports: []int{port}})
}()
select {
case err := <-secondErr:
if err != nil {
close(release)
t.Fatalf("second scan failed: %v", err)
}
case <-time.After(2 * time.Second):
close(release)
t.Fatal("second scan blocked behind first scan")
}
if !hasResult(secondResults, ResultTypePort, "open", "") {
close(release)
t.Fatalf("missing second scan result: %#v", secondResults)
}
close(release)
if err := <-firstErr; err != nil {
t.Fatalf("first scan failed: %v", err)
}
}
func TestScanDoesNotReplaceGlobalRuntime(t *testing.T) {
listener := startFTPListener(t)
defer listener.Close()
previousConfig := common.GetGlobalConfig()
previousState := common.GetGlobalState()
previousFlags := *common.GetFlagVars()
defer func() {
common.SetGlobalConfig(previousConfig)
common.SetGlobalState(previousState)
*common.GetFlagVars() = previousFlags
}()
sentinelConfig := common.NewConfig()
sentinelState := common.NewState()
common.SetGlobalConfig(sentinelConfig)
common.SetGlobalState(sentinelState)
common.GetFlagVars().LogLevel = "sentinel"
scanner := NewScanner(Config{
DisablePing: true,
DisableBrute: true,
Timeout: time.Second,
Threads: 16,
Plugins: []string{"ftp"},
})
port := listener.Addr().(*net.TCPAddr).Port
if _, err := scanner.Scan(context.Background(), Target{Host: "127.0.0.1", Ports: []int{port}}); err != nil {
t.Fatal(err)
}
if common.GetGlobalConfig() != sentinelConfig {
t.Fatal("SDK scan replaced global config")
}
if common.GetGlobalState() != sentinelState {
t.Fatal("SDK scan replaced global state")
}
if common.GetFlagVars().LogLevel != "sentinel" {
t.Fatal("SDK scan replaced global flags")
}
}
func startFTPListener(t *testing.T) net.Listener {
t.Helper()