mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 19:11:25 -07:00
546ab091da
the shared transport was a bare DefaultTransport.Clone() with the stock MaxIdleConnsPerHost=2, and call-sites only close response bodies without draining them - so go could never return a conn to the idle pool and every request re-dialed. high thread counts just thrashed the dialer. - plumb Threads through Options into buildTransport; size MaxIdleConnsPerHost to the worker count (floored) so concurrent workers on one host pool instead of re-dialing, MaxIdleConns=512, MaxConnsPerHost=0, IdleConnTimeout=90s, ForceAttemptHTTP2. the socks5 branch gets its own keepalive net.Dialer so it doesn't lose os-level pooling under proxy.Direct. - add DrainClose to read (capped) and close a body so the conn is reusable. - benchmark proves it: 50 sequential requests reuse 1 conn tuned vs 50 bare.
640 lines
20 KiB
Go
640 lines
20 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
// Package sif provides the main functionality for the SIF (Security Information Finder) tool.
|
|
// It handles the initialization, configuration, and execution of various security scanning modules.
|
|
|
|
package sif
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/dropalldatabases/sif/internal/config"
|
|
"github.com/dropalldatabases/sif/internal/httpx"
|
|
"github.com/dropalldatabases/sif/internal/logger"
|
|
"github.com/dropalldatabases/sif/internal/modules"
|
|
"github.com/dropalldatabases/sif/internal/output"
|
|
"github.com/dropalldatabases/sif/internal/report"
|
|
"github.com/dropalldatabases/sif/internal/scan"
|
|
"github.com/dropalldatabases/sif/internal/scan/builtin"
|
|
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
|
jsscan "github.com/dropalldatabases/sif/internal/scan/js"
|
|
)
|
|
|
|
// App represents the main application structure for sif.
|
|
// It encapsulates the configuration settings, target URLs, and logging information.
|
|
type App struct {
|
|
settings *config.Settings
|
|
targets []string
|
|
logFiles []string
|
|
}
|
|
|
|
// Version is set by main to the resolved build version and shown on the banner.
|
|
var Version = "dev"
|
|
|
|
// reportFileMode is the permission applied to written report files: owner
|
|
// read/write, group/other read. reports aren't secret but may name targets.
|
|
const reportFileMode = 0o644
|
|
|
|
type UrlResult struct {
|
|
Url string `json:"url"`
|
|
Results []ModuleResult
|
|
}
|
|
|
|
type ModuleResult struct {
|
|
Id string `json:"id"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
// ScanResult is the interface that all scan result types must implement.
|
|
// This mirrors the definition in pkg/scan/result.go for use by the main package.
|
|
type ScanResult interface {
|
|
ResultType() string
|
|
}
|
|
|
|
// NewModuleResult creates a ModuleResult with compile-time type safety.
|
|
// The data parameter must implement ScanResult, which is enforced at compile time.
|
|
func NewModuleResult[T ScanResult](data T) ModuleResult {
|
|
return ModuleResult{
|
|
Id: data.ResultType(),
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// New creates a new App struct by parsing the configuration options,
|
|
// figuring out the targets from list or file, etc.
|
|
//
|
|
// Errors if no targets are supplied through URLs or File.
|
|
func New(settings *config.Settings) (*App, error) {
|
|
app := &App{settings: settings}
|
|
|
|
if !settings.ApiMode {
|
|
fmt.Println(output.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
|
|
tagline := "blazing-fast pentesting suite"
|
|
if Version != "dev" {
|
|
tagline += " · v" + Version
|
|
}
|
|
fmt.Println(output.Subheading.Render("\n" + tagline + "\n\nbsd 3-clause · (c) 2022-2026 vmfunc, xyzeva & contributors\n"))
|
|
} else {
|
|
output.SetAPIMode(true)
|
|
}
|
|
|
|
// Skip target requirement if just listing modules
|
|
if settings.ListModules {
|
|
return app, nil
|
|
}
|
|
|
|
switch {
|
|
case len(settings.URLs) > 0:
|
|
app.targets = settings.URLs
|
|
case settings.File != "":
|
|
if _, err := os.Stat(settings.File); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := os.Open(settings.File)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer data.Close()
|
|
|
|
scanner := bufio.NewScanner(data)
|
|
scanner.Split(bufio.ScanLines)
|
|
for scanner.Scan() {
|
|
app.targets = append(app.targets, scanner.Text())
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("target(s) must be supplied with -u or -f\n\nSee 'sif -h' for more information")
|
|
}
|
|
|
|
// Validate all URLs early
|
|
for _, url := range app.targets {
|
|
if err := validateURL(url); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return app, nil
|
|
}
|
|
|
|
// validateURL checks that a URL has a valid HTTP/HTTPS protocol.
|
|
func validateURL(url string) error {
|
|
if url == "" {
|
|
return fmt.Errorf("empty URL provided")
|
|
}
|
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
|
return fmt.Errorf("URL %s must include http:// or https:// protocol", url)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run runs the pentesting suite, with the targets specified, according to the
|
|
// settings specified.
|
|
func (app *App) Run() error {
|
|
// Handle --list-modules before any other processing
|
|
if app.settings.ListModules {
|
|
loader, err := modules.NewLoader()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create module loader: %w", err)
|
|
}
|
|
if err := loader.LoadAll(); err != nil {
|
|
log.Warnf("Failed to load modules: %v", err)
|
|
}
|
|
|
|
// Register built-in Go modules
|
|
builtin.Register()
|
|
|
|
fmt.Println("Available modules:")
|
|
for _, m := range modules.All() {
|
|
info := m.Info()
|
|
fmt.Printf(" %-25s %s [%s]\n", info.ID, info.Name, strings.Join(info.Tags, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if app.settings.Debug {
|
|
log.SetLevel(log.DebugLevel)
|
|
}
|
|
|
|
if app.settings.ApiMode {
|
|
log.SetLevel(5)
|
|
}
|
|
|
|
if app.settings.LogDir != "" {
|
|
if err := logger.Init(app.settings.LogDir); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := logger.Close(); err != nil {
|
|
log.Errorf("closing logger: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// wire proxy/headers/cookie/rate-limit into the shared http client once,
|
|
// before any scanner runs. a bad proxy/header shouldn't kill the run -
|
|
// scanners fall back to a plain client if this fails.
|
|
if err := httpx.Configure(httpx.Options{
|
|
Proxy: app.settings.Proxy,
|
|
Headers: app.settings.Header,
|
|
Cookie: app.settings.Cookie,
|
|
RateLimit: app.settings.RateLimit,
|
|
Threads: app.settings.Threads,
|
|
}); err != nil {
|
|
log.Warnf("http client config failed, continuing with defaults: %v", err)
|
|
}
|
|
|
|
// target expansion - securitytrails discovers new domains before scanning
|
|
if app.settings.SecurityTrails {
|
|
expanded := app.expandTargets()
|
|
if len(expanded) > 0 {
|
|
output.Info("SecurityTrails discovered %d additional targets", len(expanded))
|
|
app.targets = append(app.targets, expanded...)
|
|
}
|
|
}
|
|
|
|
scansRun := make([]string, 0, 16)
|
|
|
|
// accumulate every module result across targets so the report writers can
|
|
// serialize the full run after the loop. only collected when an export flag
|
|
// is set, so the common path pays nothing.
|
|
wantReport := app.settings.SARIF != "" || app.settings.Markdown != ""
|
|
reportResults := make([]report.Result, 0, 16)
|
|
|
|
for _, url := range app.targets {
|
|
output.Info("Starting scan on %s", output.Highlight.Render(url))
|
|
|
|
moduleResults := make([]ModuleResult, 0, 16)
|
|
|
|
if app.settings.LogDir != "" {
|
|
if err := logger.CreateFile(&app.logFiles, url, app.settings.LogDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !app.settings.NoScan {
|
|
scan.Scan(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
scansRun = append(scansRun, "Basic Scan")
|
|
}
|
|
|
|
if app.settings.Framework {
|
|
result, err := frameworks.DetectFramework(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running framework detection: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Framework Detection")
|
|
}
|
|
}
|
|
|
|
if app.settings.Dirlist != "none" {
|
|
result, err := scan.Dirlist(app.settings.Dirlist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir, scan.DirlistOptions{
|
|
MatchCodes: app.settings.DirMatchCodes,
|
|
FilterCodes: app.settings.DirFilterCodes,
|
|
FilterSizes: app.settings.DirFilterSizes,
|
|
FilterWords: app.settings.DirFilterWords,
|
|
FilterRegex: app.settings.DirFilterRegex,
|
|
Calibrate: app.settings.DirCalibrate,
|
|
Wordlist: app.settings.DirWordlist,
|
|
Extensions: app.settings.DirExtensions,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Error while running directory scan: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Directory Listing")
|
|
}
|
|
}
|
|
|
|
var dnsResults []string
|
|
|
|
if app.settings.Dnslist != "none" {
|
|
result, err := scan.Dnslist(app.settings.Dnslist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running dns scan: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"dnslist", result})
|
|
dnsResults = result // Store the DNS results
|
|
scansRun = append(scansRun, "DNS Scan")
|
|
}
|
|
|
|
// Only run subdomain takeover check if DNS scan is enabled
|
|
if app.settings.SubdomainTakeover {
|
|
result, err := scan.SubdomainTakeover(url, dnsResults, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Subdomain Takeover Vulnerability Check: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"subdomain_takeover", result})
|
|
scansRun = append(scansRun, "Subdomain Takeover")
|
|
}
|
|
}
|
|
} else if app.settings.SubdomainTakeover {
|
|
log.Warnf("Subdomain Takeover check is enabled but DNS scan is disabled. Skipping Subdomain Takeover check.")
|
|
}
|
|
|
|
if app.settings.Dorking {
|
|
result, err := scan.Dork(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Dork module: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"dork", result})
|
|
scansRun = append(scansRun, "Dork")
|
|
}
|
|
}
|
|
|
|
if app.settings.Ports != "none" {
|
|
result, err := scan.Ports(context.Background(), app.settings.Ports, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running port scan: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"portscan", result})
|
|
scansRun = append(scansRun, "Port Scan")
|
|
}
|
|
}
|
|
|
|
if app.settings.Whois {
|
|
scan.Whois(url, app.settings.LogDir)
|
|
scansRun = append(scansRun, "Whois")
|
|
}
|
|
|
|
if app.settings.Git {
|
|
result, err := scan.Git(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Git module: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"git", result})
|
|
scansRun = append(scansRun, "Git")
|
|
}
|
|
}
|
|
|
|
if app.settings.Nuclei {
|
|
result, err := scan.Nuclei(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Nuclei module: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"nuclei", result})
|
|
scansRun = append(scansRun, "Nuclei")
|
|
}
|
|
}
|
|
|
|
if app.settings.JavaScript {
|
|
result, err := jsscan.JavascriptScan(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running JS module: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "JS")
|
|
}
|
|
}
|
|
|
|
if app.settings.CMS {
|
|
result, err := scan.CMS(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running CMS detection: %s", err)
|
|
scansRun = append(scansRun, "CMS")
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
}
|
|
}
|
|
|
|
if app.settings.Headers {
|
|
result, err := scan.Headers(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running HTTP Header Analysis: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"headers", result})
|
|
scansRun = append(scansRun, "HTTP Headers")
|
|
}
|
|
}
|
|
|
|
if app.settings.SecurityHeaders {
|
|
result, err := scan.SecurityHeaders(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Security Header Analysis: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Security Headers")
|
|
}
|
|
}
|
|
|
|
if app.settings.CloudStorage {
|
|
result, err := scan.CloudStorage(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running C3 Scan: %s", err)
|
|
} else {
|
|
moduleResults = append(moduleResults, ModuleResult{"cloudstorage", result})
|
|
scansRun = append(scansRun, "Cloud Storage")
|
|
}
|
|
}
|
|
|
|
if app.settings.Shodan {
|
|
result, err := scan.Shodan(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running Shodan lookup: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Shodan")
|
|
}
|
|
}
|
|
|
|
if app.settings.SQL {
|
|
result, err := scan.SQL(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running SQL reconnaissance: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "SQL Recon")
|
|
}
|
|
}
|
|
|
|
if app.settings.LFI {
|
|
result, err := scan.LFI(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running LFI reconnaissance: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "LFI Recon")
|
|
}
|
|
}
|
|
|
|
if app.settings.CORS {
|
|
result, err := scan.CORS(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running CORS probe: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "CORS")
|
|
}
|
|
}
|
|
|
|
if app.settings.Redirect {
|
|
result, err := scan.Redirect(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running open redirect probe: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Open Redirect")
|
|
}
|
|
}
|
|
|
|
if app.settings.XSS {
|
|
result, err := scan.XSS(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running reflected XSS probe: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Reflected XSS")
|
|
}
|
|
}
|
|
|
|
if app.settings.Crawl {
|
|
result, err := scan.Crawl(url, app.settings.CrawlDepth, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running web crawl: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Crawl")
|
|
}
|
|
}
|
|
|
|
if app.settings.Passive {
|
|
result, err := scan.Passive(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running passive discovery: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Passive")
|
|
}
|
|
}
|
|
|
|
if app.settings.Probe {
|
|
result, err := scan.Probe(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("Error while running probe: %s", err)
|
|
} else if result != nil {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
scansRun = append(scansRun, "Probe")
|
|
}
|
|
}
|
|
|
|
// Load and run modules
|
|
if app.settings.AllModules || app.settings.Modules != "" || app.settings.ModuleTags != "" {
|
|
loader, err := modules.NewLoader()
|
|
if err != nil {
|
|
log.Warnf("Failed to create module loader: %v", err)
|
|
} else {
|
|
if err := loader.LoadAll(); err != nil {
|
|
log.Warnf("Failed to load modules: %v", err)
|
|
}
|
|
|
|
// Register built-in Go modules
|
|
builtin.Register()
|
|
|
|
// Determine which modules to run
|
|
var toRun []modules.Module
|
|
switch {
|
|
case app.settings.AllModules:
|
|
toRun = modules.All()
|
|
case app.settings.ModuleTags != "":
|
|
for _, tag := range strings.Split(app.settings.ModuleTags, ",") {
|
|
toRun = append(toRun, modules.ByTag(strings.TrimSpace(tag))...)
|
|
}
|
|
case app.settings.Modules != "":
|
|
for _, id := range strings.Split(app.settings.Modules, ",") {
|
|
if m, ok := modules.Get(strings.TrimSpace(id)); ok {
|
|
toRun = append(toRun, m)
|
|
} else {
|
|
log.Warnf("Module not found: %s", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Execute modules
|
|
opts := modules.Options{
|
|
Timeout: app.settings.Timeout,
|
|
Threads: app.settings.Threads,
|
|
LogDir: app.settings.LogDir,
|
|
}
|
|
|
|
for _, m := range toRun {
|
|
modLog := output.Module(m.Info().ID)
|
|
modLog.Start()
|
|
result, err := m.Execute(context.Background(), url, opts)
|
|
if err != nil {
|
|
modLog.Error("failed: %v", err)
|
|
continue
|
|
}
|
|
if result != nil && len(result.Findings) > 0 {
|
|
moduleResults = append(moduleResults, NewModuleResult(result))
|
|
modLog.Complete(len(result.Findings), "findings")
|
|
} else {
|
|
modLog.Complete(0, "findings")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if app.settings.ApiMode {
|
|
result := UrlResult{
|
|
Url: url,
|
|
Results: moduleResults,
|
|
}
|
|
|
|
marshalled, err := json.Marshal(result)
|
|
if err != nil {
|
|
log.Errorf("failed to marshal result: %s", err)
|
|
continue
|
|
}
|
|
fmt.Println(string(marshalled))
|
|
}
|
|
|
|
if wantReport {
|
|
reportResults = append(reportResults, collectReportResults(url, moduleResults)...)
|
|
}
|
|
}
|
|
|
|
if wantReport {
|
|
if err := app.writeReports(reportResults); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !app.settings.ApiMode {
|
|
output.PrintSummary(scansRun, app.logFiles)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// collectReportResults flattens one target's module results into the report
|
|
// model, carrying each finding as raw json so the report package stays free of
|
|
// scan types. a result that won't marshal is skipped rather than failing the run.
|
|
func collectReportResults(target string, moduleResults []ModuleResult) []report.Result {
|
|
out := make([]report.Result, 0, len(moduleResults))
|
|
for _, mr := range moduleResults {
|
|
data, err := json.Marshal(mr.Data)
|
|
if err != nil {
|
|
log.Warnf("report: skipping %s result for %s: %v", mr.Id, target, err)
|
|
continue
|
|
}
|
|
out = append(out, report.Result{Target: target, Module: mr.Id, Data: data})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// writeReports serializes the collected results to the requested export files.
|
|
// each writer runs independently so a bad path for one format doesn't suppress
|
|
// the other.
|
|
func (app *App) writeReports(results []report.Result) error {
|
|
if path := app.settings.SARIF; path != "" {
|
|
data, err := report.SARIF(results)
|
|
if err != nil {
|
|
return fmt.Errorf("build sarif report: %w", err)
|
|
}
|
|
if err := os.WriteFile(path, data, reportFileMode); err != nil {
|
|
return fmt.Errorf("write sarif report %q: %w", path, err)
|
|
}
|
|
output.Success("sarif report written to %s", path)
|
|
}
|
|
|
|
if path := app.settings.Markdown; path != "" {
|
|
data := report.Markdown(results)
|
|
if err := os.WriteFile(path, data, reportFileMode); err != nil {
|
|
return fmt.Errorf("write markdown report %q: %w", path, err)
|
|
}
|
|
output.Success("markdown report written to %s", path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// expandTargets queries SecurityTrails for each original target and returns
|
|
// newly discovered domains (subdomains + associated) for target expansion
|
|
func (app *App) expandTargets() []string {
|
|
seen := make(map[string]struct{})
|
|
for _, t := range app.targets {
|
|
seen[t] = struct{}{}
|
|
}
|
|
|
|
// snapshot original targets - don't expand discovered ones
|
|
originals := make([]string, len(app.targets))
|
|
copy(originals, app.targets)
|
|
|
|
var expanded []string
|
|
|
|
for _, url := range originals {
|
|
result, err := scan.SecurityTrails(url, app.settings.Timeout, app.settings.LogDir)
|
|
if err != nil {
|
|
log.Errorf("SecurityTrails error for %s: %v", url, err)
|
|
continue
|
|
}
|
|
if result == nil {
|
|
continue
|
|
}
|
|
|
|
for _, d := range result.DiscoveredURLs() {
|
|
if _, exists := seen[d]; !exists {
|
|
seen[d] = struct{}{}
|
|
expanded = append(expanded, d)
|
|
}
|
|
}
|
|
}
|
|
|
|
return expanded
|
|
}
|