/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2025 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/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 } 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 ▄█ █ █▀ ")) fmt.Println(output.Subheading.Render("\nblazing-fast pentesting suite\n\nbsd 3-clause · (c) 2022-2025 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 logger.Close() } // 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(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.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") } } // 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 }