Compare commits

..
Author SHA1 Message Date
knakul853 507a4feaee only save what needed 2025-06-10 19:42:04 +05:30
20 changed files with 453 additions and 248 deletions

No files matched your search

-8
View File
@@ -463,14 +463,6 @@ func EnablePassiveMode() NucleiSDKOptions {
}
}
// EnableMatcherStatus allows enabling matcher status
func EnableMatcherStatus() NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.MatcherStatus = true
return nil
}
}
// WithAuthProvider allows setting a custom authprovider implementation
func WithAuthProvider(provider authprovider.AuthProvider) NucleiSDKOptions {
return func(e *NucleiEngine) error {
+6 -11
View File
@@ -52,19 +52,14 @@ func (d *Dynamic) GetDomainAndDomainRegex() ([]string, []string) {
}
func (d *Dynamic) UnmarshalJSON(data []byte) error {
if d == nil {
return errorutil.New("cannot unmarshal into nil Dynamic struct")
}
// Use an alias type (auxiliary) to avoid a recursive call in this method.
type Alias Dynamic
// If d.Secret was nil, json.Unmarshal will allocate a new Secret object
// and populate it from the top level JSON fields.
if err := json.Unmarshal(data, (*Alias)(d)); err != nil {
if err := json.Unmarshal(data, &d); err != nil {
return err
}
var s Secret
if err := json.Unmarshal(data, &s); err != nil {
return err
}
d.Secret = &s
return nil
}
-125
View File
@@ -1,125 +0,0 @@
package authx
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDynamicUnmarshalJSON(t *testing.T) {
t.Run("basic-unmarshal", func(t *testing.T) {
data := []byte(`{
"template": "test-template.yaml",
"variables": [
{
"key": "username",
"value": "testuser"
}
],
"secrets": [
{
"type": "BasicAuth",
"domains": ["example.com"],
"username": "user1",
"password": "pass1"
}
],
"type": "BasicAuth",
"domains": ["test.com"],
"username": "testuser",
"password": "testpass"
}`)
var d Dynamic
err := d.UnmarshalJSON(data)
require.NoError(t, err)
// Secret
require.NotNil(t, d.Secret)
require.Equal(t, "BasicAuth", d.Secret.Type)
require.Equal(t, []string{"test.com"}, d.Secret.Domains)
require.Equal(t, "testuser", d.Secret.Username)
require.Equal(t, "testpass", d.Secret.Password)
// Dynamic fields
require.Equal(t, "test-template.yaml", d.TemplatePath)
require.Len(t, d.Variables, 1)
require.Equal(t, "username", d.Variables[0].Key)
require.Equal(t, "testuser", d.Variables[0].Value)
require.Len(t, d.Secrets, 1)
require.Equal(t, "BasicAuth", d.Secrets[0].Type)
require.Equal(t, []string{"example.com"}, d.Secrets[0].Domains)
require.Equal(t, "user1", d.Secrets[0].Username)
require.Equal(t, "pass1", d.Secrets[0].Password)
})
t.Run("complex-unmarshal", func(t *testing.T) {
data := []byte(`{
"template": "test-template.yaml",
"variables": [
{
"key": "token",
"value": "Bearer xyz"
}
],
"secrets": [
{
"type": "CookiesAuth",
"domains": ["example.com"],
"cookies": [
{
"key": "session",
"value": "abc123"
}
]
}
],
"type": "HeadersAuth",
"domains": ["api.test.com"],
"headers": [
{
"key": "X-API-Key",
"value": "secret-key"
}
]
}`)
var d Dynamic
err := d.UnmarshalJSON(data)
require.NoError(t, err)
// Secret
require.NotNil(t, d.Secret)
require.Equal(t, "HeadersAuth", d.Secret.Type)
require.Equal(t, []string{"api.test.com"}, d.Secret.Domains)
require.Len(t, d.Secret.Headers, 1)
require.Equal(t, "X-API-Key", d.Secret.Headers[0].Key)
require.Equal(t, "secret-key", d.Secret.Headers[0].Value)
// Dynamic fields
require.Equal(t, "test-template.yaml", d.TemplatePath)
require.Len(t, d.Variables, 1)
require.Equal(t, "token", d.Variables[0].Key)
require.Equal(t, "Bearer xyz", d.Variables[0].Value)
require.Len(t, d.Secrets, 1)
require.Equal(t, "CookiesAuth", d.Secrets[0].Type)
require.Equal(t, []string{"example.com"}, d.Secrets[0].Domains)
require.Len(t, d.Secrets[0].Cookies, 1)
require.Equal(t, "session", d.Secrets[0].Cookies[0].Key)
require.Equal(t, "abc123", d.Secrets[0].Cookies[0].Value)
})
t.Run("invalid-json", func(t *testing.T) {
data := []byte(`{invalid json}`)
var d Dynamic
err := d.UnmarshalJSON(data)
require.Error(t, err)
})
t.Run("empty-json", func(t *testing.T) {
data := []byte(`{}`)
var d Dynamic
err := d.UnmarshalJSON(data)
require.NoError(t, err)
})
}
+1 -1
View File
@@ -31,7 +31,7 @@ const (
CLIConfigFileName = "config.yaml"
ReportingConfigFilename = "reporting-config.yaml"
// Version is the current version of nuclei
Version = `v3.4.5`
Version = `v3.4.4`
// Directory Names of custom templates
CustomS3TemplatesDirName = "s3"
CustomGitHubTemplatesDirName = "github"
+3 -3
View File
@@ -38,7 +38,7 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
match, err = template.Executer.Execute(ctx)
}
if err != nil {
gologger.Warning().Msgf("[%s] Could not execute step (self-contained): %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), err)
gologger.Warning().Msgf("[%s] Could not execute step: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), err)
}
results.CompareAndSwap(false, match)
}(v)
@@ -140,7 +140,7 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
}
}
if err != nil {
gologger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), value.Input, err)
gologger.Warning().Msgf("[%s] Could not execute step: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), err)
}
results.CompareAndSwap(false, match)
}(index, skip, scannedValue)
@@ -206,7 +206,7 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
}
}
if err != nil {
gologger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), value.Input, err)
gologger.Warning().Msgf("[%s] Could not execute step: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), err)
}
results.CompareAndSwap(false, match)
}(tpl, target, sg)
-1
View File
@@ -185,7 +185,6 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
func (request *Request) getDnsClient(options *protocols.ExecutorOptions, metadata map[string]interface{}) (*retryabledns.Client, error) {
dnsClientOptions := &dnsclientpool.Configuration{
Retries: request.Retries,
Proxy: options.Options.AliveSocksProxy,
}
if len(request.Resolvers) > 0 {
if len(request.Resolvers) > 0 {
@@ -51,8 +51,6 @@ type Configuration struct {
Retries int
// Resolvers contains the specific per request resolvers
Resolvers []string
// Proxy contains the proxy to use for the dns client
Proxy string
}
// Hash returns the hash of the configuration to allow client pooling
@@ -62,8 +60,6 @@ func (c *Configuration) Hash() string {
builder.WriteString(strconv.Itoa(c.Retries))
builder.WriteString("l")
builder.WriteString(strings.Join(c.Resolvers, ""))
builder.WriteString("p")
builder.WriteString(c.Proxy)
hash := builder.String()
return hash
}
@@ -87,11 +83,7 @@ func Get(options *types.Options, configuration *Configuration) (*retryabledns.Cl
} else if len(configuration.Resolvers) > 0 {
resolvers = configuration.Resolvers
}
client, err := retryabledns.NewWithOptions(retryabledns.Options{
BaseResolvers: resolvers,
MaxRetries: configuration.Retries,
Proxy: options.AliveSocksProxy,
})
client, err := retryabledns.New(resolvers, configuration.Retries)
if err != nil {
return nil, errors.Wrap(err, "could not create dns client")
}
+4 -9
View File
@@ -223,15 +223,10 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
}
func dumpResponse(event *output.InternalWrappedEvent, requestOptions *protocols.ExecutorOptions, responseBody string, input string) {
if requestOptions.Options.Debug || requestOptions.Options.DebugResponse || requestOptions.Options.StoreResponse {
msg := fmt.Sprintf("[%s] Dumped Headless response for %s\n\n", requestOptions.TemplateID, input)
if requestOptions.Options.Debug || requestOptions.Options.DebugResponse {
resp := responsehighlighter.Highlight(event.OperatorsResult, responseBody, requestOptions.Options.NoColor, false)
gologger.Debug().Msgf("%s%s", msg, resp)
}
if requestOptions.Options.StoreResponse {
requestOptions.Output.WriteStoreDebugData(input, requestOptions.TemplateID, "headless", fmt.Sprintf("%s%s", msg, responseBody))
}
cliOptions := requestOptions.Options
if cliOptions.Debug || cliOptions.DebugResponse {
highlightedResponse := responsehighlighter.Highlight(event.OperatorsResult, responseBody, cliOptions.NoColor, false)
gologger.Debug().Msgf("[%s] Dumped Headless response for %s\n\n%s", requestOptions.TemplateID, input, highlightedResponse)
}
}
-12
View File
@@ -11,7 +11,6 @@ import (
json "github.com/json-iterator/go"
"github.com/pkg/errors"
"github.com/projectdiscovery/fastdialer/fastdialer"
_ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/time"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz"
@@ -23,7 +22,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
"github.com/projectdiscovery/rawhttp"
@@ -146,7 +144,6 @@ type Request struct {
generator *generators.PayloadGenerator // optional, only enabled when using payloads
httpClient *retryablehttp.Client
rawhttpClient *rawhttp.Client
dialer *fastdialer.Dialer
// description: |
// SelfContained specifies if the request is self-contained.
@@ -351,15 +348,6 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
}
request.customHeaders = make(map[string]string)
request.httpClient = client
dialer, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{
CustomDialer: options.CustomFastdialer,
})
if err != nil {
return errors.Wrap(err, "could not get dialer")
}
request.dialer = dialer
request.options = options
for _, option := range request.options.Options.CustomHeaders {
parts := strings.SplitN(option, ":", 2)
+3 -3
View File
@@ -841,7 +841,7 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
if input.MetaInput.CustomIP != "" {
outputEvent["ip"] = input.MetaInput.CustomIP
} else {
outputEvent["ip"] = request.dialer.GetDialedIP(hostname)
outputEvent["ip"] = protocolstate.Dialer.GetDialedIP(hostname)
// try getting cname
request.addCNameIfAvailable(hostname, outputEvent)
}
@@ -1085,11 +1085,11 @@ func (request *Request) validateNFixEvent(input *contextargs.Context, gr *genera
// addCNameIfAvailable adds the cname to the event if available
func (request *Request) addCNameIfAvailable(hostname string, outputEvent map[string]interface{}) {
if request.dialer == nil {
if protocolstate.Dialer == nil {
return
}
data, err := request.dialer.GetDNSData(hostname)
data, err := protocolstate.Dialer.GetDNSData(hostname)
if err == nil {
switch len(data.CNAME) {
case 0:
+5 -3
View File
@@ -237,9 +237,7 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
}
// Create a client for the class
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{
CustomDialer: options.CustomFastdialer,
})
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{})
if err != nil {
return errors.Wrap(err, "could not get network client")
}
@@ -261,3 +259,7 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
func (request *Request) Requests() int {
return len(request.Address)
}
func (request *Request) SetDialer(dialer *fastdialer.Dialer) {
request.dialer = dialer
}
@@ -21,9 +21,7 @@ func Init(options *types.Options) error {
}
// Configuration contains the custom configuration options for a client
type Configuration struct {
CustomDialer *fastdialer.Dialer
}
type Configuration struct{}
// Hash returns the hash of the configuration to allow client pooling
func (c *Configuration) Hash() string {
@@ -32,10 +30,5 @@ func (c *Configuration) Hash() string {
// Get creates or gets a client for the protocol based on custom configuration
func Get(options *types.Options, configuration *Configuration /*TODO review unused parameters*/) (*fastdialer.Dialer, error) {
if configuration != nil && configuration.CustomDialer != nil {
return configuration.CustomDialer, nil
}
return normalClient, nil
}
+2 -6
View File
@@ -25,9 +25,9 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"
protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
errorutil "github.com/projectdiscovery/utils/errors"
@@ -64,11 +64,7 @@ func (request *Request) getOpenPorts(target *contextargs.Context) ([]string, err
errs = append(errs, err)
continue
}
if request.dialer == nil {
request.dialer, _ = networkclientpool.Get(request.options.Options, &networkclientpool.Configuration{})
}
conn, err := request.dialer.Dial(target.Context(), "tcp", addr)
conn, err := protocolstate.Dialer.Dial(target.Context(), "tcp", addr)
if err != nil {
errs = append(errs, err)
continue
-3
View File
@@ -5,7 +5,6 @@ import (
"encoding/base64"
"sync/atomic"
"github.com/projectdiscovery/fastdialer/fastdialer"
"github.com/projectdiscovery/ratelimit"
mapsutil "github.com/projectdiscovery/utils/maps"
stringsutil "github.com/projectdiscovery/utils/strings"
@@ -133,8 +132,6 @@ type ExecutorOptions struct {
ExportReqURLPattern bool
// GlobalMatchers is the storage for global matchers with http passive templates
GlobalMatchers *globalmatchers.Storage
// CustomFastdialer is a fastdialer dialer instance
CustomFastdialer *fastdialer.Dialer
}
// todo: centralizing components is not feasible with current clogged architecture
+1 -3
View File
@@ -115,9 +115,7 @@ func (request *Request) IsClusterable() bool {
func (request *Request) Compile(options *protocols.ExecutorOptions) error {
request.options = options
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{
CustomDialer: options.CustomFastdialer,
})
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{})
if err != nil {
return errorutil.NewWithTag("ssl", "could not get network client").Wrap(err)
}
+1 -3
View File
@@ -100,9 +100,7 @@ const (
func (request *Request) Compile(options *protocols.ExecutorOptions) error {
request.options = options
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{
CustomDialer: options.CustomFastdialer,
})
client, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{})
if err != nil {
return errors.Wrap(err, "could not get network client")
}
+12 -4
View File
@@ -1,13 +1,13 @@
package generic
import (
"strings"
"sync/atomic"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/utils"
mapsutil "github.com/projectdiscovery/utils/maps"
)
@@ -64,9 +64,17 @@ func (g *Generic) ExecuteWithResults(ctx *scan.ScanContext) error {
// ideally this should never happen since protocol exits on error and callback is not called
return
}
utils.FillPreviousEvent(req.GetID(), event, previous)
ID := req.GetID()
if ID != "" {
builder := &strings.Builder{}
for k, v := range event.InternalEvent {
builder.WriteString(ID)
builder.WriteString("_")
builder.WriteString(k)
_ = previous.Set(builder.String(), v)
builder.Reset()
}
}
if event.HasOperatorResult() {
g.results.CompareAndSwap(false, true)
}
+54 -2
View File
@@ -2,6 +2,8 @@ package multiproto
import (
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
@@ -9,11 +11,16 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/utils"
mapsutil "github.com/projectdiscovery/utils/maps"
stringsutil "github.com/projectdiscovery/utils/strings"
)
var builderPool = sync.Pool{
New: func() any {
return &strings.Builder{}
},
}
// Mutliprotocol is a template executer engine that executes multiple protocols
// with logic in between
type MultiProtocol struct {
@@ -64,6 +71,32 @@ func (m *MultiProtocol) ExecuteWithResults(ctx *scan.ScanContext) error {
previous := mapsutil.NewSyncLockMap[string, any]()
// This map contains a list of keys to explicitly include for storage in the `previous`
// event map. This is a security and performance measure to prevent memory exhaustion.
// Only small, scalar values should be included here. If a user needs a larger value
// from a previous request (like a body or header), they must use an `extractor` with `internal: true`.
var includeInPrevious = map[string]struct{}{
// Generic
"host": {},
"port": {},
"path": {},
"scheme": {},
"url": {},
"ip": {},
"type": {},
"matched-at": {},
// HTTP
"status_code": {},
"content_length": {},
"content_type": {},
// DNS
"rcode": {},
"question": {},
"qtype": {},
}
// template context: contains values extracted using `internal` extractor from previous protocols
// these values are extracted from each protocol in queue and are passed to next protocol in queue
// instead of adding seperator field to handle such cases these values are appended to `dynamicValues` (which are meant to be used in workflows)
@@ -90,7 +123,26 @@ func (m *MultiProtocol) ExecuteWithResults(ctx *scan.ScanContext) error {
return
}
utils.FillPreviousEvent(req.GetID(), event, previous)
ID := req.GetID()
if ID != "" {
builder := builderPool.Get().(*strings.Builder)
builder.Reset()
defer builderPool.Put(builder)
for k, v := range event.InternalEvent {
// Only store keys that are explicitly on the include list.
// This is a critical optimization to prevent memory exhaustion.
if _, included := includeInPrevious[k]; !included {
continue
}
builder.WriteString(ID)
builder.WriteString("_")
builder.WriteString(k)
_ = previous.Set(builder.String(), v)
builder.Reset()
}
}
// log event and generate result for the event
ctx.LogEvent(event)
+359
View File
@@ -2,8 +2,11 @@ package multiproto_test
import (
"context"
"fmt"
"log"
"os"
"runtime"
"sync"
"testing"
"time"
@@ -23,6 +26,57 @@ import (
var executerOpts protocols.ExecutorOptions
// MemorySnapshot represents a point-in-time memory measurement
type MemorySnapshot struct {
Timestamp time.Time
Alloc uint64
TotalAlloc uint64
Sys uint64
NumGC uint32
}
// TakeMemorySnapshot captures current memory statistics
func TakeMemorySnapshot() *MemorySnapshot {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return &MemorySnapshot{
Timestamp: time.Now(),
Alloc: stats.Alloc,
TotalAlloc: stats.TotalAlloc,
Sys: stats.Sys,
NumGC: stats.NumGC,
}
}
// MemoryDiff represents the difference between two memory snapshots
type MemoryDiff struct {
Duration time.Duration
AllocDiff int64
TotalDiff int64
SysDiff int64
GCDiff int64
}
// Compare compares two memory snapshots and returns the difference
func (s *MemorySnapshot) Compare(other *MemorySnapshot) *MemoryDiff {
return &MemoryDiff{
Duration: other.Timestamp.Sub(s.Timestamp),
AllocDiff: int64(other.Alloc) - int64(s.Alloc),
TotalDiff: int64(other.TotalAlloc) - int64(s.TotalAlloc),
SysDiff: int64(other.Sys) - int64(s.Sys),
GCDiff: int64(other.NumGC) - int64(s.NumGC),
}
}
// String returns a formatted string representation of the memory difference
func (d *MemoryDiff) String() string {
return fmt.Sprintf(
"Duration: %v, Alloc: %+d bytes (%+.2f MB), Total: %+d bytes, Sys: %+d bytes, GC: %+d",
d.Duration, d.AllocDiff, float64(d.AllocDiff)/1024/1024, d.TotalDiff, d.SysDiff, d.GCDiff,
)
}
func setup() {
options := testutils.DefaultOptions
testutils.Init(options)
@@ -47,6 +101,311 @@ func setup() {
executerOpts.WorkflowLoader = workflowLoader
}
// Helper function to create a test template execution
func executeTemplate(templatePath string, target string) error {
template, err := templates.Parse(templatePath, nil, executerOpts)
if err != nil {
return fmt.Errorf("could not parse template: %w", err)
}
err = template.Executer.Compile()
if err != nil {
return fmt.Errorf("could not compile template: %w", err)
}
input := contextargs.NewWithInput(context.Background(), target)
ctx := scan.NewScanContext(context.Background(), input)
_, err = template.Executer.Execute(ctx)
if err != nil {
return fmt.Errorf("could not execute template: %w", err)
}
return nil
}
// TestMemoryLeakDetection tests for memory leaks in multiprotocol execution
func TestMemoryLeakDetection(t *testing.T) {
tests := []struct {
name string
templatePath string
target string
numExecutions int
maxMemoryMB int
description string
}{
{
name: "SmallScale_DynamicExtractor",
templatePath: "testcases/multiprotodynamic.yaml",
target: "http://scanme.sh",
numExecutions: 10,
maxMemoryMB: 10,
description: "Basic memory leak detection with dynamic extractor",
},
{
name: "MediumScale_ProtoPrefix",
templatePath: "testcases/multiprotowithprefix.yaml",
target: "https://cloud.projectdiscovery.io/sign-in",
numExecutions: 50,
maxMemoryMB: 25,
description: "Medium scale execution with protocol prefix",
},
{
name: "LargeScale_Mixed",
templatePath: "testcases/multiprotodynamic.yaml",
target: "http://scanme.sh",
numExecutions: 100,
maxMemoryMB: 50,
description: "Large scale execution for memory leak detection",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Force GC and get baseline
runtime.GC()
runtime.GC() // Call twice to ensure cleanup
time.Sleep(100 * time.Millisecond) // Allow GC to complete
baseline := TakeMemorySnapshot()
t.Logf("Baseline memory: Alloc=%d bytes (%.2f MB)",
baseline.Alloc, float64(baseline.Alloc)/1024/1024)
// Execute multiple times
for i := 0; i < tt.numExecutions; i++ {
err := executeTemplate(tt.templatePath, tt.target)
if err != nil {
t.Logf("Execution %d failed (continuing): %v", i, err)
continue
}
// Periodic memory checks
if i%10 == 0 && i > 0 {
runtime.GC()
current := TakeMemorySnapshot()
diff := baseline.Compare(current)
memoryMB := float64(diff.AllocDiff) / 1024 / 1024
t.Logf("Execution %d: %s", i, diff.String())
if memoryMB > float64(tt.maxMemoryMB) {
t.Errorf("Memory usage exceeded limit: %.2f MB > %d MB",
memoryMB, tt.maxMemoryMB)
}
}
}
// Final memory check
runtime.GC()
runtime.GC()
time.Sleep(100 * time.Millisecond)
final := TakeMemorySnapshot()
finalDiff := baseline.Compare(final)
finalMemoryMB := float64(finalDiff.AllocDiff) / 1024 / 1024
t.Logf("Final memory analysis: %s", finalDiff.String())
if finalMemoryMB > float64(tt.maxMemoryMB) {
t.Errorf("Final memory usage exceeded limit: %.2f MB > %d MB",
finalMemoryMB, tt.maxMemoryMB)
} else {
t.Logf("✅ Memory usage within acceptable limits: %.2f MB <= %d MB",
finalMemoryMB, tt.maxMemoryMB)
}
})
}
}
// TestTemplateContextCleanup verifies that template contexts are properly cleaned up
func TestTemplateContextCleanup(t *testing.T) {
// Create executor options with template context store
opts := executerOpts
opts.CreateTemplateCtxStore()
template, err := templates.Parse("testcases/multiprotodynamic.yaml", nil, opts)
require.NoError(t, err, "could not parse template")
err = template.Executer.Compile()
require.NoError(t, err, "could not compile template")
// Create multiple contexts and execute
numContexts := 20
inputs := make([]*contextargs.Context, numContexts)
for i := 0; i < numContexts; i++ {
target := fmt.Sprintf("http://test%d.example.com", i)
inputs[i] = contextargs.NewWithInput(context.Background(), target)
ctx := scan.NewScanContext(context.Background(), inputs[i])
_, err := template.Executer.Execute(ctx)
if err != nil {
t.Logf("Execution %d failed (continuing): %v", i, err)
}
}
// Verify contexts exist
contextCount := 0
for _, input := range inputs {
if opts.HasTemplateCtx(input.MetaInput) {
contextCount++
}
}
t.Logf("Active template contexts: %d", contextCount)
// Clean up contexts manually (simulating proper cleanup)
for _, input := range inputs {
opts.RemoveTemplateCtx(input.MetaInput)
}
// Verify cleanup
remainingContexts := 0
for _, input := range inputs {
if opts.HasTemplateCtx(input.MetaInput) {
remainingContexts++
}
}
if remainingContexts > 0 {
t.Errorf("Template contexts not properly cleaned up: %d remaining", remainingContexts)
} else {
t.Logf("✅ All template contexts properly cleaned up")
}
}
// TestConcurrentExecution tests memory usage under concurrent execution
func TestConcurrentExecution(t *testing.T) {
if testing.Short() {
t.Skip("Skipping concurrent test in short mode")
}
const (
numGoroutines = 5
numExecutions = 20
maxMemoryMB = 100
)
baseline := TakeMemorySnapshot()
var wg sync.WaitGroup
errChan := make(chan error, numGoroutines)
// Start concurrent executions
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < numExecutions; j++ {
target := fmt.Sprintf("http://test%d-%d.example.com", id, j)
err := executeTemplate("testcases/multiprotodynamic.yaml", target)
if err != nil {
t.Logf("Goroutine %d execution %d failed: %v", id, j, err)
continue
}
// Periodic memory check
if j%5 == 0 {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
memoryMB := float64(stats.Alloc) / 1024 / 1024
if memoryMB > maxMemoryMB {
errChan <- fmt.Errorf("goroutine %d: memory exceeded: %.2f MB", id, memoryMB)
return
}
}
}
}(i)
}
// Wait for completion with timeout
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case err := <-errChan:
t.Fatal(err)
case <-done:
runtime.GC()
final := TakeMemorySnapshot()
diff := baseline.Compare(final)
t.Logf("✅ Concurrent execution completed successfully: %s", diff.String())
case <-time.After(2 * time.Minute):
t.Fatal("Test timed out")
}
}
// BenchmarkMultiProtocolExecution benchmarks multiprotocol execution performance
func BenchmarkMultiProtocolExecution(b *testing.B) {
benchmarks := []struct {
name string
templatePath string
target string
}{
{
name: "DynamicExtractor",
templatePath: "testcases/multiprotodynamic.yaml",
target: "http://scanme.sh",
},
{
name: "ProtoPrefix",
templatePath: "testcases/multiprotowithprefix.yaml",
target: "https://example.com",
},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
template, err := templates.Parse(bm.templatePath, nil, executerOpts)
if err != nil {
b.Fatalf("could not parse template: %v", err)
}
err = template.Executer.Compile()
if err != nil {
b.Fatalf("could not compile template: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
input := contextargs.NewWithInput(context.Background(), bm.target)
ctx := scan.NewScanContext(context.Background(), input)
_, err := template.Executer.Execute(ctx)
if err != nil {
b.Logf("Execution failed (continuing): %v", err)
}
}
})
}
}
// BenchmarkMemoryAllocation specifically benchmarks memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
template, err := templates.Parse("testcases/multiprotodynamic.yaml", nil, executerOpts)
if err != nil {
b.Fatalf("could not parse template: %v", err)
}
err = template.Executer.Compile()
if err != nil {
b.Fatalf("could not compile template: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
input := contextargs.NewWithInput(context.Background(), "http://scanme.sh")
ctx := scan.NewScanContext(context.Background(), input)
_, _ = template.Executer.Execute(ctx)
}
}
func TestMultiProtoWithDynamicExtractor(t *testing.T) {
Template, err := templates.Parse("testcases/multiprotodynamic.yaml", nil, executerOpts)
require.Nil(t, err, "could not parse template")
-34
View File
@@ -1,34 +0,0 @@
package utils
import (
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
mapsutil "github.com/projectdiscovery/utils/maps"
)
// FillPreviousEvent is a helper function to get the previous event from the event
// without leading to duplicate prefixes
func FillPreviousEvent(reqID string, event *output.InternalWrappedEvent, previous *mapsutil.SyncLockMap[string, any]) {
if reqID == "" {
return
}
for k, v := range event.InternalEvent {
if _, ok := previous.Get(k); ok {
continue
}
if strings.HasPrefix(k, reqID+"_") {
continue
}
var builder strings.Builder
builder.WriteString(reqID)
builder.WriteString("_")
builder.WriteString(k)
_ = previous.Set(builder.String(), v)
}
}