mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
feat(output): add routable Sink for per-scan output capture (#331)
* feat(output): add routable Sink for per-scan output capture introduce a Sink (writer plus an interactive flag) that the package-level chrome funcs delegate to via DefaultSink, and give ModuleLogger a sink it writes through. a caller can now hand a scan its own Sink so that scan's chrome lands on a dedicated writer instead of the process-wide default. default routing is unchanged: Info/Warn/Module and friends still write to the same stdout/stderr sink SetSilent configures. * fix(output): trim redundant sink doc comments drop comments on thin package-level wrappers that just forward to the Sink methods, and tighten the Sink methods' own comments to note the api-mode no-op instead of restating the func name
This commit is contained in:
+69
-32
@@ -196,42 +196,73 @@ func Writer() io.Writer {
|
||||
return sink
|
||||
}
|
||||
|
||||
// Info prints an informational message with [*] prefix
|
||||
func Info(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixInfo.Render("[*]"), msg)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Success prints a success message with [+] prefix
|
||||
func Success(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixSuccess.Render("[+]"), msg)
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Warn prints a warning message with [!] prefix
|
||||
func Warn(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixWarning.Render("[!]"), msg)
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Error prints an error message with [-] prefix
|
||||
func Error(format string, args ...interface{}) {
|
||||
// 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", prefixError.Render("[-]"), msg)
|
||||
fmt.Fprintf(s.w, "%s %s\n", prefixInfo.Render("[*]"), fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Success logs a [+]-prefixed message; a no-op in API mode.
|
||||
func (s *Sink) Success(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(s.w, "%s %s\n", prefixSuccess.Render("[+]"), fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warn logs a [!]-prefixed message; a no-op in API mode.
|
||||
func (s *Sink) Warn(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(s.w, "%s %s\n", prefixWarning.Render("[!]"), fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error logs a [-]-prefixed message; a no-op in API mode.
|
||||
func (s *Sink) Error(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
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 {
|
||||
@@ -248,11 +279,16 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +296,7 @@ func Module(name string) *ModuleLogger {
|
||||
type ModuleLogger struct {
|
||||
name string
|
||||
style lipgloss.Style
|
||||
sink *Sink
|
||||
}
|
||||
|
||||
func (m *ModuleLogger) prefix() string {
|
||||
@@ -272,7 +309,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
|
||||
@@ -281,7 +318,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
|
||||
@@ -290,7 +327,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
|
||||
@@ -299,7 +336,7 @@ 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)
|
||||
@@ -307,7 +344,7 @@ 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
|
||||
@@ -315,7 +352,7 @@ 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
|
||||
|
||||
@@ -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()")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user