mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 10:10:18 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
507a4feaee |
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user