Files
sif/sif.go
T
vmfunc dbe79c495e feat(scan): add web crawler and passive subdomain/url discovery
-crawl spiders same-host links/scripts/forms through the shared httpx
client so proxy/headers/rate-limit and robots.txt are honored, bounded
by -crawl-depth. -passive pulls subdomains from keyless ct feeds (crt.sh,
certspotter) and historical urls from wayback, each source isolated so
one feed being down doesn't sink the rest and the target sees no traffic.
2026-06-09 18:11:38 -07:00

557 lines
17 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/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"
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,
}); 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)
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)
if err != nil {
log.Errorf("Error while running directory scan: %s", err)
} else {
moduleResults = append(moduleResults, ModuleResult{"dirlist", 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")
}
}
// 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 !app.settings.ApiMode {
output.PrintSummary(scansRun, app.logFiles)
}
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
}