mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
allow concurrent embedded scans
This commit is contained in:
+40
-11
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user