mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
merge sdk architecture polish
This commit is contained in:
+36
-2
@@ -17,6 +17,20 @@ import (
|
|||||||
"github.com/shadow1ng/fscan/webscan/lib"
|
"github.com/shadow1ng/fscan/webscan/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ScanReport summarizes one scan execution.
|
||||||
|
type ScanReport struct {
|
||||||
|
Duration time.Duration
|
||||||
|
TasksTotal int64
|
||||||
|
TasksCompleted int64
|
||||||
|
Packets int64
|
||||||
|
TCPPackets int64
|
||||||
|
TCPSuccessPackets int64
|
||||||
|
TCPFailedPackets int64
|
||||||
|
UDPPackets int64
|
||||||
|
HTTPPackets int64
|
||||||
|
ResourceExhausted int64
|
||||||
|
}
|
||||||
|
|
||||||
// ScanStrategy 定义扫描策略接口
|
// ScanStrategy 定义扫描策略接口
|
||||||
type ScanStrategy interface {
|
type ScanStrategy interface {
|
||||||
Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
Execute(ctx context.Context, session *common.ScanSession, info common.HostInfo, ch chan struct{}, wg *sync.WaitGroup)
|
||||||
@@ -78,7 +92,8 @@ func selectStrategy(config *common.Config, state *common.State, info common.Host
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunScan 执行整体扫描流程
|
// RunScan 执行整体扫描流程
|
||||||
func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) {
|
func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSession) (ScanReport, error) {
|
||||||
|
start := time.Now()
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -88,7 +103,7 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess
|
|||||||
// 初始化HTTP客户端(静默,无需日志)
|
// 初始化HTTP客户端(静默,无需日志)
|
||||||
if err := lib.Inithttp(config); err != nil {
|
if err := lib.Inithttp(config); err != nil {
|
||||||
session.LogError(i18n.Tr("http_client_init_failed", err))
|
session.LogError(i18n.Tr("http_client_init_failed", err))
|
||||||
return
|
return buildScanReport(state, start), fmt.Errorf("initialize http client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择策略
|
// 选择策略
|
||||||
@@ -131,6 +146,25 @@ func RunScan(ctx context.Context, info common.HostInfo, session *common.ScanSess
|
|||||||
|
|
||||||
// 完成扫描
|
// 完成扫描
|
||||||
finishScan(session)
|
finishScan(session)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return buildScanReport(state, start), err
|
||||||
|
}
|
||||||
|
return buildScanReport(state, start), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildScanReport(state *common.State, start time.Time) ScanReport {
|
||||||
|
return ScanReport{
|
||||||
|
Duration: time.Since(start),
|
||||||
|
TasksTotal: state.GetEnd(),
|
||||||
|
TasksCompleted: state.GetNum(),
|
||||||
|
Packets: state.GetPacketCount(),
|
||||||
|
TCPPackets: state.GetTCPPacketCount(),
|
||||||
|
TCPSuccessPackets: state.GetTCPSuccessPacketCount(),
|
||||||
|
TCPFailedPackets: state.GetTCPFailedPacketCount(),
|
||||||
|
UDPPackets: state.GetUDPPacketCount(),
|
||||||
|
HTTPPackets: state.GetHTTPPacketCount(),
|
||||||
|
ResourceExhausted: state.GetResourceExhaustedCount(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishScan 完成扫描并输出结果
|
// finishScan 完成扫描并输出结果
|
||||||
|
|||||||
@@ -68,5 +68,8 @@ func main() {
|
|||||||
defer common.CloseLogger()
|
defer common.CloseLogger()
|
||||||
|
|
||||||
// 执行扫描
|
// 执行扫描
|
||||||
core.RunScan(context.Background(), *result.Info, result.Session)
|
if _, err := core.RunScan(context.Background(), *result.Info, result.Session); err != nil {
|
||||||
|
common.LogError(i18n.Tr("error_generic", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-17
@@ -32,19 +32,22 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := fscan.NewScanner(config)
|
scanner := fscan.NewScanner(config)
|
||||||
err := scanner.ScanEach(context.Background(), func(result fscan.Result) error {
|
report, err := scanner.ScanReport(context.Background(), fscan.Target{
|
||||||
// Store, forward, or filter the result in the embedding system.
|
Host: "192.168.1.10",
|
||||||
return nil
|
Ports: []int{22, 3306, 6379},
|
||||||
}, fscan.Target{
|
})
|
||||||
Host: "192.168.1.10",
|
if err != nil {
|
||||||
Ports: []int{22, 3306, 6379},
|
panic(err)
|
||||||
})
|
}
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("scan finished")
|
fmt.Printf("scan finished: %+v stats=%+v\n", report.Summary, report.Stats)
|
||||||
|
for _, result := range report.Results {
|
||||||
|
// Store, forward, or filter the result in the embedding system.
|
||||||
|
if credential, ok := result.AsCredential(); ok {
|
||||||
|
fmt.Printf("weak credential: %s %s:%s\n", credential.Target, credential.Username, credential.Password)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,11 +59,13 @@ By default, the SDK runs a conservative service-oriented plugin set and blocks p
|
|||||||
|
|
||||||
| Area | API |
|
| Area | API |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Scanning | `NewScanner`, `Scan`, `ScanEach` |
|
| Scanning | `NewScanner`, `Scan`, `ScanEach`, `ScanReport` |
|
||||||
| Configuration | `Config`, `Target`, `CredentialPair`, `ValidateConfig` |
|
| Configuration | `Config`, `Target`, `CredentialPair`, `ValidateConfig` |
|
||||||
| Plugins | `DefaultSafePlugins`, `ListPlugins`, `GetPlugin`, `IsSafePlugin`, `PluginInfo` |
|
| Plugins | `DefaultSafePlugins`, `ListPlugins`, `GetPlugin`, `IsSafePlugin`, `PluginCapabilities`, `PluginInfo` |
|
||||||
| Results | `Result`, `ResultTypeHost`, `ResultTypePort`, `ResultTypeService`, `ResultTypeVuln` |
|
| Results | `Result`, `ResultTypeHost`, `ResultTypePort`, `ResultTypeService`, `ResultTypeVuln` |
|
||||||
| Result helpers | `Port`, `Service`, `Plugin`, `Username`, `Password`, `Banner`, `Vulnerability`, `URL`, `Protocol`, `IsWeb`, `IsCredential` |
|
| Result helpers | `Port`, `Service`, `Plugin`, `Username`, `Password`, `Banner`, `Vulnerability`, `URL`, `Protocol`, `IsWeb`, `IsCredential`, `AsPort`, `AsService`, `AsCredential`, `AsVulnerability` |
|
||||||
| Summary | `SummarizeResults`, `ResultSummary.Add` |
|
| Summary and stats | `ScanReport`, `ScanStats`, `SummarizeResults`, `ResultSummary.Add` |
|
||||||
|
|
||||||
Use `Scan` when you want all results returned as a slice. Use `ScanEach` when results should be streamed into another system; handler calls are serialized, and returning an error stops the scan and returns that error.
|
Use `Scan` when you want all results returned as a slice. Use `ScanReport` when the embedding system also needs summary counts and runtime counters. Use `ScanEach` when results should be streamed into another system; handler calls are serialized, and returning an error stops the scan and returns that error.
|
||||||
|
|
||||||
|
Plugin capabilities are exposed as stable strings: `detect`, `auth-check`, `brute`, `poc`, and `local-effect`. Default embedded safe mode blocks `poc` and `local-effect` plugins unless `AllowUnsafePlugins` is set.
|
||||||
|
|||||||
@@ -192,6 +192,75 @@ func (r Result) IsWeb() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsPort returns a typed port result when the result describes an open port.
|
||||||
|
func (r Result) AsPort() (PortResult, bool) {
|
||||||
|
if !r.IsPort() {
|
||||||
|
return PortResult{}, false
|
||||||
|
}
|
||||||
|
port, ok := r.Port()
|
||||||
|
if !ok {
|
||||||
|
return PortResult{}, false
|
||||||
|
}
|
||||||
|
return PortResult{Target: r.Target, Port: port}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsService returns a typed service result when service-like fields are present.
|
||||||
|
func (r Result) AsService() (ServiceResult, bool) {
|
||||||
|
if !r.IsService() {
|
||||||
|
return ServiceResult{}, false
|
||||||
|
}
|
||||||
|
service := ServiceResult{
|
||||||
|
Target: r.Target,
|
||||||
|
IsWeb: r.IsWeb(),
|
||||||
|
}
|
||||||
|
if port, ok := r.Port(); ok {
|
||||||
|
service.Port = port
|
||||||
|
}
|
||||||
|
service.Service, _ = r.Service()
|
||||||
|
service.Banner, _ = r.Banner()
|
||||||
|
service.Product, _ = r.DetailString("product")
|
||||||
|
service.Version, _ = r.DetailString("version")
|
||||||
|
service.Protocol, _ = r.Protocol()
|
||||||
|
service.URL, _ = r.URL()
|
||||||
|
return service, service.Service != "" || service.Banner != "" || service.URL != "" || service.Port != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsCredential returns a typed credential result when the result is a weak credential.
|
||||||
|
func (r Result) AsCredential() (CredentialResult, bool) {
|
||||||
|
if !r.IsCredential() {
|
||||||
|
return CredentialResult{}, false
|
||||||
|
}
|
||||||
|
username, userOK := r.Username()
|
||||||
|
password, passOK := r.Password()
|
||||||
|
if !userOK && !passOK {
|
||||||
|
return CredentialResult{}, false
|
||||||
|
}
|
||||||
|
service, _ := r.Service()
|
||||||
|
return CredentialResult{
|
||||||
|
Target: r.Target,
|
||||||
|
Service: service,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsVulnerability returns a typed vulnerability result when vulnerability data is present.
|
||||||
|
func (r Result) AsVulnerability() (VulnerabilityResult, bool) {
|
||||||
|
if !r.IsVuln() || r.IsCredential() {
|
||||||
|
return VulnerabilityResult{}, false
|
||||||
|
}
|
||||||
|
vulnerability, ok := r.Vulnerability()
|
||||||
|
if !ok || vulnerability == "" {
|
||||||
|
return VulnerabilityResult{}, false
|
||||||
|
}
|
||||||
|
service, _ := r.Service()
|
||||||
|
return VulnerabilityResult{
|
||||||
|
Target: r.Target,
|
||||||
|
Service: service,
|
||||||
|
Vulnerability: vulnerability,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
func intFromInt64(v int64) (int, bool) {
|
func intFromInt64(v int64) (int, bool) {
|
||||||
max := int64(^uint(0) >> 1)
|
max := int64(^uint(0) >> 1)
|
||||||
min := -max - 1
|
min := -max - 1
|
||||||
|
|||||||
@@ -59,9 +59,11 @@ func TestResultPortDoesNotParseBareIPv6(t *testing.T) {
|
|||||||
|
|
||||||
func TestResultCredentialHelpers(t *testing.T) {
|
func TestResultCredentialHelpers(t *testing.T) {
|
||||||
result := Result{
|
result := Result{
|
||||||
Type: ResultTypeVuln,
|
Type: ResultTypeVuln,
|
||||||
|
Target: "127.0.0.1:22",
|
||||||
Details: map[string]interface{}{
|
Details: map[string]interface{}{
|
||||||
"type": "weak_credential",
|
"type": "weak_credential",
|
||||||
|
"service": "ssh",
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "toor",
|
"password": "toor",
|
||||||
},
|
},
|
||||||
@@ -81,6 +83,61 @@ func TestResultCredentialHelpers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTypedResultViews(t *testing.T) {
|
||||||
|
portResult, ok := (Result{
|
||||||
|
Type: ResultTypePort,
|
||||||
|
Target: "127.0.0.1",
|
||||||
|
Details: map[string]interface{}{"port": 22},
|
||||||
|
}).AsPort()
|
||||||
|
if !ok || portResult.Port != 22 || portResult.Target != "127.0.0.1" {
|
||||||
|
t.Fatalf("AsPort = %#v/%v, want port 22", portResult, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceResult, ok := (Result{
|
||||||
|
Type: ResultTypeService,
|
||||||
|
Target: "127.0.0.1:80",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"port": 80,
|
||||||
|
"service": "http",
|
||||||
|
"banner": "nginx",
|
||||||
|
"product": "nginx",
|
||||||
|
"version": "1.25",
|
||||||
|
"is_web": true,
|
||||||
|
"protocol": "http",
|
||||||
|
"url": "http://127.0.0.1:80",
|
||||||
|
},
|
||||||
|
}).AsService()
|
||||||
|
if !ok || serviceResult.Service != "http" || serviceResult.Port != 80 || !serviceResult.IsWeb {
|
||||||
|
t.Fatalf("AsService = %#v/%v, want http web service", serviceResult, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialResult, ok := (Result{
|
||||||
|
Type: ResultTypeVuln,
|
||||||
|
Target: "127.0.0.1:22",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"type": "weak_credential",
|
||||||
|
"service": "ssh",
|
||||||
|
"username": "root",
|
||||||
|
"password": "toor",
|
||||||
|
},
|
||||||
|
}).AsCredential()
|
||||||
|
if !ok || credentialResult.Username != "root" || credentialResult.Password != "toor" {
|
||||||
|
t.Fatalf("AsCredential = %#v/%v, want root/toor", credentialResult, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
vulnResult, ok := (Result{
|
||||||
|
Type: ResultTypeVuln,
|
||||||
|
Target: "127.0.0.1:25",
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"service": "smtp",
|
||||||
|
"vulnerability": "open relay",
|
||||||
|
},
|
||||||
|
}).AsVulnerability()
|
||||||
|
if !ok || vulnResult.Service != "smtp" || vulnResult.Vulnerability != "open relay" {
|
||||||
|
t.Fatalf("AsVulnerability = %#v/%v, want smtp/open relay", vulnResult, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSummarizeResults(t *testing.T) {
|
func TestSummarizeResults(t *testing.T) {
|
||||||
results := []Result{
|
results := []Result{
|
||||||
{Type: ResultTypeHost, Target: "127.0.0.1"},
|
{Type: ResultTypeHost, Target: "127.0.0.1"},
|
||||||
|
|||||||
+154
-24
@@ -85,11 +85,12 @@ func GetPlugin(name string) (PluginInfo, bool) {
|
|||||||
return PluginInfo{}, false
|
return PluginInfo{}, false
|
||||||
}
|
}
|
||||||
return PluginInfo{
|
return PluginInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
Types: pluginTypes(name),
|
Types: pluginTypes(name),
|
||||||
Ports: pluginPorts(name),
|
Capabilities: PluginCapabilities(name),
|
||||||
Safe: IsSafePlugin(name),
|
Ports: pluginPorts(name),
|
||||||
Default: isDefaultSafePlugin(name),
|
Safe: IsSafePlugin(name),
|
||||||
|
Default: isDefaultSafePlugin(name),
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,31 +110,56 @@ func IsSafePlugin(name string) bool {
|
|||||||
if name == "" || !plugins.Exists(name) {
|
if name == "" || !plugins.Exists(name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return plugins.IsSafe(name)
|
return plugins.IsSafe(name) && !hasPluginCapability(name, PluginCapabilityPOC, PluginCapabilityLocalEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginCapabilities returns the SDK-facing behavior classes for a plugin.
|
||||||
|
func PluginCapabilities(name string) []string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" || !plugins.Exists(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pluginCapabilities(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
report, err := s.ScanReport(ctx, targets...)
|
||||||
|
return report.Results, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanReport runs the scanner and returns results with summary and runtime stats.
|
||||||
|
func (s *Scanner) ScanReport(ctx context.Context, targets ...Target) (ScanReport, error) {
|
||||||
var (
|
var (
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
results []Result
|
results []Result
|
||||||
)
|
)
|
||||||
err := s.ScanEach(ctx, func(result Result) error {
|
stats, err := s.scanEach(ctx, func(result Result) error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}, targets...)
|
}, targets...)
|
||||||
return snapshotResults(&mu, results), err
|
results = snapshotResults(&mu, results)
|
||||||
|
return ScanReport{
|
||||||
|
Results: results,
|
||||||
|
Summary: SummarizeResults(results),
|
||||||
|
Stats: stats,
|
||||||
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScanEach runs the scanner and calls handle serially for each structured
|
// ScanEach runs the scanner and calls handle serially for each structured
|
||||||
// result without retaining all results in memory. If handle returns an error,
|
// result without retaining all results in memory. If handle returns an error,
|
||||||
// the scan context is canceled and that error is returned.
|
// the scan context is canceled and that error is returned.
|
||||||
func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ...Target) error {
|
func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ...Target) error {
|
||||||
|
_, err := s.scanEach(ctx, handle, targets...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) scanEach(ctx context.Context, handle ResultHandler, targets ...Target) (ScanStats, error) {
|
||||||
if handle == nil {
|
if handle == nil {
|
||||||
return fmt.Errorf("fscan: result handler is required")
|
return ScanStats{}, fmt.Errorf("fscan: result handler is required")
|
||||||
}
|
}
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
@@ -142,7 +168,7 @@ func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ..
|
|||||||
targets = s.config.Targets
|
targets = s.config.Targets
|
||||||
}
|
}
|
||||||
if err := validateConfig(s.config, targets); err != nil {
|
if err := validateConfig(s.config, targets); err != nil {
|
||||||
return err
|
return ScanStats{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
@@ -155,13 +181,14 @@ func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ..
|
|||||||
handleMu sync.Mutex
|
handleMu sync.Mutex
|
||||||
handlerErr error
|
handlerErr error
|
||||||
)
|
)
|
||||||
|
var stats ScanStats
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
return stored
|
return stats, stored
|
||||||
}
|
}
|
||||||
return err
|
return stats, err
|
||||||
}
|
}
|
||||||
sink := func(raw *output.ScanResult) error {
|
sink := func(raw *output.ScanResult) error {
|
||||||
if result, ok := convertOutputResult(raw); ok {
|
if result, ok := convertOutputResult(raw); ok {
|
||||||
@@ -179,31 +206,38 @@ func (s *Scanner) ScanEach(ctx context.Context, handle ResultHandler, targets ..
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := s.scanOne(ctx, target, sink); err != nil {
|
report, err := s.scanOne(ctx, target, sink)
|
||||||
return err
|
stats.add(coreStatsToSDK(report))
|
||||||
|
if err != nil {
|
||||||
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
|
return stats, stored
|
||||||
|
}
|
||||||
|
return stats, err
|
||||||
}
|
}
|
||||||
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
return stored
|
return stats, stored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
if stored := getHandlerError(&errMu, &handlerErr); stored != nil {
|
||||||
return stored
|
return stats, stored
|
||||||
}
|
}
|
||||||
return ctx.Err()
|
return stats, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.ResultSink) error {
|
func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.ResultSink) (core.ScanReport, error) {
|
||||||
fv := buildFlagVars(s.config, target)
|
fv := buildFlagVars(s.config, target)
|
||||||
info := common.HostInfo{Host: strings.TrimSpace(target.Host), URL: strings.TrimSpace(target.URL)}
|
info := common.HostInfo{Host: strings.TrimSpace(target.Host), URL: strings.TrimSpace(target.URL)}
|
||||||
|
|
||||||
previousLanguage := i18n.GetLanguage()
|
if strings.TrimSpace(s.config.Language) != "" {
|
||||||
i18n.SetLanguage(fv.Language)
|
previousLanguage := i18n.GetLanguage()
|
||||||
defer i18n.SetLanguage(previousLanguage)
|
i18n.SetLanguage(fv.Language)
|
||||||
|
defer i18n.SetLanguage(previousLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
cfg, state, err := common.BuildConfig(fv, &info)
|
cfg, state, err := common.BuildConfig(fv, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return core.ScanReport{}, err
|
||||||
}
|
}
|
||||||
if len(s.config.UserPassPairs) > 0 {
|
if len(s.config.UserPassPairs) > 0 {
|
||||||
cfg.Credentials.UserPassPairs = make([]commonconfig.CredentialPair, 0, len(s.config.UserPassPairs))
|
cfg.Credentials.UserPassPairs = make([]commonconfig.CredentialPair, 0, len(s.config.UserPassPairs))
|
||||||
@@ -221,8 +255,7 @@ func (s *Scanner) scanOne(ctx context.Context, target Target, sink common.Result
|
|||||||
|
|
||||||
session := common.NewScanSession(cfg, state, fv)
|
session := common.NewScanSession(cfg, state, fv)
|
||||||
session.ResultSink = sink
|
session.ResultSink = sink
|
||||||
core.RunScan(ctx, info, session)
|
return core.RunScan(ctx, info, session)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateConfig(config Config, targets []Target) error {
|
func validateConfig(config Config, targets []Target) error {
|
||||||
@@ -392,6 +425,75 @@ func pluginPorts(name string) []int {
|
|||||||
return ports
|
return ports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pluginCapabilities(name string) []string {
|
||||||
|
capSet := map[string]struct{}{}
|
||||||
|
add := func(capability string) {
|
||||||
|
capSet[capability] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugins.HasType(name, PluginTypeService) || plugins.HasType(name, PluginTypeWeb) {
|
||||||
|
add(PluginCapabilityDetect)
|
||||||
|
}
|
||||||
|
if serviceAuthPlugins[name] {
|
||||||
|
add(PluginCapabilityAuthCheck)
|
||||||
|
add(PluginCapabilityBrute)
|
||||||
|
}
|
||||||
|
if activePOCPlugins[name] || strings.Contains(name, "poc") {
|
||||||
|
add(PluginCapabilityPOC)
|
||||||
|
}
|
||||||
|
if plugins.HasType(name, PluginTypeLocal) {
|
||||||
|
add(PluginCapabilityLocalEffect)
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilities := make([]string, 0, len(capSet))
|
||||||
|
for capability := range capSet {
|
||||||
|
capabilities = append(capabilities, capability)
|
||||||
|
}
|
||||||
|
sort.Strings(capabilities)
|
||||||
|
return capabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPluginCapability(name string, capabilities ...string) bool {
|
||||||
|
pluginCaps := pluginCapabilities(name)
|
||||||
|
for _, want := range capabilities {
|
||||||
|
for _, got := range pluginCaps {
|
||||||
|
if got == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAuthPlugins = map[string]bool{
|
||||||
|
"activemq": true,
|
||||||
|
"cassandra": true,
|
||||||
|
"elasticsearch": true,
|
||||||
|
"ftp": true,
|
||||||
|
"kafka": true,
|
||||||
|
"ldap": true,
|
||||||
|
"memcached": true,
|
||||||
|
"mongodb": true,
|
||||||
|
"mssql": true,
|
||||||
|
"mysql": true,
|
||||||
|
"neo4j": true,
|
||||||
|
"oracle": true,
|
||||||
|
"postgresql": true,
|
||||||
|
"rabbitmq": true,
|
||||||
|
"redis": true,
|
||||||
|
"rsync": true,
|
||||||
|
"smb": true,
|
||||||
|
"smtp": true,
|
||||||
|
"ssh": true,
|
||||||
|
"telnet": true,
|
||||||
|
"vnc": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var activePOCPlugins = map[string]bool{
|
||||||
|
"ms17010": true,
|
||||||
|
"webpoc": true,
|
||||||
|
}
|
||||||
|
|
||||||
func isDefaultSafePlugin(name string) bool {
|
func isDefaultSafePlugin(name string) bool {
|
||||||
for _, plugin := range defaultSafePlugins {
|
for _, plugin := range defaultSafePlugins {
|
||||||
if plugin == name {
|
if plugin == name {
|
||||||
@@ -426,6 +528,34 @@ func convertOutputResult(raw *output.ScanResult) (Result, bool) {
|
|||||||
return result, result.Target != "" || result.Status != ""
|
return result, result.Target != "" || result.Status != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func coreStatsToSDK(report core.ScanReport) ScanStats {
|
||||||
|
return ScanStats{
|
||||||
|
Duration: report.Duration,
|
||||||
|
TasksTotal: report.TasksTotal,
|
||||||
|
TasksCompleted: report.TasksCompleted,
|
||||||
|
Packets: report.Packets,
|
||||||
|
TCPPackets: report.TCPPackets,
|
||||||
|
TCPSuccessPackets: report.TCPSuccessPackets,
|
||||||
|
TCPFailedPackets: report.TCPFailedPackets,
|
||||||
|
UDPPackets: report.UDPPackets,
|
||||||
|
HTTPPackets: report.HTTPPackets,
|
||||||
|
ResourceExhausted: report.ResourceExhausted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScanStats) add(other ScanStats) {
|
||||||
|
s.Duration += other.Duration
|
||||||
|
s.TasksTotal += other.TasksTotal
|
||||||
|
s.TasksCompleted += other.TasksCompleted
|
||||||
|
s.Packets += other.Packets
|
||||||
|
s.TCPPackets += other.TCPPackets
|
||||||
|
s.TCPSuccessPackets += other.TCPSuccessPackets
|
||||||
|
s.TCPFailedPackets += other.TCPFailedPackets
|
||||||
|
s.UDPPackets += other.UDPPackets
|
||||||
|
s.HTTPPackets += other.HTTPPackets
|
||||||
|
s.ResourceExhausted += other.ResourceExhausted
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ func TestIsSafePlugin(t *testing.T) {
|
|||||||
if IsSafePlugin("webpoc") {
|
if IsSafePlugin("webpoc") {
|
||||||
t.Fatal("webpoc should not be safe")
|
t.Fatal("webpoc should not be safe")
|
||||||
}
|
}
|
||||||
|
if IsSafePlugin("ms17010") {
|
||||||
|
t.Fatal("active poc plugins should not be safe")
|
||||||
|
}
|
||||||
if IsSafePlugin("definitely-missing") {
|
if IsSafePlugin("definitely-missing") {
|
||||||
t.Fatal("unknown plugin should not be safe")
|
t.Fatal("unknown plugin should not be safe")
|
||||||
}
|
}
|
||||||
@@ -107,6 +110,9 @@ func TestListPlugins(t *testing.T) {
|
|||||||
if !containsString(ssh.Types, PluginTypeService) {
|
if !containsString(ssh.Types, PluginTypeService) {
|
||||||
t.Fatalf("ssh types = %#v, want service", ssh.Types)
|
t.Fatalf("ssh types = %#v, want service", ssh.Types)
|
||||||
}
|
}
|
||||||
|
if !containsString(ssh.Capabilities, PluginCapabilityDetect) || !containsString(ssh.Capabilities, PluginCapabilityAuthCheck) {
|
||||||
|
t.Fatalf("ssh capabilities = %#v, want detect/auth-check", ssh.Capabilities)
|
||||||
|
}
|
||||||
if !containsInt(ssh.Ports, 22) {
|
if !containsInt(ssh.Ports, 22) {
|
||||||
t.Fatalf("ssh ports = %#v, want 22", ssh.Ports)
|
t.Fatalf("ssh ports = %#v, want 22", ssh.Ports)
|
||||||
}
|
}
|
||||||
@@ -123,6 +129,9 @@ func TestListPlugins(t *testing.T) {
|
|||||||
if !containsString(webpoc.Types, PluginTypeWeb) {
|
if !containsString(webpoc.Types, PluginTypeWeb) {
|
||||||
t.Fatalf("webpoc types = %#v, want web", webpoc.Types)
|
t.Fatalf("webpoc types = %#v, want web", webpoc.Types)
|
||||||
}
|
}
|
||||||
|
if !containsString(webpoc.Capabilities, PluginCapabilityPOC) {
|
||||||
|
t.Fatalf("webpoc capabilities = %#v, want poc", webpoc.Capabilities)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestScanHonorsCanceledContext(t *testing.T) {
|
func TestScanHonorsCanceledContext(t *testing.T) {
|
||||||
@@ -247,6 +256,37 @@ func TestScanUsesConfigTargets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScanReportReturnsSummaryAndStats(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
port := listener.Addr().(*net.TCPAddr).Port
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Threads: 16,
|
||||||
|
Plugins: []string{"ftp"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
report, err := scanner.ScanReport(ctx, Target{Host: "127.0.0.1", Ports: []int{port}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(report.Results) == 0 || report.Summary.Total != len(report.Results) {
|
||||||
|
t.Fatalf("report summary/results mismatch: %#v", report)
|
||||||
|
}
|
||||||
|
if report.Stats.Duration <= 0 {
|
||||||
|
t.Fatalf("report duration = %s, want positive", report.Stats.Duration)
|
||||||
|
}
|
||||||
|
if report.Stats.TasksCompleted == 0 {
|
||||||
|
t.Fatalf("report stats = %#v, want completed tasks", report.Stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestScanExplicitTargetsOverrideConfigTargets(t *testing.T) {
|
func TestScanExplicitTargetsOverrideConfigTargets(t *testing.T) {
|
||||||
configured := startFTPListener(t)
|
configured := startFTPListener(t)
|
||||||
defer configured.Close()
|
defer configured.Close()
|
||||||
@@ -441,6 +481,31 @@ func TestScanDoesNotReplaceGlobalRuntime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScanWithoutLanguageDoesNotTouchGlobalLanguage(t *testing.T) {
|
||||||
|
listener := startFTPListener(t)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
previousLanguage := i18n.GetLanguage()
|
||||||
|
defer i18n.SetLanguage(previousLanguage)
|
||||||
|
i18n.SetLanguage(i18n.LangEN)
|
||||||
|
|
||||||
|
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 got := i18n.GetLanguage(); got != i18n.LangEN {
|
||||||
|
t.Fatalf("SDK scan leaked global language = %q, want %q", got, i18n.LangEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startFTPListener(t *testing.T) net.Listener {
|
func startFTPListener(t *testing.T) net.Listener {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
+74
-5
@@ -13,6 +13,19 @@ const (
|
|||||||
PluginTypeService = "service"
|
PluginTypeService = "service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginCapabilityDetect marks passive or low-impact detection behavior.
|
||||||
|
PluginCapabilityDetect = "detect"
|
||||||
|
// PluginCapabilityAuthCheck marks credential validation behavior.
|
||||||
|
PluginCapabilityAuthCheck = "auth-check"
|
||||||
|
// PluginCapabilityBrute marks dictionary-style credential attempts.
|
||||||
|
PluginCapabilityBrute = "brute"
|
||||||
|
// PluginCapabilityPOC marks active vulnerability checks or exploitation.
|
||||||
|
PluginCapabilityPOC = "poc"
|
||||||
|
// PluginCapabilityLocalEffect marks plugins that change or inspect local host state.
|
||||||
|
PluginCapabilityLocalEffect = "local-effect"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ResultTypeHost reports a live host.
|
// ResultTypeHost reports a live host.
|
||||||
ResultTypeHost = "HOST"
|
ResultTypeHost = "HOST"
|
||||||
@@ -40,11 +53,12 @@ type CredentialPair struct {
|
|||||||
|
|
||||||
// PluginInfo describes one registered scanner plugin.
|
// PluginInfo describes one registered scanner plugin.
|
||||||
type PluginInfo struct {
|
type PluginInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Types []string `json:"types,omitempty"`
|
Types []string `json:"types,omitempty"`
|
||||||
Ports []int `json:"ports,omitempty"`
|
Capabilities []string `json:"capabilities,omitempty"`
|
||||||
Safe bool `json:"safe"`
|
Ports []int `json:"ports,omitempty"`
|
||||||
Default bool `json:"default"`
|
Safe bool `json:"safe"`
|
||||||
|
Default bool `json:"default"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultSummary counts common result categories.
|
// ResultSummary counts common result categories.
|
||||||
@@ -58,6 +72,27 @@ type ResultSummary struct {
|
|||||||
Credentials int `json:"credentials"`
|
Credentials int `json:"credentials"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScanStats reports runtime counters for one embedded scan call.
|
||||||
|
type ScanStats struct {
|
||||||
|
Duration time.Duration `json:"duration"`
|
||||||
|
TasksTotal int64 `json:"tasks_total"`
|
||||||
|
TasksCompleted int64 `json:"tasks_completed"`
|
||||||
|
Packets int64 `json:"packets"`
|
||||||
|
TCPPackets int64 `json:"tcp_packets"`
|
||||||
|
TCPSuccessPackets int64 `json:"tcp_success_packets"`
|
||||||
|
TCPFailedPackets int64 `json:"tcp_failed_packets"`
|
||||||
|
UDPPackets int64 `json:"udp_packets"`
|
||||||
|
HTTPPackets int64 `json:"http_packets"`
|
||||||
|
ResourceExhausted int64 `json:"resource_exhausted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanReport returns structured results with summary and runtime counters.
|
||||||
|
type ScanReport struct {
|
||||||
|
Results []Result `json:"results"`
|
||||||
|
Summary ResultSummary `json:"summary"`
|
||||||
|
Stats ScanStats `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
// ResultHandler receives one structured result. Calls are serialized by the
|
// ResultHandler receives one structured result. Calls are serialized by the
|
||||||
// scanner. Returning an error asks the scanner to stop and returns that error
|
// scanner. Returning an error asks the scanner to stop and returns that error
|
||||||
// to the caller.
|
// to the caller.
|
||||||
@@ -114,3 +149,37 @@ type Result struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Details map[string]interface{} `json:"details,omitempty"`
|
Details map[string]interface{} `json:"details,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PortResult is a typed view over an open port result.
|
||||||
|
type PortResult struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceResult is a typed view over a service or web detection result.
|
||||||
|
type ServiceResult struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
Banner string `json:"banner,omitempty"`
|
||||||
|
Product string `json:"product,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Protocol string `json:"protocol,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
IsWeb bool `json:"is_web,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialResult is a typed view over a weak credential result.
|
||||||
|
type CredentialResult struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VulnerabilityResult is a typed view over a vulnerability result.
|
||||||
|
type VulnerabilityResult struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
Vulnerability string `json:"vulnerability"`
|
||||||
|
}
|
||||||
|
|||||||
+8
-5
@@ -5,6 +5,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -47,10 +48,10 @@ type ScanRequest struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
// POC
|
// POC
|
||||||
PocPath string `json:"poc_path"`
|
PocPath string `json:"poc_path"`
|
||||||
PocName string `json:"poc_name"`
|
PocName string `json:"poc_name"`
|
||||||
PocFull bool `json:"poc_full"`
|
PocFull bool `json:"poc_full"`
|
||||||
DisablePoc bool `json:"disable_poc"`
|
DisablePoc bool `json:"disable_poc"`
|
||||||
|
|
||||||
// 项目缓存
|
// 项目缓存
|
||||||
ProjectID string `json:"project_id,omitempty"`
|
ProjectID string `json:"project_id,omitempty"`
|
||||||
@@ -234,7 +235,9 @@ func (h *ScanHandler) runScan(req ScanRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 执行扫描
|
// 执行扫描
|
||||||
core.RunScan(ctx, info, session)
|
if _, err := core.RunScan(ctx, info, session); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
common.LogError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// 项目缓存回写:合并本次扫描结果
|
// 项目缓存回写:合并本次扫描结果
|
||||||
if req.ProjectID != "" {
|
if req.ProjectID != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user