mirror of
https://github.com/lunchcat/sif.git
synced 2026-03-12 21:23:04 -07:00
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:
2
go.mod
2
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
|
||||
)
|
||||
|
||||
@@ -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
283
internal/output/output.go
Normal 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
155
internal/output/progress.go
Normal 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
109
internal/output/spinner.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
30
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
|
||||
|
||||
Reference in New Issue
Block a user