feat(tmplexec): add feature flag to control error enrichment in debug mode for better traceability and cleaner output

This commit is contained in:
knakul853 2025-07-21 21:10:43 +05:30
parent 3e9bee7400
commit 1af32d3b9d
3 changed files with 220 additions and 26 deletions

View File

@ -217,7 +217,7 @@ func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
lastMatcherEvent.Lock()
defer lastMatcherEvent.Unlock()
lastMatcherEvent.InternalEvent["error"] = getErrorCause(ctx.GenerateErrorMessage())
lastMatcherEvent.InternalEvent["error"] = getErrorCause(ctx.GenerateErrorMessage(), e.options.Options.Debug)
writeFailureCallback(lastMatcherEvent, e.options.Options.MatcherStatus)
}
@ -233,7 +233,7 @@ func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
Info: e.options.TemplateInfo,
Type: e.getTemplateType(),
Host: ctx.Input.MetaInput.Input,
Error: getErrorCause(ctx.GenerateErrorMessage()),
Error: getErrorCause(ctx.GenerateErrorMessage(), e.options.Options.Debug),
},
},
OperatorsResult: &operators.Result{
@ -249,24 +249,30 @@ func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
// getErrorCause tries to parse the cause of given error
// this is legacy support due to use of errorutil in existing libraries
// but this should not be required once all libraries are updated
func getErrorCause(err error) string {
// debugMode controls whether to enrich errors with stack traces
func getErrorCause(err error, debugMode bool) string {
if err == nil {
return ""
}
errx := errkit.FromError(err)
var cause error
for _, e := range errx.Errors() {
if e != nil && strings.Contains(e.Error(), "context deadline exceeded") {
continue
if debugMode {
errx := errkit.FromError(err)
var cause error
for _, e := range errx.Errors() {
if e != nil && strings.Contains(e.Error(), "context deadline exceeded") {
continue
}
cause = e
break
}
cause = e
break
if cause == nil {
cause = errkit.Append(errkit.New("could not get error cause"), errx)
}
// parseScanErrorWithDebug prettifies the error message and removes everything except the cause
return parseScanErrorWithDebug(cause.Error(), debugMode)
}
if cause == nil {
cause = errkit.Append(errkit.New("could not get error cause"), errx)
}
// parseScanError prettifies the error message and removes everything except the cause
return parseScanError(cause.Error())
return parseScanErrorWithDebug(err.Error(), debugMode)
}
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.

184
pkg/tmplexec/exec_test.go Normal file
View File

@ -0,0 +1,184 @@
package tmplexec
import (
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetErrorCause_DebugMode(t *testing.T) {
testCases := []struct {
name string
err error
debugMode bool
expectTrace bool
description string
}{
{
name: "Debug mode enabled - should enrich error",
err: errors.New("malformed request specified: GET /file=>"),
debugMode: true,
expectTrace: true,
description: "When debug mode is enabled, errors should be enriched with additional context",
},
{
name: "Debug mode disabled - should not enrich error",
err: errors.New("malformed request specified: GET /file=>"),
debugMode: false,
expectTrace: false,
description: "When debug mode is disabled, errors should not be enriched to avoid stack traces",
},
{
name: "Nil error - debug mode enabled",
err: nil,
debugMode: true,
expectTrace: false,
description: "Nil errors should return empty string regardless of debug mode",
},
{
name: "Nil error - debug mode disabled",
err: nil,
debugMode: false,
expectTrace: false,
description: "Nil errors should return empty string regardless of debug mode",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := getErrorCause(tc.err, tc.debugMode)
if tc.err == nil {
assert.Empty(t, result, "Expected empty string for nil error")
return
}
// Basic validation - result should contain the original error message
assert.Contains(t, result, "malformed request specified",
"Result should contain the original error message")
if tc.debugMode {
// In debug mode, we expect the error to be processed through errkit
// This doesn't necessarily mean stack traces in the final result,
// but ensures the error went through the enrichment path
assert.NotEmpty(t, result, "Debug mode should produce non-empty result")
} else {
// In non-debug mode, we should get a simple error message
// without any stack trace information
assert.NotContains(t, result, "Stacktrace:",
"Non-debug mode should not contain stack trace")
assert.NotContains(t, result, "goroutine",
"Non-debug mode should not contain goroutine information")
assert.NotContains(t, result, "runtime/debug.Stack()",
"Non-debug mode should not contain runtime stack information")
}
})
}
}
func TestParseScanErrorWithDebug(t *testing.T) {
testCases := []struct {
name string
msg string
debugMode bool
expected string
}{
{
name: "Simple error - debug mode off",
msg: "connection refused",
debugMode: false,
expected: "connection refused",
},
{
name: "Simple error - debug mode on",
msg: "connection refused",
debugMode: true,
expected: "connection refused",
},
{
name: "ReadStatusLine error - debug mode off",
msg: "ReadStatusLine: malformed HTTP response",
debugMode: false,
expected: "malformed HTTP response",
},
{
name: "Empty message",
msg: "",
debugMode: false,
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := parseScanErrorWithDebug(tc.msg, tc.debugMode)
assert.Contains(t, result, tc.expected,
"Result should contain expected error message")
})
}
}
func TestGetErrorCause_ContextDeadlineHandling(t *testing.T) {
testCases := []struct {
name string
err error
debugMode bool
}{
{
name: "Context deadline exceeded - debug mode",
err: errors.New("context deadline exceeded"),
debugMode: true,
},
{
name: "Context deadline exceeded - non-debug mode",
err: errors.New("context deadline exceeded"),
debugMode: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := getErrorCause(tc.err, tc.debugMode)
assert.NotEmpty(t, result, "Should handle context deadline exceeded errors")
})
}
}
func TestGetErrorCause_ErrorMessageFormat(t *testing.T) {
originalError := errors.New("test error message")
debugResult := getErrorCause(originalError, true)
nonDebugResult := getErrorCause(originalError, false)
// Both should contain the original error message
assert.Contains(t, debugResult, "test error message",
"Debug result should contain original error message")
assert.Contains(t, nonDebugResult, "test error message",
"Non-debug result should contain original error message")
// Non-debug should be simpler (no enrichment artifacts)
assert.True(t, len(nonDebugResult) <= len(debugResult) ||
strings.Count(nonDebugResult, "\n") <= strings.Count(debugResult, "\n"),
"Non-debug result should be simpler than debug result")
}
// Benchmark to ensure the non-debug path is more efficient
func BenchmarkGetErrorCause_Debug(b *testing.B) {
err := errors.New("benchmark test error")
b.ResetTimer()
for i := 0; i < b.N; i++ {
getErrorCause(err, true)
}
}
func BenchmarkGetErrorCause_NonDebug(b *testing.B) {
err := errors.New("benchmark test error")
b.ResetTimer()
for i := 0; i < b.N; i++ {
getErrorCause(err, false)
}
}

View File

@ -42,9 +42,8 @@ var (
reNoKind = regexp.MustCompile(`([\[][^][]+[\]]|errKind=[^ ]+) `)
)
// parseScanError parses given scan error and only returning the cause
// instead of inefficient one
func parseScanError(msg string) string {
// parseScanErrorWithDebug processes error messages with optional debug mode
func parseScanErrorWithDebug(msg string, debugMode bool) string {
if msg == "" {
return ""
}
@ -58,14 +57,19 @@ func parseScanError(msg string) string {
parts := strings.Split(msg, ":")
msg = strings.TrimSpace(parts[len(parts)-1])
}
e := errkit.FromError(errors.New(msg))
for _, err := range e.Errors() {
if err != nil && strings.Contains(err.Error(), "context deadline exceeded") {
continue
if debugMode {
e := errkit.FromError(errors.New(msg))
for _, err := range e.Errors() {
if err != nil && strings.Contains(err.Error(), "context deadline exceeded") {
continue
}
msg = reNoKind.ReplaceAllString(err.Error(), "")
return msg
}
msg = reNoKind.ReplaceAllString(err.Error(), "")
return msg
wrapped := errkit.Append(errkit.New("failed to get error cause"), e).Error()
return reNoKind.ReplaceAllString(wrapped, "")
}
wrapped := errkit.Append(errkit.New("failed to get error cause"), e).Error()
return reNoKind.ReplaceAllString(wrapped, "")
return reNoKind.ReplaceAllString(msg, "")
}