feat(output): add styled console output with module loggers

- Add output package with colored prefixes and module loggers
- Each module gets unique background color based on name hash
- Add spinner for indeterminate operations
- Add progress bar for known-count operations
- Update all scan files to use ModuleLogger pattern
- Add clean PrintSummary for scan completion
This commit is contained in:
vmfunc
2026-01-03 05:51:26 -08:00
parent ab17191c31
commit 00a66adf27
22 changed files with 880 additions and 396 deletions

2
go.mod
View File

@@ -14,6 +14,7 @@ require (
github.com/projectdiscovery/ratelimit v0.0.9
github.com/projectdiscovery/utils v0.1.1
github.com/rocketlaunchr/google-search v1.1.6
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -220,6 +221,5 @@ require (
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)

View File

@@ -19,6 +19,7 @@ import (
"runtime"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/output"
)
// Loader handles module discovery and loading.
@@ -80,7 +81,10 @@ func (l *Loader) LoadAll() error {
}
}
log.Infof("📦 Loaded %d modules", l.loaded)
if l.loaded > 0 {
modLog := output.Module("MODULES")
modLog.Info("Loaded %d modules", l.loaded)
}
return nil
}

283
internal/output/output.go Normal file
View File

@@ -0,0 +1,283 @@
package output
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Clean, subtle color palette
var (
ColorGreen = lipgloss.Color("#22c55e") // success green
ColorBlue = lipgloss.Color("#3b82f6") // info blue
ColorYellow = lipgloss.Color("#eab308") // warning yellow
ColorRed = lipgloss.Color("#ef4444") // error red
ColorGray = lipgloss.Color("#6b7280") // muted gray
ColorWhite = lipgloss.Color("#f3f4f6") // bright text
)
// Prefix styles
var (
prefixInfo = lipgloss.NewStyle().Foreground(ColorBlue).Bold(true)
prefixSuccess = lipgloss.NewStyle().Foreground(ColorGreen).Bold(true)
prefixWarning = lipgloss.NewStyle().Foreground(ColorYellow).Bold(true)
prefixError = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
)
// Text styles
var (
Highlight = lipgloss.NewStyle().Bold(true).Foreground(ColorWhite)
Muted = lipgloss.NewStyle().Foreground(ColorGray)
Status = lipgloss.NewStyle().Bold(true).Foreground(ColorGreen)
)
// Box style for banners
var Box = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(ColorGray).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
// Subheading style
var Subheading = lipgloss.NewStyle().
Foreground(ColorGray).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
// Severity styles
var (
SeverityLow = lipgloss.NewStyle().Foreground(ColorGreen)
SeverityMedium = lipgloss.NewStyle().Foreground(ColorYellow)
SeverityHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("#f97316")) // orange
SeverityCritical = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
)
// Module color palette - visually distinct, nice colors
var moduleColors = []lipgloss.Color{
lipgloss.Color("#6366f1"), // indigo
lipgloss.Color("#8b5cf6"), // violet
lipgloss.Color("#ec4899"), // pink
lipgloss.Color("#f97316"), // orange
lipgloss.Color("#14b8a6"), // teal
lipgloss.Color("#06b6d4"), // cyan
lipgloss.Color("#84cc16"), // lime
lipgloss.Color("#a855f7"), // purple
lipgloss.Color("#f43f5e"), // rose
lipgloss.Color("#0ea5e9"), // sky
}
// getModuleColor returns a consistent color for a module name
func getModuleColor(name string) lipgloss.Color {
// Simple hash to pick a color
hash := 0
for _, c := range name {
hash = hash*31 + int(c)
}
if hash < 0 {
hash = -hash
}
return moduleColors[hash%len(moduleColors)]
}
// moduleStyleFor returns a styled prefix for a module
func moduleStyleFor(name string) lipgloss.Style {
return lipgloss.NewStyle().
Background(getModuleColor(name)).
Foreground(lipgloss.Color("#ffffff")).
Bold(true).
Padding(0, 1)
}
// IsTTY returns true if stdout is a terminal
var IsTTY = checkTTY()
func checkTTY() bool {
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
return true
}
return false
}
// apiMode disables visual output when true
var apiMode bool
// SetAPIMode enables or disables API mode
func SetAPIMode(enabled bool) {
apiMode = enabled
}
// Info prints an informational message with [*] prefix
func Info(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixInfo.Render("[*]"), msg)
}
// Success prints a success message with [+] prefix
func Success(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixSuccess.Render("[+]"), msg)
}
// Warn prints a warning message with [!] prefix
func Warn(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixWarning.Render("[!]"), msg)
}
// Error prints an error message with [-] prefix
func Error(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixError.Render("[-]"), msg)
}
// ScanStart prints a styled scan start message
func ScanStart(scanName string) {
if apiMode {
return
}
fmt.Printf("%s starting %s\n", prefixInfo.Render("[*]"), scanName)
}
// ScanComplete prints a styled scan completion message
func ScanComplete(scanName string, resultCount int, resultType string) {
if apiMode {
return
}
fmt.Printf("%s %s complete (%d %s)\n", prefixInfo.Render("[*]"), scanName, resultCount, resultType)
}
// Module creates a prefixed logger for a specific module/tool
func Module(name string) *ModuleLogger {
return &ModuleLogger{
name: name,
style: moduleStyleFor(name),
}
}
// ModuleLogger provides prefixed logging for a specific module
type ModuleLogger struct {
name string
style lipgloss.Style
}
func (m *ModuleLogger) prefix() string {
return m.style.Render(m.name)
}
// Info prints an info message with module prefix
func (m *ModuleLogger) Info(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", m.prefix(), msg)
}
// Success prints a success message with module prefix
func (m *ModuleLogger) Success(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
}
// Warn prints a warning message with module prefix
func (m *ModuleLogger) Warn(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
}
// Error prints an error message with module prefix
func (m *ModuleLogger) Error(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
}
// Start prints a scan start message with module prefix (adds newline before for separation)
func (m *ModuleLogger) Start() {
if apiMode {
return
}
fmt.Printf("\n%s starting scan\n", m.prefix())
}
// Complete prints a scan complete message with module prefix
func (m *ModuleLogger) Complete(resultCount int, resultType string) {
if apiMode {
return
}
fmt.Printf("%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
}
// ClearLine clears the current line (for progress bar updates)
func ClearLine() {
if !IsTTY {
return
}
fmt.Print("\033[2K\r")
}
// Summary styles
var (
summaryHeader = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
Background(lipgloss.Color("#22c55e")).
Padding(0, 2)
summaryLine = lipgloss.NewStyle().
Foreground(ColorGray)
)
// PrintSummary prints a clean scan completion summary
func PrintSummary(scans []string, logFiles []string) {
if apiMode {
return
}
fmt.Println()
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
fmt.Println()
fmt.Printf(" %s\n", summaryHeader.Render("SCAN COMPLETE"))
fmt.Println()
// Print scans
scanList := strings.Join(scans, ", ")
fmt.Printf(" %s %s\n", Muted.Render("Scans:"), scanList)
// Print log files if any
if len(logFiles) > 0 {
fmt.Printf(" %s %s\n", Muted.Render("Output:"), strings.Join(logFiles, ", "))
}
fmt.Println()
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
fmt.Println()
}

155
internal/output/progress.go Normal file
View File

@@ -0,0 +1,155 @@
package output
import (
"fmt"
"sync"
"sync/atomic"
)
// Progress bar configuration
const (
progressWidth = 30
progressFilled = "="
progressCurrent = ">"
progressEmpty = " "
)
// Progress displays a progress bar for operations with known counts
type Progress struct {
total int64
current int64
message string
lastItem string
mu sync.Mutex
paused bool
}
// NewProgress creates a new progress bar
func NewProgress(total int, message string) *Progress {
return &Progress{
total: int64(total),
message: message,
}
}
// Increment advances the progress by 1 and optionally updates the current item
func (p *Progress) Increment(item string) {
atomic.AddInt64(&p.current, 1)
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Set sets the progress to a specific value
func (p *Progress) Set(current int, item string) {
atomic.StoreInt64(&p.current, int64(current))
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Pause temporarily stops rendering (use before printing other output)
func (p *Progress) Pause() {
p.mu.Lock()
p.paused = true
p.mu.Unlock()
ClearLine()
}
// Resume resumes rendering after a pause
func (p *Progress) Resume() {
p.mu.Lock()
p.paused = false
p.mu.Unlock()
p.render()
}
// Done clears the progress bar line
func (p *Progress) Done() {
if apiMode || !IsTTY {
return
}
ClearLine()
}
func (p *Progress) render() {
if apiMode {
return
}
// In non-TTY mode, print progress at milestones only
if !IsTTY {
current := atomic.LoadInt64(&p.current)
total := p.total
percent := int(current * 100 / total)
// Print at 0%, 25%, 50%, 75%, 100%
if current == 1 || percent == 25 || percent == 50 || percent == 75 || current == total {
fmt.Printf(" [%d%%] %d/%d\n", percent, current, total)
}
return
}
current := atomic.LoadInt64(&p.current)
total := p.total
p.mu.Lock()
lastItem := p.lastItem
p.mu.Unlock()
// Calculate percentage
percent := 0
if total > 0 {
percent = int(current * 100 / total)
}
// Build progress bar
filled := 0
if total > 0 {
filled = int(progressWidth * current / total)
}
if filled > progressWidth {
filled = progressWidth
}
bar := ""
for i := 0; i < progressWidth; i++ {
if i < filled {
bar += progressFilled
} else if i == filled && current < total {
bar += progressCurrent
} else {
bar += progressEmpty
}
}
// Truncate item if too long
maxItemLen := 30
if len(lastItem) > maxItemLen {
lastItem = lastItem[:maxItemLen-3] + "..."
}
// Format: [========> ] 45% (4500/10000) /admin
line := fmt.Sprintf(" [%s] %3d%% (%d/%d) %s",
prefixInfo.Render(bar),
percent,
current,
total,
Muted.Render(lastItem),
)
ClearLine()
fmt.Print(line)
}

109
internal/output/spinner.go Normal file
View File

@@ -0,0 +1,109 @@
package output
import (
"fmt"
"os"
"sync"
"time"
)
// Spinner frames using simple ASCII
var spinnerFrames = []string{"|", "/", "-", "\\"}
// Spinner displays an animated spinner for indeterminate operations
type Spinner struct {
message string
running bool
done chan struct{}
mu sync.Mutex
interval time.Duration
}
// NewSpinner creates a new spinner with the given message
func NewSpinner(message string) *Spinner {
return &Spinner{
message: message,
interval: 100 * time.Millisecond,
done: make(chan struct{}),
}
}
// Start begins the spinner animation
func (s *Spinner) Start() {
if apiMode {
return
}
s.mu.Lock()
if s.running {
s.mu.Unlock()
return
}
s.running = true
s.done = make(chan struct{})
s.mu.Unlock()
// In non-TTY mode, just print the message once
if !IsTTY {
fmt.Printf(" %s...\n", s.message)
return
}
go s.animate()
}
// Stop halts the spinner and clears the line
func (s *Spinner) Stop() {
if apiMode {
return
}
s.mu.Lock()
if !s.running {
s.mu.Unlock()
return
}
s.running = false
close(s.done)
s.mu.Unlock()
// Give animation goroutine time to exit
time.Sleep(s.interval)
// Clear the spinner line
if IsTTY {
ClearLine()
}
}
// Update changes the spinner message while running
func (s *Spinner) Update(message string) {
s.mu.Lock()
s.message = message
s.mu.Unlock()
}
func (s *Spinner) animate() {
frame := 0
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-s.done:
return
case <-ticker.C:
s.mu.Lock()
msg := s.message
s.mu.Unlock()
spinnerChar := prefixInfo.Render(spinnerFrames[frame])
line := fmt.Sprintf("\r %s %s", spinnerChar, msg)
fmt.Fprint(os.Stdout, "\033[2K") // Clear line
fmt.Fprint(os.Stdout, line)
frame = (frame + 1) % len(spinnerFrames)
}
}
}

View File

@@ -13,16 +13,13 @@
package scan
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
type CMSResult struct {
@@ -31,59 +28,70 @@ type CMSResult struct {
}
func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("CMS detection") + "..."))
log := output.Module("CMS")
log.Start()
spin := output.NewSpinner("Detecting content management system")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
cmslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "CMS 🔍",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
spin.Stop()
return nil, err
}
bodyString := string(body)
// WordPress
if detectWordPress(url, client, bodyString) {
spin.Stop()
result := &CMSResult{Name: "WordPress", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
// Drupal
if strings.Contains(resp.Header.Get("X-Drupal-Cache"), "HIT") || strings.Contains(bodyString, "Drupal.settings") {
spin.Stop()
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
// Joomla
if strings.Contains(bodyString, "joomla") || strings.Contains(bodyString, "/media/system/js/core.js") {
spin.Stop()
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
cmslog.Info("No CMS detected")
spin.Stop()
log.Info("No CMS detected")
log.Complete(0, "detected")
return nil, nil
}

View File

@@ -1,30 +1,17 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"bufio"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
const (
@@ -40,36 +27,20 @@ type DirectoryResult struct {
}
// Dirlist performs directory fuzzing on the target URL.
//
// Parameters:
// - size: determines the size of the directory list to use ("small", "medium", or "large")
// - url: the target URL to scan
// - timeout: maximum duration for each request
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
//
// Returns:
// - []DirectoryResult: a slice of discovered directories and their status codes
// - error: any error encountered during the scan
func Dirlist(size string, url string, timeout time.Duration, threads int, logdir string) ([]DirectoryResult, error) {
fmt.Println(styles.Separator.Render("📂 Starting " + styles.Status.Render("directory fuzzing") + "..."))
log := output.Module("DIRLIST")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
dirlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dirlist 📂",
}).With("url", url)
var list string
switch size {
case "small":
list = directoryURL + smallFile
@@ -79,14 +50,13 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
list = directoryURL + bigFile
}
dirlog.Infof("Starting %s directory listing", size)
resp, err := http.Get(list)
if err != nil {
log.Errorf("Error downloading directory list: %s", err)
log.Error("Error downloading directory list: %s", err)
return nil, err
}
defer resp.Body.Close()
var directories []string
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -98,6 +68,8 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(directories), "fuzzing")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
@@ -112,15 +84,20 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("%s", directory)
progress.Increment(directory)
charmlog.Debugf("%s", directory)
resp, err := client.Get(url + "/" + directory)
if err != nil {
log.Debugf("Error %s: %s", directory, err)
return
charmlog.Debugf("Error %s: %s", directory, err)
continue
}
if resp.StatusCode != 404 && resp.StatusCode != 403 {
dirlog.Infof("%s [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(directory))
progress.Pause()
log.Success("found: %s [%s]", output.Highlight.Render(directory), output.Status.Render(strconv.Itoa(resp.StatusCode)))
progress.Resume()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
}
@@ -137,6 +114,9 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
}(thread)
}
wg.Wait()
progress.Done()
log.Complete(len(results), "found")
return results, nil
}

View File

@@ -1,29 +1,16 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"bufio"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
const (
@@ -34,27 +21,11 @@ const (
)
// Dnslist performs DNS subdomain enumeration on the target domain.
//
// Parameters:
// - size: determines the size of the subdomain list to use ("small", "medium", or "large")
// - url: the target URL to scan
// - timeout: maximum duration for each DNS lookup
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
//
// Returns:
// - []string: a slice of discovered subdomains
// - error: any error encountered during the enumeration
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
fmt.Println(styles.Separator.Render("📡 Starting " + styles.Status.Render("DNS fuzzing") + "..."))
dnslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dnslist 📡",
}).With("url", url)
log := output.Module("DNS")
log.Start()
var list string
switch size {
case "small":
list = dnsURL + dnsSmallFile
@@ -64,14 +35,13 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
list = dnsURL + dnsBigFile
}
dnslog.Infof("Starting %s DNS listing", size)
resp, err := http.Get(list)
if err != nil {
log.Errorf("Error downloading DNS list: %s", err)
log.Error("Error downloading DNS list: %s", err)
return nil, err
}
defer resp.Body.Close()
var dns []string
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -83,7 +53,7 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
@@ -92,6 +62,8 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(dns), "enumerating")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
@@ -106,29 +78,41 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("Looking up: %s", domain)
progress.Increment(domain)
charmlog.Debugf("Looking up: %s", domain)
// Check HTTP
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
if err != nil {
log.Debugf("Error %s: %s", domain, err)
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
dnslog.Infof("%s %s.%s", styles.Status.Render("[http]"), styles.Highlight.Render(domain), sanitizedURL)
progress.Pause()
log.Success("found: %s.%s [http]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
}
}
// Check HTTPS
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
if err != nil {
log.Debugf("Error %s: %s", domain, err)
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
dnslog.Infof("%s %s.%s", styles.Status.Render("[https]"), styles.Highlight.Render(domain), sanitizedURL)
progress.Pause()
log.Success("found: %s.%s [https]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
}
@@ -137,6 +121,9 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
}(thread)
}
wg.Wait()
progress.Done()
log.Complete(len(urls), "found")
return urls, nil
}

View File

@@ -19,7 +19,6 @@ import (
"bufio"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
@@ -27,7 +26,7 @@ import (
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
googlesearch "github.com/rocketlaunchr/google-search"
)
@@ -55,27 +54,25 @@ type DorkResult struct {
// - []DorkResult: A slice of results from the dorking operation
// - error: Any error encountered during the dorking process
func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) {
output.ScanStart("URL dorking")
fmt.Println(styles.Separator.Render("🤓 Starting " + styles.Status.Render("URL Dorking") + "..."))
spin := output.NewSpinner("Running Google dorks")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
output.Error("Error creating log file: %v", err)
return nil, err
}
}
dorklog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dorking 🤓",
}).With("url", url)
dorklog.Infof("Starting URL dorking...")
resp, err := http.Get(dorkURL + dorkFile)
if err != nil {
log.Errorf("Error downloading dork list: %s", err)
spin.Stop()
output.Error("Error downloading dork list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -103,13 +100,15 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
if err != nil {
dorklog.Debugf("error searching for dork %s: %v", dork, err)
log.Debugf("error searching for dork %s: %v", dork, err)
continue
}
if len(results) > 0 {
dorklog.Infof("%s dork results found for dork [%s]", styles.Status.Render(strconv.Itoa(len(results))), styles.Highlight.Render(dork))
spin.Stop()
output.Success("%s dork results found for dork %s", output.Status.Render(strconv.Itoa(len(results))), output.Highlight.Render(dork))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s dork results found for dork [%s]\n", strconv.Itoa(len(results)), dork))
logger.Write(sanitizedURL, logdir, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
}
result := DorkResult{
@@ -123,6 +122,8 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
}(thread)
}
wg.Wait()
spin.Stop()
output.ScanComplete("URL dorking", len(dorkResults), "found")
return dorkResults, nil
}

View File

@@ -1,28 +1,15 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
// detectionThreshold is the minimum confidence for a detection to be reported.
@@ -40,22 +27,24 @@ type detectionResult struct {
// DetectFramework runs all registered detectors against the target URL.
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Framework Detection") + "..."))
log := output.Module("FRAMEWORK")
log.Start()
frameworklog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Framework Detection 🔍",
}).With("url", url)
spin := output.NewSpinner("Detecting frameworks")
spin.Start()
client := &http.Client{Timeout: timeout}
resp, err := client.Get(url)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
if err != nil {
spin.Stop()
return nil, err
}
bodyStr := string(body)
@@ -63,7 +52,8 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
// Get all registered detectors
detectors := GetDetectors()
if len(detectors) == 0 {
frameworklog.Warn("No framework detectors registered")
spin.Stop()
log.Warn("No framework detectors registered")
return nil, nil
}
@@ -98,8 +88,11 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
}
}
spin.Stop()
if best.confidence <= detectionThreshold {
frameworklog.Info("No framework detected with sufficient confidence")
log.Info("No framework detected with sufficient confidence")
log.Complete(0, "detected")
return nil, nil
}
@@ -122,24 +115,26 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
logger.Write(url, logdir, logEntry)
}
frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)",
styles.Highlight.Render(best.name), best.version, best.confidence)
log.Success("Detected %s framework (version: %s, confidence: %.2f)",
output.Highlight.Render(best.name), best.version, best.confidence)
if versionMatch.Confidence > 0 {
frameworklog.Debugf("Version detected from: %s (confidence: %.2f)",
charmlog.Debugf("Version detected from: %s (confidence: %.2f)",
versionMatch.Source, versionMatch.Confidence)
}
if len(cves) > 0 {
frameworklog.Warnf("Risk level: %s", styles.SeverityHigh.Render(result.RiskLevel))
log.Warn("Risk level: %s", output.SeverityHigh.Render(result.RiskLevel))
for _, cve := range cves {
frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve))
log.Warn("Found potential vulnerability: %s", output.Highlight.Render(cve))
}
for _, suggestion := range suggestions {
frameworklog.Infof("Recommendation: %s", suggestion)
log.Info("Recommendation: %s", suggestion)
}
}
log.Complete(1, "detected")
return result, nil
}

View File

@@ -14,17 +14,15 @@ package scan
import (
"bufio"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
const (
@@ -33,27 +31,26 @@ const (
)
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log := output.Module("GIT")
log.Start()
fmt.Println(styles.Separator.Render("🌿 Starting " + styles.Status.Render("git repository scanning") + "..."))
spin := output.NewSpinner("Scanning for exposed git repositories")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
gitlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Git 🌿",
}).With("url", url)
gitlog.Infof("Starting repository scanning")
resp, err := http.Get(gitURL + gitFile)
if err != nil {
log.Errorf("Error downloading git list: %s", err)
spin.Stop()
log.Error("Error downloading git list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -82,17 +79,18 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
continue
}
log.Debugf("%s", repourl)
charmlog.Debugf("%s", repourl)
resp, err := client.Get(url + "/" + repourl)
if err != nil {
log.Debugf("Error %s: %s", repourl, err)
charmlog.Debugf("Error %s: %s", repourl, err)
}
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
// log url, directory, and status code
gitlog.Infof("%s git found at [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(repourl))
spin.Stop()
log.Success("Git found at %s [%s]", output.Highlight.Render(repourl), output.Status.Render(strconv.Itoa(resp.StatusCode)))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s git found at [%s]\n", strconv.Itoa(resp.StatusCode), repourl))
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" git found at ["+repourl+"]\n")
}
foundUrls = append(foundUrls, resp.Request.URL.String())
@@ -102,5 +100,8 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
}
wg.Wait()
spin.Stop()
log.Complete(len(foundUrls), "found")
return foundUrls, nil
}

View File

@@ -13,15 +13,12 @@
package scan
import (
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
type HeaderResult struct {
@@ -30,21 +27,18 @@ type HeaderResult struct {
}
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("HTTP Header Analysis") + "..."))
log := output.Module("HEADERS")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
headerlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Headers 🔍",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
}
@@ -60,12 +54,13 @@ func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult,
for name, values := range resp.Header {
for _, value := range values {
results = append(results, HeaderResult{Name: name, Value: value})
headerlog.Infof("%s: %s", styles.Highlight.Render(name), value)
log.Info("%s: %s", output.Highlight.Render(name), value)
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s: %s\n", name, value))
logger.Write(sanitizedURL, logdir, name+": "+value+"\n")
}
}
}
log.Complete(len(results), "found")
return results, nil
}

View File

@@ -16,13 +16,13 @@ import (
"bufio"
"io"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/antchfx/htmlquery"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/output"
"github.com/dropalldatabases/sif/internal/scan/js/frameworks"
urlutil "github.com/projectdiscovery/utils/url"
)
@@ -36,16 +36,20 @@ type JavascriptScanResult struct {
func (r *JavascriptScanResult) ResultType() string { return "js" }
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
jslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "🚧 JavaScript",
}).With("url", url)
log := output.Module("JS")
log.Start()
spin := output.NewSpinner("Scanning JavaScript files")
spin.Start()
baseUrl, err := urlutil.Parse(url)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := http.Get(url)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
@@ -84,9 +88,10 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
for _, script := range scripts {
if strings.Contains(script, "/_buildManifest.js") {
jslog.Infof("Detected Next.JS pages router! Getting all scripts from %s", script)
log.Info("Detected Next.JS pages router! Getting all scripts from %s", script)
nextScripts, err := frameworks.GetPagesRouterScripts(script)
if err != nil {
spin.Stop()
return nil, err
}
@@ -99,30 +104,30 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
}
}
jslog.Infof("Got %d scripts, now running scans on them", len(scripts))
log.Info("Got %d scripts, now running scans on them", len(scripts))
supabaseResults := make([]supabaseScanResult, 0, len(scripts))
for _, script := range scripts {
jslog.Infof("Scanning %s", script)
charmlog.Debugf("Scanning %s", script)
resp, err := http.Get(script)
if err != nil {
jslog.Warnf("Failed to fetch script: %s", err)
charmlog.Warnf("Failed to fetch script: %s", err)
continue
}
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
resp.Body.Close()
if err != nil {
jslog.Errorf("Failed to read script body: %s", err)
charmlog.Errorf("Failed to read script body: %s", err)
continue
}
content := string(bodyBytes)
jslog.Infof("Running supabase scanner on %s", script)
charmlog.Debugf("Running supabase scanner on %s", script)
scriptSupabaseResults, err := ScanSupabase(content, script)
if err != nil {
jslog.Errorf("Error while scanning supabase: %s", err)
charmlog.Errorf("Error while scanning supabase: %s", err)
}
if scriptSupabaseResults != nil {
@@ -130,10 +135,14 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
}
}
spin.Stop()
result := JavascriptScanResult{
SupabaseResults: supabaseResults,
FoundEnvironmentVars: map[string]string{},
}
log.Complete(len(supabaseResults), "found")
return &result, nil
}

View File

@@ -17,15 +17,14 @@ import (
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
// LFIResult represents the results of LFI reconnaissance
@@ -113,23 +112,22 @@ var commonLFIParams = []string{
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
log := output.Module("LFI")
log.Start()
spin := output.NewSpinner("Scanning for LFI vulnerabilities")
spin.Start()
sanitizedURL := strings.Split(targetURL, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
lfilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "LFI 📁",
}).With("url", targetURL)
lfilog.Infof("Starting LFI reconnaissance...")
result := &LFIResult{
Vulnerabilities: make([]LFIVulnerability, 0, 16),
}
@@ -170,7 +168,7 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
result.TestedParams = len(paramsToTest)
result.TestedPayloads = len(lfiPayloads)
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
log.Info("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
// create work items
type workItem struct {
@@ -218,7 +216,7 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
resp, err := client.Get(testURL)
if err != nil {
log.Debugf("Error testing %s: %v", testURL, err)
charmlog.Debugf("Error testing %s: %v", testURL, err)
continue
}
@@ -252,10 +250,12 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
mu.Unlock()
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
styles.SeverityHigh.Render(evidence.description),
styles.Highlight.Render(item.param),
styles.Status.Render(item.payload.target))
spin.Stop()
log.Warn("LFI vulnerability found: %s in param %s - %s",
output.SeverityHigh.Render(evidence.description),
output.Highlight.Render(item.param),
output.Status.Render(item.payload.target))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir,
@@ -270,9 +270,11 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
}
wg.Wait()
spin.Stop()
// summary
if len(result.Vulnerabilities) > 0 {
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
log.Warn("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
criticalCount := 0
highCount := 0
for _, v := range result.Vulnerabilities {
@@ -283,13 +285,15 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
}
}
if criticalCount > 0 {
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
log.Error("%d CRITICAL vulnerabilities found!", criticalCount)
}
if highCount > 0 {
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
log.Warn("%d HIGH severity vulnerabilities found", highCount)
}
log.Complete(len(result.Vulnerabilities), "found")
} else {
lfilog.Infof("No LFI vulnerabilities detected")
log.Info("No LFI vulnerabilities detected")
log.Complete(0, "found")
return nil, nil
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/nuclei/format"
"github.com/dropalldatabases/sif/internal/nuclei/templates"
"github.com/dropalldatabases/sif/internal/styles"
sifoutput "github.com/dropalldatabases/sif/internal/output"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
@@ -43,12 +43,15 @@ import (
)
func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]output.ResultEvent, error) {
fmt.Println(styles.Separator.Render("⚛️ Starting " + styles.Status.Render("nuclei template scanning") + "..."))
sifoutput.ScanStart("nuclei template scanning")
spin := sifoutput.NewSpinner("Running nuclei templates")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
nucleilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "nuclei ⚛️",
Prefix: "nuclei",
}).With("url", url)
// Apply threads, timeout, log settings
@@ -128,5 +131,8 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
_ = engine.Execute(store.Templates(), input)
engine.WorkPool().Wait()
spin.Stop()
sifoutput.ScanComplete("nuclei template scanning", len(results), "found")
return results, nil
}

View File

@@ -1,15 +1,3 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
@@ -17,42 +5,36 @@ import (
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
const commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
func Ports(scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log.Printf("%s", styles.Separator.Render("🚪 Starting "+styles.Status.Render("port scanning")+"..."))
log := output.Module("PORTS")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
portlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Ports 🚪",
})
portlog.Infof("Starting %s port scanning", scope)
var ports []int
switch scope {
case "common":
resp, err := http.Get(commonPorts)
if err != nil {
log.Errorf("Error downloading ports list: %s", err)
log.Error("Error downloading ports list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -70,9 +52,13 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
}
}
progress := output.NewProgress(len(ports), "scanning")
var openPorts []string
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(threads)
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -82,25 +68,29 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("Looking up: %d", port)
progress.Increment(strconv.Itoa(port))
charmlog.Debugf("Looking up: %d", port)
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
if err != nil {
log.Debugf("Error %d: %v", port, err)
charmlog.Debugf("Error %d: %v", port, err)
} else {
progress.Pause()
log.Success("open: %s:%s [tcp]", sanitizedURL, output.Highlight.Render(strconv.Itoa(port)))
progress.Resume()
mu.Lock()
openPorts = append(openPorts, strconv.Itoa(port))
portlog.Infof("%s %s:%s", styles.Status.Render("[tcp]"), sanitizedURL, styles.Highlight.Render(strconv.Itoa(port)))
mu.Unlock()
tcp.Close()
}
}
}(thread)
}
wg.Wait()
progress.Done()
if len(openPorts) > 0 {
portlog.Infof("Found %d open ports: %s", len(openPorts), strings.Join(openPorts, ", "))
} else {
portlog.Error("Found no open ports")
}
log.Complete(len(openPorts), "open")
return openPorts, nil
}

View File

@@ -19,9 +19,7 @@ package scan
import (
"bufio"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
@@ -29,7 +27,7 @@ import (
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
@@ -61,21 +59,17 @@ func fetchRobotsTXT(url string, client *http.Client) *http.Response {
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
func Scan(url string, timeout time.Duration, threads int, logdir string) {
fmt.Println(styles.Separator.Render("🐾 Starting " + styles.Status.Render("base url scanning") + "..."))
output.ScanStart("base URL scanning")
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
log.Errorf("Error creating log file: %v", err)
output.Error("Error creating log file: %v", err)
return
}
}
scanlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Scan 👁️‍🗨️",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -90,7 +84,7 @@ func Scan(url string, timeout time.Duration, threads int, logdir string) {
defer resp.Body.Close()
if resp.StatusCode != 404 && resp.StatusCode != 301 && resp.StatusCode != 302 && resp.StatusCode != 307 {
scanlog.Infof("file [%s] found", styles.Status.Render("robots.txt"))
output.Success("File %s found", output.Status.Render("robots.txt"))
var robotsData []string
scanner := bufio.NewScanner(resp.Body)
@@ -115,17 +109,17 @@ func Scan(url string, timeout time.Duration, threads int, logdir string) {
}
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
scanlog.Debugf("%s", robot)
log.Debugf("%s", robot)
resp, err := client.Get(url + "/" + sanitizedRobot)
if err != nil {
scanlog.Debugf("Error %s: %s", sanitizedRobot, err)
log.Debugf("Error %s: %s", sanitizedRobot, err)
continue
}
if resp.StatusCode != 404 {
scanlog.Infof("%s from robots: [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(sanitizedRobot))
output.Success("%s from robots: %s", output.Status.Render(strconv.Itoa(resp.StatusCode)), output.Highlight.Render(sanitizedRobot))
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s from robots: [%s]\n", strconv.Itoa(resp.StatusCode), sanitizedRobot))
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" from robots: ["+sanitizedRobot+"]\n")
}
}
resp.Body.Close()

View File

@@ -23,9 +23,8 @@ import (
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
const shodanBaseURL = "https://api.shodan.io"
@@ -93,21 +92,22 @@ type shodanData struct {
// Shodan performs a Shodan lookup for the given URL
// The API key should be provided via the SHODAN_API_KEY environment variable
func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Shodan lookup") + "..."))
output.ScanStart("Shodan lookup")
shodanlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Shodan 🔍",
}).With("url", targetURL)
spin := output.NewSpinner("Querying Shodan API")
spin.Start()
apiKey := os.Getenv("SHODAN_API_KEY")
apiKey := getShodanAPIKey()
if apiKey == "" {
shodanlog.Warn("SHODAN_API_KEY environment variable not set, skipping Shodan lookup")
spin.Stop()
output.Warn("SHODAN_API_KEY environment variable not set, skipping Shodan lookup")
return nil, fmt.Errorf("SHODAN_API_KEY environment variable not set")
}
// extract hostname from URL
parsedURL, err := url.Parse(targetURL)
if err != nil {
spin.Stop()
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
hostname := parsedURL.Hostname()
@@ -115,34 +115,44 @@ func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResu
// resolve hostname to IP
ip, err := resolveHostname(hostname)
if err != nil {
shodanlog.Warnf("Failed to resolve hostname %s: %v", hostname, err)
spin.Stop()
output.Warn("Failed to resolve hostname %s: %v", hostname, err)
return nil, fmt.Errorf("failed to resolve hostname: %w", err)
}
shodanlog.Infof("Resolved %s to %s", hostname, ip)
output.Info("Resolved %s to %s", hostname, ip)
// query Shodan API
result, err := queryShodanHost(ip, apiKey, timeout)
if err != nil {
shodanlog.Warnf("Shodan lookup failed: %v", err)
spin.Stop()
output.Warn("Shodan lookup failed: %v", err)
return nil, err
}
spin.Stop()
// log results
if logdir != "" {
sanitizedURL := strings.Split(targetURL, "://")[1]
if err := logger.WriteHeader(sanitizedURL, logdir, "Shodan lookup"); err != nil {
shodanlog.Errorf("Error writing log header: %v", err)
output.Error("Error writing log header: %v", err)
}
logShodanResults(sanitizedURL, logdir, result)
}
// print results
printShodanResults(shodanlog, result)
printShodanResults(result)
output.ScanComplete("Shodan lookup", 1, "completed")
return result, nil
}
// getShodanAPIKey returns the Shodan API key from environment
func getShodanAPIKey() string {
return os.Getenv("SHODAN_API_KEY")
}
func resolveHostname(hostname string) (string, error) {
// check if already an IP
if net.ParseIP(hostname) != nil {
@@ -248,21 +258,21 @@ func truncateBanner(banner string, maxLen int) string {
return banner
}
func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
func printShodanResults(result *ShodanResult) {
if result.IP != "" {
shodanlog.Infof("IP: %s", styles.Highlight.Render(result.IP))
output.Info("IP: %s", output.Highlight.Render(result.IP))
}
if len(result.Hostnames) > 0 {
shodanlog.Infof("Hostnames: %s", strings.Join(result.Hostnames, ", "))
output.Info("Hostnames: %s", strings.Join(result.Hostnames, ", "))
}
if result.Organization != "" {
shodanlog.Infof("Organization: %s", result.Organization)
output.Info("Organization: %s", result.Organization)
}
if result.ISP != "" {
shodanlog.Infof("ISP: %s", result.ISP)
output.Info("ISP: %s", result.ISP)
}
if result.Country != "" {
@@ -270,11 +280,11 @@ func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
if result.City != "" {
location = result.City + ", " + result.Country
}
shodanlog.Infof("Location: %s", location)
output.Info("Location: %s", location)
}
if result.OS != "" {
shodanlog.Infof("OS: %s", result.OS)
output.Info("OS: %s", result.OS)
}
if len(result.Ports) > 0 {
@@ -282,11 +292,11 @@ func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
for i, port := range result.Ports {
portStrs[i] = fmt.Sprintf("%d", port)
}
shodanlog.Infof("Open Ports: %s", styles.Status.Render(strings.Join(portStrs, ", ")))
output.Info("Open Ports: %s", output.Status.Render(strings.Join(portStrs, ", ")))
}
if len(result.Vulns) > 0 {
shodanlog.Warnf("Vulnerabilities: %s", styles.SeverityHigh.Render(strings.Join(result.Vulns, ", ")))
output.Warn("Vulnerabilities: %s", output.SeverityHigh.Render(strings.Join(result.Vulns, ", ")))
}
for _, service := range result.Services {
@@ -297,10 +307,7 @@ func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
serviceInfo += " " + service.Version
}
}
shodanlog.Infof("Service: %s", serviceInfo)
if service.Banner != "" {
shodanlog.Debugf(" Banner: %s", service.Banner)
}
output.Info("Service: %s", serviceInfo)
}
}

View File

@@ -13,18 +13,17 @@
package scan
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
)
// SQLResult represents the results of SQL reconnaissance
@@ -115,23 +114,22 @@ var databaseErrorPatterns = []struct {
// SQL performs SQL reconnaissance on the target URL
func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*SQLResult, error) {
fmt.Println(styles.Separator.Render("🗃️ Starting " + styles.Status.Render("SQL reconnaissance") + "..."))
log := output.Module("SQL")
log.Start()
spin := output.NewSpinner("Scanning for SQL exposures")
spin.Start()
sanitizedURL := strings.Split(targetURL, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
sqllog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "SQL 🗃️",
}).With("url", targetURL)
sqllog.Infof("Starting SQL reconnaissance...")
result := &SQLResult{
AdminPanels: make([]SQLAdminPanel, 0, 8),
DatabaseErrors: make([]SQLDatabaseError, 0, 8),
@@ -168,7 +166,7 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
resp, err := client.Get(checkURL)
if err != nil {
log.Debugf("Error checking %s: %v", checkURL, err)
charmlog.Debugf("Error checking %s: %v", checkURL, err)
continue
}
@@ -193,13 +191,15 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
result.AdminPanels = append(result.AdminPanels, panel)
mu.Unlock()
sqllog.Warnf("Found %s at [%s] (status: %d)",
styles.SeverityHigh.Render(adminPath.panelType),
styles.Highlight.Render(checkURL),
spin.Stop()
log.Warn("Found %s at %s (status: %d)",
output.SeverityHigh.Render(adminPath.panelType),
output.Highlight.Render(checkURL),
resp.StatusCode)
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Found %s at [%s] (status: %d)\n", adminPath.panelType, checkURL, resp.StatusCode))
logger.Write(sanitizedURL, logdir, "Found "+adminPath.panelType+" at ["+checkURL+"] (status: "+strconv.Itoa(resp.StatusCode)+")\n")
}
}
} else {
@@ -211,7 +211,7 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
wg.Wait()
// check main URL for database errors
checkDatabaseErrors(client, targetURL, sanitizedURL, result, sqllog, logdir, &mu, seenErrors)
checkDatabaseErrors(client, targetURL, sanitizedURL, result, logdir, &mu, seenErrors)
// check common endpoints that might expose database errors
errorCheckPaths := []string{
@@ -226,22 +226,27 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
for _, path := range errorCheckPaths {
checkURL := strings.TrimSuffix(targetURL, "/") + path
checkDatabaseErrors(client, checkURL, sanitizedURL, result, sqllog, logdir, &mu, seenErrors)
checkDatabaseErrors(client, checkURL, sanitizedURL, result, logdir, &mu, seenErrors)
}
spin.Stop()
// summary
totalFindings := len(result.AdminPanels) + len(result.DatabaseErrors)
if len(result.AdminPanels) > 0 {
sqllog.Warnf("Found %d database admin panel(s)", len(result.AdminPanels))
log.Warn("Found %d database admin panel(s)", len(result.AdminPanels))
}
if len(result.DatabaseErrors) > 0 {
sqllog.Warnf("Found %d database error disclosure(s)", len(result.DatabaseErrors))
log.Warn("Found %d database error disclosure(s)", len(result.DatabaseErrors))
}
if len(result.AdminPanels) == 0 && len(result.DatabaseErrors) == 0 {
sqllog.Infof("No SQL exposures found")
if totalFindings == 0 {
log.Info("No SQL exposures found")
log.Complete(0, "found")
return nil, nil
}
log.Complete(totalFindings, "found")
return result, nil
}
@@ -279,7 +284,7 @@ func isAdminPanel(body string, panelType string) bool {
}
}
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, sqllog *log.Logger, logdir string, mu *sync.Mutex, seen map[string]bool) {
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, logdir string, mu *sync.Mutex, seen map[string]bool) {
resp, err := client.Get(checkURL)
if err != nil {
return
@@ -310,12 +315,12 @@ func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, res
result.DatabaseErrors = append(result.DatabaseErrors, dbError)
mu.Unlock()
sqllog.Warnf("Database error disclosure: %s at [%s]",
styles.SeverityHigh.Render(pattern.databaseType),
styles.Highlight.Render(checkURL))
output.Warn("Database error disclosure: %s at %s",
output.SeverityHigh.Render(pattern.databaseType),
output.Highlight.Render(checkURL))
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Database error disclosure: %s at [%s]\n", pattern.databaseType, checkURL))
logger.Write(sanitizedURL, logdir, "Database error disclosure: "+pattern.databaseType+" at ["+checkURL+"]\n")
}
break // only report one database type per URL
}

View File

@@ -13,36 +13,31 @@
package scan
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/internal/output"
"github.com/likexian/whois"
)
func Whois(url string, logdir string) {
fmt.Println(styles.Separator.Render("💭 Starting " + styles.Status.Render("WHOIS Lookup") + "..."))
output.ScanStart("WHOIS lookup")
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, " WHOIS scanning"); err != nil {
log.Errorf("Error creating log file: %v", err)
output.Error("Error creating log file: %v", err)
return
}
}
whoislog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "WHOIS 💭",
})
whoislog.Infof("Starting WHOIS")
result, err := whois.Whois(sanitizedURL)
if err == nil {
log.Info(result)
logger.Write(sanitizedURL, logdir, result)
output.ScanComplete("WHOIS lookup", 1, "completed")
} else {
output.Error("WHOIS lookup failed: %v", err)
}
}

View File

@@ -1,68 +1,30 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// Package styles provides custom styling options for the SIF tool's console output.
// It uses the lipgloss library to create visually appealing and consistent text styles.
// This package re-exports styles from internal/output for backwards compatibility.
package styles
import "github.com/charmbracelet/lipgloss"
var (
// Separator style for creating visual breaks in the output
Separator = lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), true, false).
Bold(true)
// Status style for highlighting important status messages
Status = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#00ff1a"))
// Highlight style for emphasizing specific text
Highlight = lipgloss.NewStyle().
Bold(true).
Underline(true)
// Box style for creating bordered content boxes
Box = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#fafafa")).
BorderStyle(lipgloss.RoundedBorder()).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
// Subheading style for secondary titles or headers
Subheading = lipgloss.NewStyle().
Bold(true).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
import (
"github.com/charmbracelet/lipgloss"
"github.com/dropalldatabases/sif/internal/output"
)
// Severity level styles for color-coding vulnerability severities
// Re-export styles from output package
var (
SeverityLow = lipgloss.NewStyle().
Foreground(lipgloss.Color("#00ff00"))
SeverityMedium = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ffff00"))
SeverityHigh = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ff8800"))
SeverityCritical = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ff0000"))
Status = output.Status
Highlight = output.Highlight
Box = output.Box
Subheading = output.Subheading
)
// Separator style - kept for backwards compatibility but deprecated
// Use output.ScanStart() instead
var Separator = lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), true, false).
Bold(true)
// Severity level styles - re-exported from output
var (
SeverityLow = output.SeverityLow
SeverityMedium = output.SeverityMedium
SeverityHigh = output.SeverityHigh
SeverityCritical = output.SeverityCritical
)

30
sif.go
View File

@@ -27,10 +27,10 @@ import (
"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/frameworks"
jsscan "github.com/dropalldatabases/sif/internal/scan/js"
"github.com/dropalldatabases/sif/internal/styles"
)
// App represents the main application structure for sif.
@@ -74,8 +74,10 @@ func New(settings *config.Settings) (*App, error) {
app := &App{settings: settings}
if !settings.ApiMode {
fmt.Println(styles.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
fmt.Println(styles.Subheading.Render("\nblazing-fast pentesting suite\nman's best friend\n\nbsd 3-clause · (c) 2022-2025 vmfunc, xyzeva & contributors\n"))
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
@@ -164,7 +166,7 @@ func (app *App) Run() error {
scansRun := make([]string, 0, 16)
for _, url := range app.targets {
log.Infof("📡Starting scan on %s...", url)
output.Info("Starting scan on %s", output.Highlight.Render(url))
moduleResults := make([]ModuleResult, 0, 16)
@@ -250,7 +252,6 @@ func (app *App) Run() error {
scansRun = append(scansRun, "Whois")
}
// func Git(url string, timeout time.Duration, threads int, logdir string)
if app.settings.Git {
result, err := scan.Git(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
if err != nil {
@@ -377,17 +378,18 @@ func (app *App) Run() error {
}
for _, m := range toRun {
log.Infof("🔍 Running module: %s", m.Info().ID)
modLog := output.Module(m.Info().ID)
modLog.Start()
result, err := m.Execute(context.Background(), url, opts)
if err != nil {
log.Warnf("Module %s failed: %v", m.Info().ID, err)
modLog.Error("failed: %v", err)
continue
}
if result != nil && len(result.Findings) > 0 {
moduleResults = append(moduleResults, NewModuleResult(result))
log.Infof("✅ Module %s: %d findings", m.Info().ID, len(result.Findings))
modLog.Complete(len(result.Findings), "findings")
} else {
log.Infof(" Module %s: no findings", m.Info().ID)
modLog.Complete(0, "findings")
}
}
}
@@ -408,15 +410,7 @@ func (app *App) Run() error {
}
if !app.settings.ApiMode {
scansRunList := " • " + strings.Join(scansRun, "\n • ")
if app.settings.LogDir != "" {
fmt.Println(styles.Box.Render(fmt.Sprintf("🌿 All scans completed!\n📂 Output saved to files: %s\n\n🔍 Ran scans:\n%s",
strings.Join(app.logFiles, ", "),
scansRunList)))
} else {
fmt.Println(styles.Box.Render(fmt.Sprintf("🌿 All scans completed!\n\n🔍 Ran scans:\n%s",
scansRunList)))
}
output.PrintSummary(scansRun, app.logFiles)
}
return nil