Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 60 additions & 23 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,42 +161,73 @@ func Writer() io.Writer {
return sink
}

// Info prints an informational message with [*] prefix
func Info(format string, args ...interface{}) {
// Sink is a routable output destination: the writer chrome lands on, plus
// whether interactive widgets (spinners, live progress) may animate on it. A
// scan can be handed its own Sink so its chrome routes to a chosen writer.
type Sink struct {
w io.Writer
interactive bool
}

// NewSink returns a Sink writing to w. interactive reports whether live widgets
// are allowed; a captured buffer sink passes false so spinners/progress no-op.
func NewSink(w io.Writer, interactive bool) *Sink {
return &Sink{w: w, interactive: interactive}
}

// DefaultSink is the ambient sink the package-level chrome funcs write to: the
// process output routing configured by SetSilent.
func DefaultSink() *Sink {
return &Sink{w: sink, interactive: IsTTY && !silent}
}

// Writer exposes the underlying writer, for callers that render their own chrome.
func (s *Sink) Writer() io.Writer { return s.w }

// Interactive reports whether live widgets may animate on this sink; a captured
// buffer sink reports false so spinners and progress bars stay silent.
func (s *Sink) Interactive() bool { return s.interactive }

// Info logs a [*]-prefixed message; a no-op in API mode.
func (s *Sink) Info(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s\n", prefixInfo.Render("[*]"), msg)
fmt.Fprintf(s.w, "%s %s\n", prefixInfo.Render("[*]"), fmt.Sprintf(format, args...))
}

// Success prints a success message with [+] prefix
func Success(format string, args ...interface{}) {
// Success logs a [+]-prefixed message; a no-op in API mode.
func (s *Sink) Success(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s\n", prefixSuccess.Render("[+]"), msg)
fmt.Fprintf(s.w, "%s %s\n", prefixSuccess.Render("[+]"), fmt.Sprintf(format, args...))
}

// Warn prints a warning message with [!] prefix
func Warn(format string, args ...interface{}) {
// Warn logs a [!]-prefixed message; a no-op in API mode.
func (s *Sink) Warn(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s\n", prefixWarning.Render("[!]"), msg)
fmt.Fprintf(s.w, "%s %s\n", prefixWarning.Render("[!]"), fmt.Sprintf(format, args...))
}

// Error prints an error message with [-] prefix
func Error(format string, args ...interface{}) {
// Error logs a [-]-prefixed message; a no-op in API mode.
func (s *Sink) Error(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s\n", prefixError.Render("[-]"), msg)
fmt.Fprintf(s.w, "%s %s\n", prefixError.Render("[-]"), fmt.Sprintf(format, args...))
}

func Info(format string, args ...interface{}) { DefaultSink().Info(format, args...) }

func Success(format string, args ...interface{}) { DefaultSink().Success(format, args...) }

func Warn(format string, args ...interface{}) { DefaultSink().Warn(format, args...) }

func Error(format string, args ...interface{}) { DefaultSink().Error(format, args...) }

// ScanStart prints a styled scan start message
func ScanStart(scanName string) {
if apiMode {
Expand All @@ -213,18 +244,24 @@ func ScanComplete(scanName string, resultCount int, resultType string) {
fmt.Fprintf(sink, "%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 DefaultSink().Module(name)
}

// Module creates a prefixed logger bound to this sink.
func (s *Sink) Module(name string) *ModuleLogger {
return &ModuleLogger{
name: name,
style: moduleStyleFor(name),
sink: s,
}
}

// ModuleLogger provides prefixed logging for a specific module
type ModuleLogger struct {
name string
style lipgloss.Style
sink *Sink
}

func (m *ModuleLogger) prefix() string {
Expand All @@ -237,7 +274,7 @@ func (m *ModuleLogger) Info(format string, args ...interface{}) {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s\n", m.prefix(), msg)
fmt.Fprintf(m.sink.w, "%s %s\n", m.prefix(), msg)
}

// Success prints a success message with module prefix
Expand All @@ -246,7 +283,7 @@ func (m *ModuleLogger) Success(format string, args ...interface{}) {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
fmt.Fprintf(m.sink.w, "%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
}

// Warn prints a warning message with module prefix
Expand All @@ -255,7 +292,7 @@ func (m *ModuleLogger) Warn(format string, args ...interface{}) {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
fmt.Fprintf(m.sink.w, "%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
}

// Error prints an error message with module prefix
Expand All @@ -264,23 +301,23 @@ func (m *ModuleLogger) Error(format string, args ...interface{}) {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
fmt.Fprintf(m.sink.w, "%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.Fprintf(sink, "\n%s starting scan\n", m.prefix())
fmt.Fprintf(m.sink.w, "\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.Fprintf(sink, "%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
fmt.Fprintf(m.sink.w, "%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
}

// ClearLine clears the current line (for progress bar updates). silent mode is
Expand Down
83 changes: 83 additions & 0 deletions internal/output/sink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

package output

import (
"bytes"
"strings"
"testing"
)

// TestSinkRoutesToOwnWriter is the seam a per-target buffer depends on: chrome
// written through a Sink must land on that sink's writer, not the process-wide
// default. Both the top-level funcs and a sink-bound module logger must honour
// it so a routed scan's output can be captured whole.
func TestSinkRoutesToOwnWriter(t *testing.T) {
var buf bytes.Buffer
s := NewSink(&buf, false)

s.Info("info-marker")
s.Success("success-marker")
s.Warn("warn-marker")
s.Error("error-marker")
s.Module("MOD").Warn("module-marker")

got := buf.String()
for _, want := range []string{"info-marker", "success-marker", "warn-marker", "error-marker", "module-marker"} {
if !strings.Contains(got, want) {
t.Errorf("sink writer missing %q; got:\n%s", want, got)
}
}

if s.Interactive() {
t.Error("NewSink(w, false).Interactive() = true, want false")
}
}

// TestSinkDoesNotLeakToDefault confirms a routed sink is isolated: writing to it
// must leave the process default sink untouched, which is what lets concurrent
// targets each own their buffer without stepping on each other.
func TestSinkDoesNotLeakToDefault(t *testing.T) {
var target, other bytes.Buffer

// point the default sink at `other` for the duration of this test.
prev := sink
sink = &other
defer func() { sink = prev }()

NewSink(&target, false).Info("only-in-target")

if other.Len() != 0 {
t.Errorf("routed write leaked to default sink: %q", other.String())
}
if !strings.Contains(target.String(), "only-in-target") {
t.Errorf("routed write missing from target sink: %q", target.String())
}
}

// TestDefaultSinkTracksGlobal pins that the package-level funcs still follow the
// SetSilent routing, so existing single-target behaviour is unchanged.
func TestDefaultSinkTracksGlobal(t *testing.T) {
var buf bytes.Buffer
prev := sink
sink = &buf
defer func() { sink = prev }()

Info("default-path")
if !strings.Contains(buf.String(), "default-path") {
t.Errorf("package Info did not reach the default sink: %q", buf.String())
}
if DefaultSink().Writer() != Writer() {
t.Error("DefaultSink().Writer() should equal the package Writer()")
}
}
Loading