fix(lib): scans didn't stop on ctx cancellation (#6310)

* fix(lib): scans didn't stop on ctx cancellation

Signed-off-by: Dwi Siswanto <git@dw1.io>

* Update lib/sdk_test.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(lib): wait resources to be released b4 return

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Dwi Siswanto 2025-07-09 01:04:16 +07:00 committed by GitHub
parent 3991cc6ec1
commit 7e2ec686ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 6 deletions

View File

@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"context" "context"
"io" "io"
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider" "github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog" "github.com/projectdiscovery/nuclei/v3/pkg/catalog"
@ -64,6 +65,7 @@ type NucleiEngine struct {
templatesLoaded bool templatesLoaded bool
// unexported core fields // unexported core fields
ctx context.Context
interactshClient *interactsh.Client interactshClient *interactsh.Client
catalog catalog.Catalog catalog catalog.Catalog
rateLimiter *ratelimit.Limiter rateLimiter *ratelimit.Limiter
@ -246,9 +248,9 @@ func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...f
} }
filtered := []func(event *output.ResultEvent){} filtered := []func(event *output.ResultEvent){}
for _, callback := range callback { for _, cb := range callback {
if callback != nil { if cb != nil {
filtered = append(filtered, callback) filtered = append(filtered, cb)
} }
} }
e.resultCallbacks = append(e.resultCallbacks, filtered...) e.resultCallbacks = append(e.resultCallbacks, filtered...)
@ -258,15 +260,32 @@ func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...f
return ErrNoTemplatesAvailable return ErrNoTemplatesAvailable
} }
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = e.engine.ExecuteScanWithOpts(ctx, templatesAndWorkflows, e.inputProvider, false) _ = e.engine.ExecuteScanWithOpts(ctx, templatesAndWorkflows, e.inputProvider, false)
defer e.engine.WorkPool().Wait() }()
// wait for context to be cancelled
select {
case <-ctx.Done():
<-wait(&wg) // wait for scan to finish
return ctx.Err()
case <-wait(&wg):
// scan finished
}
return nil return nil
} }
// ExecuteWithCallback is same as ExecuteCallbackWithCtx but with default context // ExecuteWithCallback is same as ExecuteCallbackWithCtx but with default context
// Note this is deprecated and will be removed in future major release // Note this is deprecated and will be removed in future major release
func (e *NucleiEngine) ExecuteWithCallback(callback ...func(event *output.ResultEvent)) error { func (e *NucleiEngine) ExecuteWithCallback(callback ...func(event *output.ResultEvent)) error {
return e.ExecuteCallbackWithCtx(context.Background(), callback...) ctx := context.Background()
if e.ctx != nil {
ctx = e.ctx
}
return e.ExecuteCallbackWithCtx(ctx, callback...)
} }
// Options return nuclei Type Options // Options return nuclei Type Options
@ -290,6 +309,7 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
e := &NucleiEngine{ e := &NucleiEngine{
opts: types.DefaultOptions(), opts: types.DefaultOptions(),
mode: singleInstance, mode: singleInstance,
ctx: ctx,
} }
for _, option := range options { for _, option := range options {
if err := option(e); err != nil { if err := option(e); err != nil {
@ -306,3 +326,13 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
func NewNucleiEngine(options ...NucleiSDKOptions) (*NucleiEngine, error) { func NewNucleiEngine(options ...NucleiSDKOptions) (*NucleiEngine, error) {
return NewNucleiEngineCtx(context.Background(), options...) return NewNucleiEngineCtx(context.Background(), options...)
} }
// wait for a waitgroup to finish
func wait(wg *sync.WaitGroup) <-chan struct{} {
ch := make(chan struct{})
go func() {
defer close(ch)
wg.Wait()
}()
return ch
}

37
lib/sdk_test.go Normal file
View File

@ -0,0 +1,37 @@
package nuclei_test
import (
"context"
"log"
"testing"
"time"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/stretchr/testify/require"
)
func TestContextCancelNucleiEngine(t *testing.T) {
// create nuclei engine with options
ctx, cancel := context.WithCancel(context.Background())
ne, err := nuclei.NewNucleiEngineCtx(ctx,
nuclei.WithTemplateFilters(nuclei.TemplateFilters{Tags: []string{"oast"}}),
nuclei.EnableStatsWithOpts(nuclei.StatsOptions{MetricServerPort: 0}),
)
require.NoError(t, err, "could not create nuclei engine")
go func() {
time.Sleep(time.Second * 2)
cancel()
log.Println("Test: context cancelled")
}()
// load targets and optionally probe non http/https targets
ne.LoadTargets([]string{"http://honey.scanme.sh"}, false)
// when callback is nil it nuclei will print JSON output to stdout
err = ne.ExecuteWithCallback(nil)
if err != nil {
// we expect a context cancellation error
require.ErrorIs(t, err, context.Canceled, "was expecting context cancellation error")
}
defer ne.Close()
}