From 00a66adf2711a0262f703223300076df05162ba1 Mon Sep 17 00:00:00 2001 From: vmfunc Date: Sat, 3 Jan 2026 05:51:26 -0800 Subject: [PATCH] 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 --- go.mod | 2 +- internal/modules/loader.go | 6 +- internal/output/output.go | 283 +++++++++++++++++++++++++++++ internal/output/progress.go | 155 ++++++++++++++++ internal/output/spinner.go | 109 +++++++++++ internal/scan/cms.go | 36 ++-- internal/scan/dirlist.go | 62 +++---- internal/scan/dnslist.go | 71 +++----- internal/scan/dork.go | 29 +-- internal/scan/frameworks/detect.go | 49 +++-- internal/scan/git.go | 37 ++-- internal/scan/headers.go | 19 +- internal/scan/js/scan.go | 33 ++-- internal/scan/lfi.go | 46 ++--- internal/scan/nuclei.go | 12 +- internal/scan/ports.go | 54 +++--- internal/scan/scan.go | 22 +-- internal/scan/shodan.go | 59 +++--- internal/scan/sql.go | 61 ++++--- internal/scan/whois.go | 17 +- internal/styles/styles.go | 84 +++------ sif.go | 30 ++- 22 files changed, 880 insertions(+), 396 deletions(-) create mode 100644 internal/output/output.go create mode 100644 internal/output/progress.go create mode 100644 internal/output/spinner.go diff --git a/go.mod b/go.mod index 708eff6..2f13895 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/internal/modules/loader.go b/internal/modules/loader.go index aeb242e..de73812 100644 --- a/internal/modules/loader.go +++ b/internal/modules/loader.go @@ -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 } diff --git a/internal/output/output.go b/internal/output/output.go new file mode 100644 index 0000000..c4fd12e --- /dev/null +++ b/internal/output/output.go @@ -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() +} diff --git a/internal/output/progress.go b/internal/output/progress.go new file mode 100644 index 0000000..964c6b8 --- /dev/null +++ b/internal/output/progress.go @@ -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) +} diff --git a/internal/output/spinner.go b/internal/output/spinner.go new file mode 100644 index 0000000..39d7731 --- /dev/null +++ b/internal/output/spinner.go @@ -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) + } + } +} diff --git a/internal/scan/cms.go b/internal/scan/cms.go index 3c8c9d1..d46e382 100644 --- a/internal/scan/cms.go +++ b/internal/scan/cms.go @@ -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 } diff --git a/internal/scan/dirlist.go b/internal/scan/dirlist.go index 887763c..882c599 100644 --- a/internal/scan/dirlist.go +++ b/internal/scan/dirlist.go @@ -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 } diff --git a/internal/scan/dnslist.go b/internal/scan/dnslist.go index 152be52..e324e31 100644 --- a/internal/scan/dnslist.go +++ b/internal/scan/dnslist.go @@ -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 } diff --git a/internal/scan/dork.go b/internal/scan/dork.go index 9f64918..d727171 100644 --- a/internal/scan/dork.go +++ b/internal/scan/dork.go @@ -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 } diff --git a/internal/scan/frameworks/detect.go b/internal/scan/frameworks/detect.go index 3398c69..9065d83 100644 --- a/internal/scan/frameworks/detect.go +++ b/internal/scan/frameworks/detect.go @@ -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 } diff --git a/internal/scan/git.go b/internal/scan/git.go index 66bbe3c..08ec1d5 100644 --- a/internal/scan/git.go +++ b/internal/scan/git.go @@ -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 } diff --git a/internal/scan/headers.go b/internal/scan/headers.go index fceebd9..ef591b9 100644 --- a/internal/scan/headers.go +++ b/internal/scan/headers.go @@ -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 } diff --git a/internal/scan/js/scan.go b/internal/scan/js/scan.go index 15ca3b5..9648703 100644 --- a/internal/scan/js/scan.go +++ b/internal/scan/js/scan.go @@ -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 } diff --git a/internal/scan/lfi.go b/internal/scan/lfi.go index 4674144..342e6c3 100644 --- a/internal/scan/lfi.go +++ b/internal/scan/lfi.go @@ -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 } diff --git a/internal/scan/nuclei.go b/internal/scan/nuclei.go index 2bf05c2..6a35c06 100644 --- a/internal/scan/nuclei.go +++ b/internal/scan/nuclei.go @@ -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 } diff --git a/internal/scan/ports.go b/internal/scan/ports.go index f96e441..1786dd9 100644 --- a/internal/scan/ports.go +++ b/internal/scan/ports.go @@ -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 } diff --git a/internal/scan/scan.go b/internal/scan/scan.go index 789cc72..7b59bf2 100644 --- a/internal/scan/scan.go +++ b/internal/scan/scan.go @@ -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() diff --git a/internal/scan/shodan.go b/internal/scan/shodan.go index f1c4b08..fead817 100644 --- a/internal/scan/shodan.go +++ b/internal/scan/shodan.go @@ -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) } } diff --git a/internal/scan/sql.go b/internal/scan/sql.go index be1798c..ce2b319 100644 --- a/internal/scan/sql.go +++ b/internal/scan/sql.go @@ -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 } diff --git a/internal/scan/whois.go b/internal/scan/whois.go index af1a0eb..9ce9c5d 100644 --- a/internal/scan/whois.go +++ b/internal/scan/whois.go @@ -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) } } diff --git a/internal/styles/styles.go b/internal/styles/styles.go index 826c064..3cd0af0 100644 --- a/internal/styles/styles.go +++ b/internal/styles/styles.go @@ -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 ) diff --git a/sif.go b/sif.go index a6bad4e..1d55809 100644 --- a/sif.go +++ b/sif.go @@ -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