package nuclei
import (
"bufio"
"bytes"
"context"
"io"
"sync"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
providerTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/ratelimit"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/projectdiscovery/utils/errkit"
"github.com/rs/xid"
)
type NucleiSDKOptions func(e *NucleiEngine) error
var (
ErrNotImplemented = errkit.New("Not implemented")
ErrNoTemplatesAvailable = errkit.New("No templates available")
ErrNoTargetsAvailable = errkit.New("No targets available")
ErrOptionsNotSupported = errkit.New("Option not supported in thread safe mode")
)
type engineMode uint
const (
singleInstance engineMode = iota
threadSafe
)
type NucleiEngine struct {
resultCallbacks []func(event *output.ResultEvent)
onFailureCallback func(event *output.InternalEvent)
disableTemplatesAutoUpgrade bool
enableStats bool
onUpdateAvailableCallback func(newVersion string)
templatesLoaded bool
ctx context.Context
interactshClient *interactsh.Client
catalog catalog.Catalog
rateLimiter *ratelimit.Limiter
store *loader.Store
httpxClient providerTypes.InputLivenessProbe
inputProvider provider.InputProvider
engine *core.Engine
mode engineMode
browserInstance *engine.Browser
httpClient *retryablehttp.Client
parser *templates.Parser
authprovider authprovider.AuthProvider
opts *types.Options
interactshOpts *interactsh.Options
hostErrCache *hosterrorscache.Cache
customWriter output.Writer
customProgress progress.Progress
rc reporting.Client
executerOpts *protocols.ExecutorOptions
Logger *gologger.Logger
}
func (e *NucleiEngine) LoadAllTemplates() error {
workflowLoader, err := workflow.NewLoader(e.executerOpts)
if err != nil {
return errkit.Wrapf(err, "Could not create workflow loader: %s", err)
}
e.executerOpts.WorkflowLoader = workflowLoader
e.store, err = loader.New(loader.NewConfig(e.opts, e.catalog, e.executerOpts))
if err != nil {
return errkit.Wrapf(err, "Could not create loader client: %s", err)
}
e.store.Load()
e.templatesLoaded = true
return nil
}
func (e *NucleiEngine) GetTemplates() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
return e.store.Templates()
}
func (e *NucleiEngine) GetWorkflows() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
return e.store.Workflows()
}
func (e *NucleiEngine) LoadTargets(targets []string, probeNonHttp bool) {
for _, target := range targets {
if probeNonHttp {
_ = e.inputProvider.SetWithProbe(e.opts.ExecutionId, target, e.httpxClient)
} else {
e.inputProvider.Set(e.opts.ExecutionId, target)
}
}
}
func (e *NucleiEngine) LoadTargetsFromReader(reader io.Reader, probeNonHttp bool) {
buff := bufio.NewScanner(reader)
for buff.Scan() {
if probeNonHttp {
_ = e.inputProvider.SetWithProbe(e.opts.ExecutionId, buff.Text(), e.httpxClient)
} else {
e.inputProvider.Set(e.opts.ExecutionId, buff.Text())
}
}
}
func (e *NucleiEngine) LoadTargetsWithHttpData(filePath string, filemode string) error {
e.opts.TargetsFilePath = filePath
e.opts.InputFileMode = filemode
httpProvider, err := provider.NewInputProvider(provider.InputOptions{Options: e.opts})
if err != nil {
e.opts.TargetsFilePath = ""
e.opts.InputFileMode = ""
return err
}
e.inputProvider = httpProvider
return nil
}
func (e *NucleiEngine) GetExecuterOptions() *protocols.ExecutorOptions {
return e.executerOpts
}
func (e *NucleiEngine) ParseTemplate(data []byte) (*templates.Template, error) {
return templates.ParseTemplateFromReader(bytes.NewReader(data), nil, e.executerOpts)
}
func (e *NucleiEngine) SignTemplate(tmplSigner *signer.TemplateSigner, data []byte) ([]byte, error) {
tmpl, err := e.ParseTemplate(data)
if err != nil {
return data, err
}
if tmpl.Verified {
return data, nil
}
if len(tmpl.Workflows) > 0 {
return data, templates.ErrNotATemplate
}
signatureData, err := tmplSigner.Sign(data, tmpl)
if err != nil {
return data, err
}
_, content := signer.ExtractSignatureAndContent(data)
buff := bytes.NewBuffer(content)
buff.WriteString("\n" + signatureData)
return buff.Bytes(), err
}
func (e *NucleiEngine) closeInternal() {
if e.interactshClient != nil {
e.interactshClient.Close()
}
if e.rc != nil {
e.rc.Close()
}
if e.customWriter != nil {
e.customWriter.Close()
}
if e.customProgress != nil {
e.customProgress.Stop()
}
if e.hostErrCache != nil {
e.hostErrCache.Close()
}
if e.executerOpts.RateLimiter != nil {
e.executerOpts.RateLimiter.Stop()
}
if e.rateLimiter != nil {
e.rateLimiter.Stop()
}
if e.inputProvider != nil {
e.inputProvider.Close()
}
if e.browserInstance != nil {
e.browserInstance.Close()
}
if e.httpxClient != nil {
_ = e.httpxClient.Close()
}
}
func (e *NucleiEngine) Close() {
e.closeInternal()
protocolinit.Close(e.opts.ExecutionId)
}
func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...func(event *output.ResultEvent)) error {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
if len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0 {
return ErrNoTemplatesAvailable
}
if e.inputProvider.Count() == 0 {
return ErrNoTargetsAvailable
}
filtered := []func(event *output.ResultEvent){}
for _, cb := range callback {
if cb != nil {
filtered = append(filtered, cb)
}
}
e.resultCallbacks = append(e.resultCallbacks, filtered...)
templatesAndWorkflows := append(e.store.Templates(), e.store.Workflows()...)
if len(templatesAndWorkflows) == 0 {
return ErrNoTemplatesAvailable
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = e.engine.ExecuteScanWithOpts(ctx, templatesAndWorkflows, e.inputProvider, false)
}()
select {
case <-ctx.Done():
<-wait(&wg)
return ctx.Err()
case <-wait(&wg):
}
return nil
}
func (e *NucleiEngine) ExecuteWithCallback(callback ...func(event *output.ResultEvent)) error {
ctx := context.Background()
if e.ctx != nil {
ctx = e.ctx
}
return e.ExecuteCallbackWithCtx(ctx, callback...)
}
func (e *NucleiEngine) Options() *types.Options {
return e.opts
}
func (e *NucleiEngine) Engine() *core.Engine {
return e.engine
}
func (e *NucleiEngine) Store() *loader.Store {
return e.store
}
func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*NucleiEngine, error) {
defaultOptions := types.DefaultOptions()
defaultOptions.ExecutionId = xid.New().String()
e := &NucleiEngine{
opts: defaultOptions,
mode: singleInstance,
ctx: ctx,
}
for _, option := range options {
if err := option(e); err != nil {
return nil, err
}
}
if err := e.init(ctx); err != nil {
return nil, err
}
return e, nil
}
func NewNucleiEngine(options ...NucleiSDKOptions) (*NucleiEngine, error) {
return NewNucleiEngineCtx(context.Background(), options...)
}
func (e *NucleiEngine) GetParser() *templates.Parser {
return e.parser
}
func wait(wg *sync.WaitGroup) <-chan struct{} {
ch := make(chan struct{})
go func() {
defer close(ch)
wg.Wait()
}()
return ch
}