mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 17:39:09 -10:00
259 lines
9.5 KiB
Go
259 lines
9.5 KiB
Go
package nuclei
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/logrusorgru/aurora/v4"
|
|
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/core"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
|
"github.com/projectdiscovery/utils/errkit"
|
|
)
|
|
|
|
// unsafeOptions are those nuclei objects/instances/types
|
|
// that are required to run nuclei engine but are not thread safe
|
|
// hence they are ephemeral and are created on every ExecuteNucleiWithOpts invocation
|
|
// in ThreadSafeNucleiEngine
|
|
type unsafeOptions struct {
|
|
executerOpts *protocols.ExecutorOptions
|
|
}
|
|
|
|
// createEphemeralObjects creates ephemeral nuclei objects/instances/types.
|
|
// outputWriter overrides the base engine writer when non-nil (used for per-call callbacks).
|
|
func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types.Options, outputWriter output.Writer) (*unsafeOptions, error) {
|
|
if outputWriter == nil {
|
|
outputWriter = base.customWriter
|
|
}
|
|
u := &unsafeOptions{}
|
|
u.executerOpts = &protocols.ExecutorOptions{
|
|
Output: outputWriter,
|
|
Options: opts,
|
|
Progress: base.customProgress,
|
|
Catalog: base.catalog,
|
|
IssuesClient: base.rc,
|
|
RateLimiter: base.rateLimiter,
|
|
Interactsh: base.interactshClient,
|
|
Colorizer: aurora.New(aurora.WithColors(true)),
|
|
ResumeCfg: types.NewResumeCfg(),
|
|
Parser: base.parser,
|
|
Browser: base.browserInstance,
|
|
DoNotCache: opts.DoNotCacheTemplates,
|
|
}
|
|
if base.interactshClient != nil {
|
|
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
|
|
}
|
|
if opts.ShouldUseHostError() && base.hostErrCache != nil {
|
|
u.executerOpts.HostErrorsCache = base.hostErrCache
|
|
}
|
|
if opts.RateLimitMinute > 0 {
|
|
opts.RateLimit = opts.RateLimitMinute
|
|
opts.RateLimitDuration = time.Minute
|
|
}
|
|
if opts.RateLimit > 0 && opts.RateLimitDuration == 0 {
|
|
opts.RateLimitDuration = time.Second
|
|
}
|
|
u.executerOpts.RateLimiter = utils.GetRateLimiter(ctx, opts.RateLimit, opts.RateLimitDuration)
|
|
return u, nil
|
|
}
|
|
|
|
// restoreBaseExcludeTags re-adds any of the engine's baseline excluded tags that
|
|
// a per-execution option dropped, appending only what is missing so the result
|
|
// is order-stable and repeated application is a no-op.
|
|
//
|
|
// The current ExcludeTags slice is cloned before merging so a caller-owned
|
|
// backing array (e.g. a reused WithTemplateFilters ExcludeTags value with spare
|
|
// capacity) is never mutated or shared across concurrent executions.
|
|
func restoreBaseExcludeTags(base []string, opts *types.Options) {
|
|
if len(base) == 0 {
|
|
return
|
|
}
|
|
merged := append([]string(nil), opts.ExcludeTags...)
|
|
present := make(map[string]struct{}, len(merged)+len(base))
|
|
for _, tag := range merged {
|
|
present[tag] = struct{}{}
|
|
}
|
|
for _, tag := range base {
|
|
if _, ok := present[tag]; ok {
|
|
continue
|
|
}
|
|
merged = append(merged, tag)
|
|
present[tag] = struct{}{}
|
|
}
|
|
opts.ExcludeTags = merged
|
|
}
|
|
|
|
// resolveEphemeralOutput combines the base/global writer with any per-call result callbacks.
|
|
// Global callbacks keep working; per-call callbacks are isolated to this execution.
|
|
func resolveEphemeralOutput(base, call *NucleiEngine) output.Writer {
|
|
out := base.customWriter
|
|
if call == nil || len(call.resultCallbacks) == 0 {
|
|
return out
|
|
}
|
|
|
|
callbacks := append([]func(event *output.ResultEvent){}, call.resultCallbacks...)
|
|
callWriter := testutils.NewMockOutputWriter(call.opts.OmitTemplate)
|
|
callWriter.WriteCallback = func(event *output.ResultEvent) {
|
|
for _, cb := range callbacks {
|
|
if cb != nil {
|
|
cb(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
if out == nil {
|
|
return callWriter
|
|
}
|
|
return output.NewMultiWriter(out, callWriter)
|
|
}
|
|
|
|
// closeEphemeralObjects closes all resources used by ephemeral nuclei objects/instances/types
|
|
func closeEphemeralObjects(u *unsafeOptions) {
|
|
if u.executerOpts.InteractshScope != nil {
|
|
u.executerOpts.InteractshScope.Close()
|
|
}
|
|
if u.executerOpts.RateLimiter != nil {
|
|
u.executerOpts.RateLimiter.Stop()
|
|
}
|
|
// dereference all objects that were inherited from base nuclei engine
|
|
// since these are meant to be closed globally by base nuclei engine
|
|
u.executerOpts.Output = nil
|
|
u.executerOpts.IssuesClient = nil
|
|
u.executerOpts.Interactsh = nil
|
|
u.executerOpts.InteractshScope = nil
|
|
u.executerOpts.HostErrorsCache = nil
|
|
u.executerOpts.Progress = nil
|
|
u.executerOpts.Catalog = nil
|
|
u.executerOpts.Parser = nil
|
|
}
|
|
|
|
// ThreadSafeNucleiEngine is a tweaked version of nuclei.Engine whose methods are thread-safe
|
|
// and can be used concurrently. Non-thread-safe methods start with Global prefix
|
|
type ThreadSafeNucleiEngine struct {
|
|
eng *NucleiEngine
|
|
}
|
|
|
|
// NewThreadSafeNucleiEngine creates a new nuclei engine with given options
|
|
// whose methods are thread-safe and can be used concurrently
|
|
// Note: Non-thread-safe methods start with Global prefix
|
|
func NewThreadSafeNucleiEngineCtx(ctx context.Context, opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) {
|
|
defaultOptions := types.DefaultOptions()
|
|
e := &NucleiEngine{
|
|
opts: defaultOptions,
|
|
mode: threadSafe,
|
|
ctx: ctx,
|
|
Logger: defaultOptions.Logger,
|
|
}
|
|
for _, option := range opts {
|
|
if err := option(e); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := e.init(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return &ThreadSafeNucleiEngine{eng: e}, nil
|
|
}
|
|
|
|
// Deprecated: use NewThreadSafeNucleiEngineCtx instead
|
|
func NewThreadSafeNucleiEngine(opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) {
|
|
return NewThreadSafeNucleiEngineCtx(context.Background(), opts...)
|
|
}
|
|
|
|
// GlobalLoadAllTemplates loads all templates from nuclei-templates repo
|
|
// This method will load all templates based on filters given at the time of nuclei engine creation in opts
|
|
func (e *ThreadSafeNucleiEngine) GlobalLoadAllTemplates() error {
|
|
return e.eng.LoadAllTemplates()
|
|
}
|
|
|
|
// GlobalResultCallback sets a callback function which will be called for each result
|
|
// across all ExecuteNucleiWithOpts invocations. For a callback scoped to a single
|
|
// execution, pass WithResultCallback in that call's opts instead.
|
|
func (e *ThreadSafeNucleiEngine) GlobalResultCallback(callback func(event *output.ResultEvent)) {
|
|
e.eng.resultCallbacks = []func(*output.ResultEvent){callback}
|
|
}
|
|
|
|
// ExecuteNucleiWithOptsCtx executes templates on targets and can be called concurrently.
|
|
// Pass WithResultCallback in opts for a per-execution result callback (combined with any
|
|
// GlobalResultCallback). Not all options are thread-safe.
|
|
func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, targets []string, opts ...NucleiSDKOptions) error {
|
|
baseOpts := e.eng.opts.Copy()
|
|
tmpEngine := &NucleiEngine{opts: baseOpts, mode: threadSafe}
|
|
for _, option := range opts {
|
|
if err := option(tmpEngine); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Per-execution options land on a COPY of the engine's options, so an option
|
|
// that assigns a whole filter set clears every field it does not name —
|
|
// WithTemplateFilters does exactly that. ExcludeTags is one of those fields,
|
|
// and it carries the .nuclei-ignore deny-list that applyRequiredDefaults
|
|
// installed at construction; nothing re-reads the ignore file afterwards, so
|
|
// the exclusions would be lost for this execution and templates tagged dos,
|
|
// bruteforce, fuzz, local or txt-service would run.
|
|
//
|
|
// Restore the engine's baseline exclusions so a per-execution filter can add
|
|
// to them but never silently discard them. IncludeTags remains the explicit
|
|
// per-tag override for callers who do want an ignored template to run.
|
|
restoreBaseExcludeTags(e.eng.opts.ExcludeTags, tmpEngine.opts)
|
|
|
|
// create ephemeral nuclei objects/instances/types using base nuclei engine
|
|
unsafeOpts, err := createEphemeralObjects(ctx, e.eng, tmpEngine.opts, resolveEphemeralOutput(e.eng, tmpEngine))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// cleanup and stop all resources
|
|
defer closeEphemeralObjects(unsafeOpts)
|
|
|
|
// load templates
|
|
workflowLoader, err := workflow.NewLoader(unsafeOpts.executerOpts)
|
|
if err != nil {
|
|
return errkit.Wrapf(err, "Could not create workflow loader: %s", err)
|
|
}
|
|
unsafeOpts.executerOpts.WorkflowLoader = workflowLoader
|
|
|
|
loaderConfig := loader.NewConfig(tmpEngine.opts, e.eng.catalog, unsafeOpts.executerOpts)
|
|
loaderConfig.MetadataIndex = e.eng.getMetadataIndex()
|
|
store, err := loader.New(loaderConfig)
|
|
if err != nil {
|
|
return errkit.Wrapf(err, "Could not create loader client: %s", err)
|
|
}
|
|
if err := store.Load(); err != nil {
|
|
return errkit.Wrapf(err, "Could not load templates: %s", err)
|
|
}
|
|
|
|
inputProvider := provider.NewSimpleInputProviderWithUrls(e.eng.opts.ExecutionId, targets...)
|
|
|
|
if len(store.Templates()) == 0 && len(store.Workflows()) == 0 {
|
|
return ErrNoTemplatesAvailable
|
|
}
|
|
if inputProvider.Count() == 0 {
|
|
return ErrNoTargetsAvailable
|
|
}
|
|
|
|
engine := core.New(tmpEngine.opts)
|
|
engine.SetExecuterOptions(unsafeOpts.executerOpts)
|
|
|
|
_ = engine.ExecuteScanWithOpts(ctx, store.Templates(), inputProvider, false)
|
|
|
|
engine.WorkPool().Wait()
|
|
return nil
|
|
}
|
|
|
|
// ExecuteNucleiWithOpts is same as ExecuteNucleiWithOptsCtx but with default context
|
|
// This is a placeholder and will be deprecated in future major release
|
|
func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOpts(targets []string, opts ...NucleiSDKOptions) error {
|
|
return e.ExecuteNucleiWithOptsCtx(context.Background(), targets, opts...)
|
|
}
|
|
|
|
// Close all resources used by nuclei engine
|
|
func (e *ThreadSafeNucleiEngine) Close() {
|
|
e.eng.Close()
|
|
}
|