mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-10-16 20:31:46 +08:00
修复扫描参数、会话状态和结果备份异常
This commit is contained in:
@@ -128,6 +128,9 @@ jobs:
|
||||
PKGS=$(go list ./... | grep -v '/libs/grdp/')
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
|
||||
|
||||
- name: Web 接口回归测试
|
||||
run: go test -vet=off -race -tags web ./web/api
|
||||
|
||||
- name: 上传覆盖率
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -238,8 +238,20 @@ func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||||
// =============================================================================
|
||||
|
||||
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
|
||||
// 检查是否为 host:port 格式
|
||||
ports := fv.Ports
|
||||
if fv.PortsFile != "" {
|
||||
lines, err := parsers.ReadLinesFromFile(fv.PortsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s", i18n.Tr("config_read_ports_failed", fv.PortsFile, err))
|
||||
}
|
||||
ports = strings.Join(lines, ",")
|
||||
if len(parsers.ParsePort(ports)) == 0 {
|
||||
return fmt.Errorf("%s", i18n.Tr("invalid_port", ports))
|
||||
}
|
||||
cfg.Target.Ports = ports
|
||||
}
|
||||
|
||||
// 检查是否为 host:port 格式
|
||||
if info.Host != "" && strings.Contains(info.Host, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||||
|
||||
@@ -311,6 +311,8 @@ config_read_passwords_failed:
|
||||
other: "Failed to read password file {{.Arg1}}: {{.Arg2}}"
|
||||
config_read_urls_failed:
|
||||
other: "Failed to read URL file {{.Arg1}}: {{.Arg2}}"
|
||||
config_read_ports_failed:
|
||||
other: "Failed to read port file {{.Arg1}}: {{.Arg2}}"
|
||||
|
||||
# ========================= Plugin Scan Messages (with parameters) =========================
|
||||
scan_plugin_not_found:
|
||||
|
||||
@@ -311,6 +311,8 @@ config_read_passwords_failed:
|
||||
other: "读取密码文件 {{.Arg1}} 失败: {{.Arg2}}"
|
||||
config_read_urls_failed:
|
||||
other: "读取URL文件 {{.Arg1}} 失败: {{.Arg2}}"
|
||||
config_read_ports_failed:
|
||||
other: "读取端口文件 {{.Arg1}} 失败: {{.Arg2}}"
|
||||
|
||||
# ========================= 插件扫描消息 (带参数) =========================
|
||||
scan_plugin_not_found:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWritersPreserveBackupOnFinalWriteFailure(t *testing.T) {
|
||||
for _, format := range []Format{FormatTXT, FormatJSON, FormatCSV} {
|
||||
t.Run(string(format), func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "result."+string(format))
|
||||
manager, err := NewManager(DefaultManagerConfig(path, format))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer manager.Close()
|
||||
if err := manager.SaveResult(&ScanResult{Type: TypeHost, Target: "127.0.0.1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backupPath := path + ".realtime.tmp"
|
||||
before, err := os.ReadFile(backupPath)
|
||||
if err != nil || len(before) == 0 {
|
||||
t.Fatalf("backup before Close: %q, %v", before, err)
|
||||
}
|
||||
var file *os.File
|
||||
switch writer := manager.writer.(type) {
|
||||
case *TXTWriter:
|
||||
file = writer.file
|
||||
case *JSONWriter:
|
||||
file = writer.file
|
||||
case *CSVWriter:
|
||||
file = writer.file
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.Close(); err == nil {
|
||||
t.Fatal("final write failure was not reported")
|
||||
}
|
||||
after, err := os.ReadFile(backupPath)
|
||||
if err != nil || !bytes.Equal(before, after) {
|
||||
t.Fatalf("recovery backup was lost after final write failed: %q, %v", after, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+27
-43
@@ -71,6 +71,25 @@ func targetWithPort(target string, port interface{}) string {
|
||||
return net.JoinHostPort(target, portText)
|
||||
}
|
||||
|
||||
// closeOutputFiles 仅在最终结果完整落盘并关闭后删除恢复备份。
|
||||
func closeOutputFiles(file, backup *os.File, backupPath string, writeErr error) error {
|
||||
if writeErr == nil {
|
||||
writeErr = file.Sync()
|
||||
}
|
||||
if err := file.Close(); err != nil && writeErr == nil {
|
||||
writeErr = err
|
||||
}
|
||||
if backup != nil {
|
||||
if err := backup.Close(); err != nil && writeErr == nil {
|
||||
writeErr = err
|
||||
}
|
||||
if writeErr == nil {
|
||||
writeErr = os.Remove(backupPath)
|
||||
}
|
||||
}
|
||||
return writeErr
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TXTWriter - 文本格式写入器
|
||||
// =============================================================================
|
||||
@@ -347,23 +366,7 @@ func (w *TXTWriter) Close() error {
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := w.file.Sync(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := w.file.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
return firstErr
|
||||
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, w.bufWriter.Flush())
|
||||
}
|
||||
|
||||
// writeSection 写入一个分类的所有结果
|
||||
@@ -565,22 +568,11 @@ func (w *JSONWriter) Close() error {
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(output, JSONIndentPrefix, JSONIndentString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
if err == nil {
|
||||
_, err = w.file.Write(data)
|
||||
}
|
||||
|
||||
if _, err := w.file.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Close()
|
||||
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, err)
|
||||
}
|
||||
|
||||
// GetFormat 获取格式类型
|
||||
@@ -692,20 +684,12 @@ func (w *CSVWriter) Close() error {
|
||||
|
||||
w.closed = true
|
||||
|
||||
// 关闭并删除实时备份文件(正常结束,不再需要)
|
||||
if w.realtimeFile != nil {
|
||||
w.realtimeFile.Close()
|
||||
os.Remove(w.realtimePath)
|
||||
}
|
||||
|
||||
w.csvWriter.Flush()
|
||||
if err := w.csvWriter.Error(); err != nil {
|
||||
return err
|
||||
err := w.csvWriter.Error()
|
||||
if err == nil {
|
||||
err = w.bufWriter.Flush()
|
||||
}
|
||||
if err := w.bufWriter.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.file.Close()
|
||||
return closeOutputFiles(w.file, w.realtimeFile, w.realtimePath, err)
|
||||
}
|
||||
|
||||
func (w *CSVWriter) writeSection(title string, headers []string, results []*ScanResult, formatter func(*ScanResult) []string) {
|
||||
|
||||
@@ -73,6 +73,11 @@ func (it *HostIterator) Close() error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// IsExcluded 检查单个主机是否匹配当前迭代器的排除规则。
|
||||
func (it *HostIterator) IsExcluded(host string) bool {
|
||||
return it.exclude != nil && it.exclude.match(host)
|
||||
}
|
||||
|
||||
func (it *HostIterator) Next() (string, bool, error) {
|
||||
for {
|
||||
if it.current == nil {
|
||||
@@ -94,7 +99,7 @@ func (it *HostIterator) Next() (string, bool, error) {
|
||||
it.current = nil
|
||||
continue
|
||||
}
|
||||
if it.exclude != nil && it.exclude.match(host) {
|
||||
if it.IsExcluded(host) {
|
||||
continue
|
||||
}
|
||||
return host, true, nil
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
func TestBuildConfigLoadsPortsFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "ports.txt")
|
||||
if err := os.WriteFile(path, []byte("# ports\n22\n8000-8002\n443,22\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, _, err := BuildConfig(&FlagVars{Ports: config.MainPorts, PortsFile: path}, &HostInfo{Host: "127.0.0.1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := parsers.ParsePort(cfg.Target.Ports), []int{22, 443, 8000, 8001, 8002}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ports = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRejectsUnusablePortsFile(t *testing.T) {
|
||||
for _, contents := range []string{"", "# no ports\n", "invalid\n0\n65536\n"} {
|
||||
path := filepath.Join(t.TempDir(), "ports.txt")
|
||||
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := BuildConfig(&FlagVars{Ports: "22", PortsFile: path}, &HostInfo{}); err == nil {
|
||||
t.Errorf("accepted unusable port file %q", contents)
|
||||
}
|
||||
}
|
||||
if _, _, err := BuildConfig(&FlagVars{PortsFile: filepath.Join(t.TempDir(), "missing.txt")}, &HostInfo{}); err == nil {
|
||||
t.Error("accepted missing port file")
|
||||
}
|
||||
}
|
||||
+19
-8
@@ -106,9 +106,10 @@ func (s *ServiceScanStrategy) Description() string {
|
||||
// Execute 执行服务扫描策略
|
||||
func (s *ServiceScanStrategy) Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup) {
|
||||
config := session.Config
|
||||
s.SetState(session.State)
|
||||
|
||||
// 验证扫描目标(需要同时检查 -h 和 -hf 参数)
|
||||
if info.Host == "" && session.Params.HostsFile == "" {
|
||||
// host:port 已在配置解析阶段转入 State。
|
||||
if info.Host == "" && session.Params.HostsFile == "" && len(session.State.GetHostPorts()) == 0 {
|
||||
session.LogError(i18n.GetText("parse_error_target_empty"))
|
||||
return
|
||||
}
|
||||
@@ -209,7 +210,11 @@ func (s *ServiceScanStrategy) performHostScan(ctx context.Context, session *comm
|
||||
if len(hostPorts) > 0 {
|
||||
merged := mergeHostPorts(nil, hostPorts)
|
||||
targets := s.convertToTargetInfos(merged, info)
|
||||
excludedPorts := portSet(config.Target.ExcludePorts)
|
||||
for _, target := range targets {
|
||||
if iter.IsExcluded(target.Host) || excludedPorts[target.Port] {
|
||||
continue
|
||||
}
|
||||
for _, pluginName := range pluginsToRun {
|
||||
if s.IsPluginApplicableByName(pluginName, target.Host, target.Port, isCustomMode, config) {
|
||||
executeScanTask(ctx, session, pluginName, target, ch, wg)
|
||||
@@ -277,17 +282,14 @@ func (s *ServiceScanStrategy) dispatchUDPPlugins(ctx context.Context, session *c
|
||||
// 用户指定 -p 时,只调度端口有交集的 UDP 插件
|
||||
var userPorts map[int]bool
|
||||
if config.Target.Ports != "" && config.Target.Ports != "all" {
|
||||
parsed := parsers.ParsePort(config.Target.Ports)
|
||||
userPorts = make(map[int]bool, len(parsed))
|
||||
for _, p := range parsed {
|
||||
userPorts[p] = true
|
||||
}
|
||||
userPorts = portSet(config.Target.Ports)
|
||||
}
|
||||
excludedPorts := portSet(config.Target.ExcludePorts)
|
||||
|
||||
for _, host := range hosts {
|
||||
for _, pluginName := range udpPlugins {
|
||||
for _, port := range plugins.GetPluginPorts(pluginName) {
|
||||
if userPorts != nil && !userPorts[port] {
|
||||
if excludedPorts[port] || (userPorts != nil && !userPorts[port]) {
|
||||
continue
|
||||
}
|
||||
target := baseInfo
|
||||
@@ -299,6 +301,15 @@ func (s *ServiceScanStrategy) dispatchUDPPlugins(ctx context.Context, session *c
|
||||
}
|
||||
}
|
||||
|
||||
func portSet(ports string) map[int]bool {
|
||||
parsed := parsers.ParsePort(ports)
|
||||
set := make(map[int]bool, len(parsed))
|
||||
for _, port := range parsed {
|
||||
set[port] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// PrepareTargets 准备目标信息
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info common.HostInfo, session *common.ScanSession) []common.HostInfo {
|
||||
// 发现目标主机和端口
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type targetRecorderPlugin struct {
|
||||
plugins.BasePlugin
|
||||
calls chan common.HostInfo
|
||||
}
|
||||
|
||||
func (p *targetRecorderPlugin) Scan(_ context.Context, info *common.HostInfo, _ *common.ScanSession) *plugins.Result {
|
||||
p.calls <- *info
|
||||
return &plugins.Result{Skipped: true}
|
||||
}
|
||||
|
||||
func registerTargetRecorder(t *testing.T, kind string, ports ...int) (string, chan common.HostInfo) {
|
||||
t.Helper()
|
||||
name := "scope_" + strings.ToLower(strings.ReplaceAll(t.Name(), "/", "_"))
|
||||
calls := make(chan common.HostInfo, 16)
|
||||
plugins.RegisterWithTypes(name, func() plugins.Plugin {
|
||||
return &targetRecorderPlugin{BasePlugin: plugins.NewBasePlugin(name), calls: calls}
|
||||
}, ports, []string{kind})
|
||||
return name, calls
|
||||
}
|
||||
|
||||
func TestServiceScanAcceptsExplicitHostPort(t *testing.T) {
|
||||
for _, target := range []string{"127.0.0.1:10001", "[::1]:10001"} {
|
||||
t.Run(target, func(t *testing.T) {
|
||||
name, calls := registerTargetRecorder(t, plugins.PluginTypeService, 10001)
|
||||
flags := &common.FlagVars{ScanMode: name, ThreadNum: 1, Silent: true}
|
||||
info := common.HostInfo{Host: target}
|
||||
cfg, state, err := common.BuildConfig(flags, &info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := common.NewScanSession(cfg, state, flags)
|
||||
var wg sync.WaitGroup
|
||||
NewServiceScanStrategy().Execute(context.Background(), session, info, make(chan struct{}, 1), &wg)
|
||||
wg.Wait()
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("dispatched %d targets, want the explicit host:port", len(calls))
|
||||
}
|
||||
called := <-calls
|
||||
if got := called.Target(); got != target {
|
||||
t.Errorf("dispatched %q, want %q", got, target)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedTargetsRespectExclusions(t *testing.T) {
|
||||
name, calls := registerTargetRecorder(t, plugins.PluginTypeService, 10001, 10002, 10003)
|
||||
dir := t.TempDir()
|
||||
hostsFile := filepath.Join(dir, "hosts.txt")
|
||||
excludeFile := filepath.Join(dir, "exclude.txt")
|
||||
if err := os.WriteFile(hostsFile, nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(excludeFile, []byte("192.0.2.2\n::1\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
flags := &common.FlagVars{ScanMode: name, HostsFile: hostsFile, ExcludeHosts: "192.0.2.1,198.51.100.0/24", ExcludeHostsFile: excludeFile, ExcludePorts: "10002-10003", Silent: true}
|
||||
cfg := common.BuildConfigFromFlags(flags)
|
||||
state := common.NewState()
|
||||
state.SetHostPorts([]string{"127.0.0.1:10001", "127.0.0.1:10002", "127.0.0.1:10003", "192.0.2.1:10001", "192.0.2.2:10001", "198.51.100.20:10001", "[::1]:10001"})
|
||||
session := common.NewScanSession(cfg, state, flags)
|
||||
var wg sync.WaitGroup
|
||||
NewServiceScanStrategy().Execute(context.Background(), session, common.HostInfo{}, make(chan struct{}, 8), &wg)
|
||||
wg.Wait()
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("dispatched %d cached targets, want only the allowed target", len(calls))
|
||||
}
|
||||
called := <-calls
|
||||
if got := called.Target(); got != "127.0.0.1:10001" {
|
||||
t.Errorf("dispatched excluded target %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPDispatchRespectsExcludedPorts(t *testing.T) {
|
||||
name, calls := registerTargetRecorder(t, plugins.PluginTypeUDP, 10001, 10002)
|
||||
cfg := common.NewConfig()
|
||||
cfg.Mode = name
|
||||
cfg.Target.Ports = "10001,10002"
|
||||
cfg.Target.ExcludePorts = "10002"
|
||||
session := common.NewScanSession(cfg, common.NewState(), &common.FlagVars{})
|
||||
var wg sync.WaitGroup
|
||||
NewServiceScanStrategy().dispatchUDPPlugins(context.Background(), session, []string{"127.0.0.1"}, common.HostInfo{}, cfg, make(chan struct{}, 2), &wg)
|
||||
wg.Wait()
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("dispatched %d UDP targets, want 1", len(calls))
|
||||
}
|
||||
if got := (<-calls).Port; got != 10001 {
|
||||
t.Errorf("dispatched excluded UDP port %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceScanUsesSessionCache(t *testing.T) {
|
||||
name, calls := registerTargetRecorder(t, plugins.PluginTypeService, 10002)
|
||||
hostsFile := filepath.Join(t.TempDir(), "hosts.txt")
|
||||
if err := os.WriteFile(hostsFile, nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
flags := &common.FlagVars{ScanMode: "all", HostsFile: hostsFile, Silent: true}
|
||||
cfg := common.BuildConfigFromFlags(flags)
|
||||
state := common.NewState()
|
||||
state.SetHostPorts([]string{"127.0.0.1:10003"})
|
||||
CacheServiceInfoWithState(state, "127.0.0.1", 10003, &ServiceInfo{Name: name})
|
||||
previous := globalState
|
||||
SetGlobalState(common.NewState())
|
||||
t.Cleanup(func() { SetGlobalState(previous) })
|
||||
session := common.NewScanSession(cfg, state, flags)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
NewServiceScanStrategy().Execute(ctx, session, common.HostInfo{}, make(chan struct{}, 16), &wg)
|
||||
wg.Wait()
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("session service cache was ignored: dispatched %d matching plugins", len(calls))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package fscan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
type progressPlugin struct {
|
||||
plugins.BasePlugin
|
||||
observed <-chan struct{}
|
||||
}
|
||||
|
||||
func (p *progressPlugin) Scan(ctx context.Context, _ *common.HostInfo, session *common.ScanSession) *plugins.Result {
|
||||
session.State.IncrementPacketCount()
|
||||
select {
|
||||
case <-p.observed:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return &plugins.Result{Skipped: true}
|
||||
}
|
||||
|
||||
func TestOnProgressIncludesScanStateWithoutController(t *testing.T) {
|
||||
const name = "test_progress_state"
|
||||
observed := make(chan struct{})
|
||||
plugins.RegisterWithTypes(name, func() plugins.Plugin {
|
||||
return &progressPlugin{BasePlugin: plugins.NewBasePlugin(name), observed: observed}
|
||||
}, []int{10001}, []string{plugins.PluginTypeService})
|
||||
var once sync.Once
|
||||
scanner := NewScanner(Config{
|
||||
Plugins: []string{name},
|
||||
OnProgress: func(p ScanProgress) {
|
||||
if p.Packets > 0 {
|
||||
once.Do(func() { close(observed) })
|
||||
}
|
||||
},
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_, err := scanner.Scan(ctx, Target{Host: "127.0.0.1:10001"})
|
||||
select {
|
||||
case <-observed:
|
||||
default:
|
||||
t.Fatalf("OnProgress never observed the active scan state: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -206,6 +206,7 @@ func (s *Scanner) scanEach(ctx context.Context, opts scanOpts, handle ResultHand
|
||||
ctrl := opts.controller
|
||||
if ctrl == nil && s.config.OnProgress != nil {
|
||||
ctrl = newScanController()
|
||||
opts.controller = ctrl
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
+54
-42
@@ -34,14 +34,14 @@ type ScanRequest struct {
|
||||
ExcludePorts string `json:"exclude_ports"`
|
||||
|
||||
// 扫描控制
|
||||
ScanMode string `json:"scan_mode"`
|
||||
ThreadNum int `json:"thread_num"`
|
||||
Timeout int `json:"timeout"`
|
||||
ModuleThreadNum int `json:"module_thread_num"`
|
||||
DisablePing bool `json:"disable_ping"`
|
||||
DisableBrute bool `json:"disable_brute"`
|
||||
DisableSubnetProbe bool `json:"disable_subnet_probe"`
|
||||
AliveOnly bool `json:"alive_only"`
|
||||
ScanMode string `json:"scan_mode"`
|
||||
ThreadNum int `json:"thread_num"`
|
||||
Timeout int `json:"timeout"`
|
||||
ModuleThreadNum int `json:"module_thread_num"`
|
||||
DisablePing bool `json:"disable_ping"`
|
||||
DisableBrute bool `json:"disable_brute"`
|
||||
DisableSubnetProbe bool `json:"disable_subnet_probe"`
|
||||
AliveOnly bool `json:"alive_only"`
|
||||
|
||||
// 认证
|
||||
Username string `json:"username"`
|
||||
@@ -169,6 +169,47 @@ func (h *ScanHandler) runScan(req ScanRequest) {
|
||||
})
|
||||
}()
|
||||
|
||||
info, session, err := buildScanSession(req)
|
||||
if err != nil {
|
||||
common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
state := session.State
|
||||
|
||||
// 过渡桥:全局状态同步(待 Phase 5 移除)
|
||||
common.SetGlobalConfig(session.Config)
|
||||
common.SetGlobalState(state)
|
||||
|
||||
// 项目缓存注入:把已知的 host:port 加入扫描目标
|
||||
if req.ProjectID != "" {
|
||||
if cached := globalProjectStore.CachedHostPorts(req.ProjectID); len(cached) > 0 {
|
||||
state.SetHostPorts(append(state.GetHostPorts(), cached...))
|
||||
}
|
||||
}
|
||||
|
||||
// 设置WebSocket结果回调
|
||||
common.SetResultCallback(func(result interface{}) {
|
||||
item := h.results.Add(result)
|
||||
if item != nil {
|
||||
h.hub.Broadcast(ws.MsgScanResult, item)
|
||||
}
|
||||
})
|
||||
|
||||
// 执行扫描
|
||||
if _, err := core.RunScan(ctx, info, session); err != nil && !errors.Is(err, context.Canceled) {
|
||||
common.LogError(err.Error())
|
||||
}
|
||||
|
||||
// 项目缓存回写:合并本次扫描结果
|
||||
if req.ProjectID != "" {
|
||||
items := h.results.List()
|
||||
if len(items) > 0 {
|
||||
_ = globalProjectStore.MergeResults(req.ProjectID, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildScanSession(req ScanRequest) (common.HostInfo, *common.ScanSession, error) {
|
||||
// 构建HostInfo
|
||||
info := common.HostInfo{
|
||||
Host: req.Host,
|
||||
@@ -218,41 +259,12 @@ func (h *ScanHandler) runScan(req ScanRequest) {
|
||||
}
|
||||
|
||||
// 构建Config和Session
|
||||
config := common.BuildConfigFromFlags(fv)
|
||||
state := common.NewState()
|
||||
config, state, err := common.BuildConfig(fv, &info)
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
session := common.NewScanSession(config, state, fv)
|
||||
|
||||
// 过渡桥:全局状态同步(待 Phase 5 移除)
|
||||
common.SetGlobalConfig(config)
|
||||
common.SetGlobalState(state)
|
||||
|
||||
// 项目缓存注入:把已知的 host:port 加入扫描目标
|
||||
if req.ProjectID != "" {
|
||||
if cached := globalProjectStore.CachedHostPorts(req.ProjectID); len(cached) > 0 {
|
||||
state.SetHostPorts(cached)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置WebSocket结果回调
|
||||
common.SetResultCallback(func(result interface{}) {
|
||||
item := h.results.Add(result)
|
||||
if item != nil {
|
||||
h.hub.Broadcast(ws.MsgScanResult, item)
|
||||
}
|
||||
})
|
||||
|
||||
// 执行扫描
|
||||
if _, err := core.RunScan(ctx, info, session); err != nil && !errors.Is(err, context.Canceled) {
|
||||
common.LogError(err.Error())
|
||||
}
|
||||
|
||||
// 项目缓存回写:合并本次扫描结果
|
||||
if req.ProjectID != "" {
|
||||
items := h.results.List()
|
||||
if len(items) > 0 {
|
||||
_ = globalProjectStore.MergeResults(req.ProjectID, items)
|
||||
}
|
||||
}
|
||||
return info, session, nil
|
||||
}
|
||||
|
||||
// Stop 停止扫描
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//go:build web
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
scanplugins "github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
func TestBuildScanSessionUsesRequestedCredentials(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
want []scanplugins.Credential
|
||||
}{
|
||||
{"single pair", "operator", " secret ", []scanplugins.Credential{{Username: "operator", Password: " secret "}}},
|
||||
{"multiple users", "alice,bob", "secret", []scanplugins.Credential{{Username: "alice", Password: "secret"}, {Username: "bob", Password: "secret"}}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, session, err := buildScanSession(ScanRequest{Host: "127.0.0.1", Username: tc.username, Password: tc.password})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := scanplugins.GenerateCredentials("ssh", session.Config); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("credentials = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildScanSessionParsesExplicitHostPort(t *testing.T) {
|
||||
for _, host := range []string{"127.0.0.1:10001", "[::1]:10001"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
info, session, err := buildScanSession(ScanRequest{Host: host})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := session.State.GetHostPorts(); info.Host != "" || !reflect.DeepEqual(got, []string{host}) {
|
||||
t.Fatalf("explicit target was not parsed: host=%q, cached=%v", info.Host, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user