mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-26 13:11:53 +08:00
add embeddable scanner SDK
This commit is contained in:
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
result.txt
|
result.txt
|
||||||
result.json
|
result.json
|
||||||
main
|
/main
|
||||||
.idea
|
.idea
|
||||||
fscan.exe
|
fscan.exe
|
||||||
fscan
|
/fscan
|
||||||
fscanapi.csv
|
fscanapi.csv
|
||||||
|
|
||||||
# IDE files / IDE 文件
|
# IDE files / IDE 文件
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ 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()
|
||||||
|
|||||||
@@ -88,3 +88,13 @@ func CloseLogger() {
|
|||||||
globalLogger.Close()
|
globalLogger.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetLogger clears the process-wide logger so embedded callers can rebuild it
|
||||||
|
// after replacing runtime configuration.
|
||||||
|
func ResetLogger() {
|
||||||
|
if globalLogger != nil {
|
||||||
|
globalLogger.Close()
|
||||||
|
}
|
||||||
|
globalLogger = nil
|
||||||
|
loggerOnce = sync.Once{}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# fscan SDK
|
||||||
|
|
||||||
|
`pkg/fscan` exposes fscan as an embeddable Go scanner while keeping the CLI unchanged.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
fscan "github.com/shadow1ng/fscan/pkg/fscan"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
scanner := fscan.NewScanner(fscan.Config{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
Threads: 128,
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Plugins: []string{"ssh", "mysql", "redis"},
|
||||||
|
})
|
||||||
|
|
||||||
|
results, err := scanner.Scan(context.Background(), fscan.Target{
|
||||||
|
Host: "192.168.1.10",
|
||||||
|
Ports: []int{22, 3306, 6379},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range 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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Package fscan exposes fscan as an embeddable scanner library.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
package fscan
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
commonconfig "github.com/shadow1ng/fscan/common/config"
|
||||||
|
"github.com/shadow1ng/fscan/common/i18n"
|
||||||
|
"github.com/shadow1ng/fscan/core"
|
||||||
|
"github.com/shadow1ng/fscan/plugins"
|
||||||
|
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/services"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
var scanMu sync.Mutex
|
||||||
|
|
||||||
|
// Scanner runs fscan from another Go process.
|
||||||
|
type Scanner struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScanner creates an embedded scanner.
|
||||||
|
func NewScanner(config Config) *Scanner {
|
||||||
|
return &Scanner{config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan runs the scanner for the provided targets and returns structured
|
||||||
|
// findings. If no targets are provided, Config.Targets is used.
|
||||||
|
func (s *Scanner) Scan(ctx context.Context, targets ...Target) ([]Result, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if len(targets) == 0 {
|
||||||
|
targets = s.config.Targets
|
||||||
|
}
|
||||||
|
if err := validateConfig(s.config, targets); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanMu.Lock()
|
||||||
|
defer scanMu.Unlock()
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
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 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return snapshotResults(&mu, results), err
|
||||||
|
}
|
||||||
|
if err := s.scanOne(ctx, target); err != nil {
|
||||||
|
return snapshotResults(&mu, results), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshotResults(&mu, results), ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) scanOne(ctx context.Context, target Target) 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)
|
||||||
|
|
||||||
|
cfg, state, err := common.BuildConfig(globalFV, &info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(s.config.UserPassPairs) > 0 {
|
||||||
|
cfg.Credentials.UserPassPairs = make([]commonconfig.CredentialPair, 0, len(s.config.UserPassPairs))
|
||||||
|
for _, pair := range s.config.UserPassPairs {
|
||||||
|
cfg.Credentials.UserPassPairs = append(cfg.Credentials.UserPassPairs, commonconfig.CredentialPair{
|
||||||
|
Username: pair.Username,
|
||||||
|
Password: pair.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Output.DisableSave = true
|
||||||
|
cfg.Output.Silent = true
|
||||||
|
cfg.Output.DisableProgress = true
|
||||||
|
cfg.Output.ShowProgress = false
|
||||||
|
|
||||||
|
common.SetGlobalConfig(cfg)
|
||||||
|
common.SetGlobalState(state)
|
||||||
|
common.ResetLogger()
|
||||||
|
common.InitLogger()
|
||||||
|
|
||||||
|
session := common.NewScanSession(cfg, state, globalFV)
|
||||||
|
core.RunScan(ctx, info, session)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(config Config, targets []Target) error {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return fmt.Errorf("fscan: at least one target is required")
|
||||||
|
}
|
||||||
|
for _, plugin := range config.Plugins {
|
||||||
|
name := strings.TrimSpace(plugin)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !plugins.Exists(name) {
|
||||||
|
return fmt.Errorf("fscan: plugin %q not found", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, target := range targets {
|
||||||
|
if strings.TrimSpace(target.Host) == "" && strings.TrimSpace(target.URL) == "" {
|
||||||
|
return fmt.Errorf("fscan: target host or URL is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(target.Host) != "" && strings.TrimSpace(target.URL) != "" {
|
||||||
|
return fmt.Errorf("fscan: target cannot set both Host and URL")
|
||||||
|
}
|
||||||
|
for _, port := range target.Ports {
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
return fmt.Errorf("fscan: invalid port %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, port := range config.Ports {
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
return fmt.Errorf("fscan: invalid port %d", port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFlagVars(config Config, target Target) *common.FlagVars {
|
||||||
|
timeout := secondsOrDefault(config.Timeout, common.DefaultTimeout)
|
||||||
|
webTimeout := secondsOrDefault(config.WebTimeout, 5)
|
||||||
|
|
||||||
|
threadNum := config.Threads
|
||||||
|
if threadNum <= 0 {
|
||||||
|
threadNum = common.DefaultThreadNum
|
||||||
|
}
|
||||||
|
moduleThreads := config.ModuleThreads
|
||||||
|
if moduleThreads <= 0 {
|
||||||
|
moduleThreads = 20
|
||||||
|
}
|
||||||
|
maxRetries := config.MaxRetries
|
||||||
|
if maxRetries <= 0 {
|
||||||
|
maxRetries = 3
|
||||||
|
}
|
||||||
|
maxRedirects := config.MaxRedirects
|
||||||
|
if maxRedirects <= 0 {
|
||||||
|
maxRedirects = 10
|
||||||
|
}
|
||||||
|
pocConcurrency := config.POCConcurrency
|
||||||
|
if pocConcurrency <= 0 {
|
||||||
|
pocConcurrency = 20
|
||||||
|
}
|
||||||
|
icmpRate := config.ICMPRate
|
||||||
|
if icmpRate <= 0 {
|
||||||
|
icmpRate = 0.1
|
||||||
|
}
|
||||||
|
language := config.Language
|
||||||
|
if language == "" {
|
||||||
|
language = common.DefaultLanguage
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := config.Ports
|
||||||
|
if len(target.Ports) > 0 {
|
||||||
|
ports = target.Ports
|
||||||
|
}
|
||||||
|
|
||||||
|
return &common.FlagVars{
|
||||||
|
Host: strings.TrimSpace(target.Host),
|
||||||
|
Ports: formatPorts(ports),
|
||||||
|
ScanMode: formatPlugins(config.Plugins),
|
||||||
|
ThreadNum: threadNum,
|
||||||
|
ModuleThreadNum: moduleThreads,
|
||||||
|
TimeoutSec: timeout,
|
||||||
|
GlobalTimeout: 180,
|
||||||
|
DisablePing: config.DisablePing,
|
||||||
|
DisableTcpProbe: config.DisableTCPProbe,
|
||||||
|
AliveOnly: false,
|
||||||
|
DisableBrute: config.DisableBrute,
|
||||||
|
MaxRetries: maxRetries,
|
||||||
|
Username: strings.Join(config.Usernames, ","),
|
||||||
|
Password: strings.Join(config.Passwords, ","),
|
||||||
|
Domain: config.Domain,
|
||||||
|
SSHKeyPath: config.SSHKeyPath,
|
||||||
|
TargetURL: strings.TrimSpace(target.URL),
|
||||||
|
WebTimeout: webTimeout,
|
||||||
|
MaxRedirects: maxRedirects,
|
||||||
|
HTTPProxy: config.HTTPProxy,
|
||||||
|
Socks5Proxy: config.Socks5Proxy,
|
||||||
|
Iface: config.Interface,
|
||||||
|
PocPath: config.POCPath,
|
||||||
|
PocName: config.POCName,
|
||||||
|
PocFull: config.POCFull,
|
||||||
|
PocNum: pocConcurrency,
|
||||||
|
DisablePocScan: config.DisablePOCScan,
|
||||||
|
PacketRateLimit: config.PacketRateLimit,
|
||||||
|
MaxPacketCount: config.MaxPacketCount,
|
||||||
|
ICMPRate: icmpRate,
|
||||||
|
Outputfile: "result.txt",
|
||||||
|
OutputFormat: "txt",
|
||||||
|
DisableSave: true,
|
||||||
|
Silent: true,
|
||||||
|
NoColor: true,
|
||||||
|
LogLevel: common.LogLevelError,
|
||||||
|
DisableProgress: true,
|
||||||
|
Language: language,
|
||||||
|
ForwardShellPort: 4444,
|
||||||
|
KeyloggerOutputFile: "keylog.txt",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPlugins(plugins []string) string {
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(plugins))
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
plugin = strings.TrimSpace(plugin)
|
||||||
|
if plugin != "" {
|
||||||
|
parts = append(parts, plugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPorts(ports []int) string {
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return commonconfig.MainPorts
|
||||||
|
}
|
||||||
|
ports = append([]int(nil), ports...)
|
||||||
|
sort.Ints(ports)
|
||||||
|
parts := make([]string, 0, len(ports))
|
||||||
|
for _, port := range ports {
|
||||||
|
parts = append(parts, strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func secondsOrDefault(value time.Duration, fallback int) int64 {
|
||||||
|
if value <= 0 {
|
||||||
|
return int64(fallback)
|
||||||
|
}
|
||||||
|
seconds := int64(value.Round(time.Second) / time.Second)
|
||||||
|
if seconds < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCallbackResult(raw interface{}) (Result, bool) {
|
||||||
|
item, ok := raw.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return Result{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
result := Result{
|
||||||
|
Type: stringValue(item["type"]),
|
||||||
|
Target: stringValue(item["target"]),
|
||||||
|
Status: stringValue(item["status"]),
|
||||||
|
Details: mapValue(item["details"]),
|
||||||
|
}
|
||||||
|
if t, ok := item["time"].(time.Time); ok {
|
||||||
|
result.Time = t
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return append([]Result(nil), results...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonconfig "github.com/shadow1ng/fscan/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildFlagVarsDefaults(t *testing.T) {
|
||||||
|
fv := buildFlagVars(Config{}, Target{Host: "127.0.0.1"})
|
||||||
|
|
||||||
|
if fv.Host != "127.0.0.1" {
|
||||||
|
t.Fatalf("Host = %q", fv.Host)
|
||||||
|
}
|
||||||
|
if fv.Ports != commonconfig.MainPorts {
|
||||||
|
t.Fatalf("Ports = %q, want MainPorts", fv.Ports)
|
||||||
|
}
|
||||||
|
if fv.ScanMode != "all" {
|
||||||
|
t.Fatalf("ScanMode = %q, want all", fv.ScanMode)
|
||||||
|
}
|
||||||
|
if !fv.DisableSave || !fv.Silent || !fv.DisableProgress {
|
||||||
|
t.Fatalf("embedded defaults should disable output side effects")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildFlagVarsTargetPortsOverride(t *testing.T) {
|
||||||
|
fv := buildFlagVars(Config{Ports: []int{22, 80}}, Target{Host: "127.0.0.1", Ports: []int{3306, 22}})
|
||||||
|
|
||||||
|
if fv.Ports != "22,3306" {
|
||||||
|
t.Fatalf("Ports = %q, want sorted target override", fv.Ports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateConfig(t *testing.T) {
|
||||||
|
if err := validateConfig(Config{}, nil); err == nil {
|
||||||
|
t.Fatal("expected missing target error")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", URL: "http://127.0.0.1"}}); err == nil {
|
||||||
|
t.Fatal("expected host/url conflict")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{Plugins: []string{"definitely-missing"}}, []Target{{Host: "127.0.0.1"}}); err == nil {
|
||||||
|
t.Fatal("expected missing plugin error")
|
||||||
|
}
|
||||||
|
if err := validateConfig(Config{}, []Target{{Host: "127.0.0.1", Ports: []int{70000}}}); err == nil {
|
||||||
|
t.Fatal("expected invalid port error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanHonorsCanceledContext(t *testing.T) {
|
||||||
|
scanner := NewScanner(Config{
|
||||||
|
DisablePing: true,
|
||||||
|
DisableBrute: true,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Plugins: []string{"redis"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
_, err := scanner.Scan(ctx, Target{Host: "127.0.0.1", Ports: []int{6379}})
|
||||||
|
if err != context.Canceled {
|
||||||
|
t.Fatalf("Scan error = %v, want context.Canceled", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package fscan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Target describes one scan target. Use Host for host/IP/CIDR/range service
|
||||||
|
// scans, or URL for web scans. Ports applies only to Host scans.
|
||||||
|
type Target struct {
|
||||||
|
Host string
|
||||||
|
URL string
|
||||||
|
Ports []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialPair pins one username/password pair.
|
||||||
|
type CredentialPair struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config controls an embedded scan. Zero values use the same conservative
|
||||||
|
// defaults as the CLI, except output is silent and file saving is disabled.
|
||||||
|
type Config struct {
|
||||||
|
Targets []Target
|
||||||
|
|
||||||
|
Plugins []string
|
||||||
|
Ports []int
|
||||||
|
|
||||||
|
Timeout time.Duration
|
||||||
|
Threads int
|
||||||
|
ModuleThreads int
|
||||||
|
MaxRetries int
|
||||||
|
|
||||||
|
DisablePing bool
|
||||||
|
DisableTCPProbe bool
|
||||||
|
DisableBrute bool
|
||||||
|
|
||||||
|
Usernames []string
|
||||||
|
Passwords []string
|
||||||
|
UserPassPairs []CredentialPair
|
||||||
|
Domain string
|
||||||
|
SSHKeyPath string
|
||||||
|
HTTPProxy string
|
||||||
|
Socks5Proxy string
|
||||||
|
Interface string
|
||||||
|
WebTimeout time.Duration
|
||||||
|
MaxRedirects int
|
||||||
|
DisablePOCScan bool
|
||||||
|
POCPath string
|
||||||
|
POCName string
|
||||||
|
POCFull bool
|
||||||
|
POCConcurrency int
|
||||||
|
PacketRateLimit int64
|
||||||
|
MaxPacketCount int64
|
||||||
|
ICMPRate float64
|
||||||
|
Language string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result is the structured scan result returned to embedded callers.
|
||||||
|
type Result struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Details map[string]interface{} `json:"details,omitempty"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user