mirror of https://github.com/daffainfo/nuclei.git
SDK: abstracted and minimal nuclei v3 sdk (#4104)
* new sdk progress * nuclei v3 new sdk/library * fix TestActionGetResource broken link * fix clistats + clustering and more * fix lint error * fix missing ticker * update advanced library usage example * fix integration tests * misc update * add utm_source and fix lint error --------- Co-authored-by: sandeep <8293321+ehsandeep@users.noreply.github.com>dev
parent
f7fe99f806
commit
2d317884b5
|
@ -56,6 +56,10 @@ jobs:
|
||||||
run: go run -race . -l ../functional-test/targets.txt -id tech-detect,tls-version
|
run: go run -race . -l ../functional-test/targets.txt -id tech-detect,tls-version
|
||||||
working-directory: v2/cmd/nuclei/
|
working-directory: v2/cmd/nuclei/
|
||||||
|
|
||||||
- name: Example Code Tests
|
- name: Example SDK Simple
|
||||||
run: go build .
|
run: go run .
|
||||||
working-directory: v2/examples/
|
working-directory: v2/examples/simple/
|
||||||
|
|
||||||
|
- name: Example SDK Advanced
|
||||||
|
run: go run .
|
||||||
|
working-directory: v2/examples/advanced/
|
||||||
|
|
|
@ -379,7 +379,7 @@ We have [a discussion thread around this](https://github.com/projectdiscovery/nu
|
||||||
|
|
||||||
### Using Nuclei From Go Code
|
### Using Nuclei From Go Code
|
||||||
|
|
||||||
Examples of using Nuclei From Go Code to run templates on targets are provided in the [examples](v2/examples/) folder.
|
Complete guide of using Nuclei as Library/SDK is available at [lib](v2/lib/README.md)
|
||||||
|
|
||||||
|
|
||||||
### Resources
|
### Resources
|
||||||
|
|
|
@ -16,7 +16,7 @@ var templatesPathTestCases = []TestCaseInfo{
|
||||||
//template folder path issue
|
//template folder path issue
|
||||||
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
|
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
|
||||||
//cwd
|
//cwd
|
||||||
{Path: "./protocols/dns/cname-fingerprint.yaml", TestCase: &cwdTemplateTest{}},
|
{Path: "./dns/detect-dangling-cname.yaml", TestCase: &cwdTemplateTest{}},
|
||||||
//relative path
|
//relative path
|
||||||
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
|
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
|
||||||
//absolute path
|
//absolute path
|
||||||
|
|
|
@ -361,7 +361,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||||
flagSet.BoolVar(&options.EnableProgressBar, "stats", false, "display statistics about the running scan"),
|
flagSet.BoolVar(&options.EnableProgressBar, "stats", false, "display statistics about the running scan"),
|
||||||
flagSet.BoolVarP(&options.StatsJSON, "stats-json", "sj", false, "display statistics in JSONL(ines) format"),
|
flagSet.BoolVarP(&options.StatsJSON, "stats-json", "sj", false, "display statistics in JSONL(ines) format"),
|
||||||
flagSet.IntVarP(&options.StatsInterval, "stats-interval", "si", 5, "number of seconds to wait between showing a statistics update"),
|
flagSet.IntVarP(&options.StatsInterval, "stats-interval", "si", 5, "number of seconds to wait between showing a statistics update"),
|
||||||
flagSet.BoolVarP(&options.Metrics, "metrics", "m", false, "expose nuclei metrics on a port"),
|
|
||||||
flagSet.IntVarP(&options.MetricsPort, "metrics-port", "mp", 9092, "port to expose nuclei metrics on"),
|
flagSet.IntVarP(&options.MetricsPort, "metrics-port", "mp", 9092, "port to expose nuclei metrics on"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
nuclei "github.com/projectdiscovery/nuclei/v2/lib"
|
||||||
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewThreadSafeNucleiEngine()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// setup sizedWaitgroup to handle concurrency
|
||||||
|
sg := sizedwaitgroup.New(10)
|
||||||
|
|
||||||
|
// scan 1 = run dns templates on scanme.sh
|
||||||
|
sg.Add()
|
||||||
|
go func() {
|
||||||
|
defer sg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"},
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// scan 2 = run templates with oast tags on honey.scanme.sh
|
||||||
|
sg.Add()
|
||||||
|
go func() {
|
||||||
|
defer sg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"http://honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{Tags: []string{"oast"}}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for all scans to finish
|
||||||
|
sg.Wait()
|
||||||
|
defer ne.Close()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// [dns-saas-service-detection] scanme.sh
|
||||||
|
// [nameserver-fingerprint] scanme.sh
|
||||||
|
// [dns-saas-service-detection] honey.scanme.sh
|
||||||
|
}
|
|
@ -1,106 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/logrusorgru/aurora"
|
|
||||||
|
|
||||||
"github.com/projectdiscovery/goflags"
|
|
||||||
"github.com/projectdiscovery/httpx/common/httpx"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/core"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/testutils"
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
|
||||||
"github.com/projectdiscovery/ratelimit"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cache := hosterrorscache.New(30, hosterrorscache.DefaultMaxHostsCount, nil)
|
|
||||||
defer cache.Close()
|
|
||||||
|
|
||||||
mockProgress := &testutils.MockProgressClient{}
|
|
||||||
reportingClient, _ := reporting.New(&reporting.Options{}, "")
|
|
||||||
defer reportingClient.Close()
|
|
||||||
|
|
||||||
outputWriter := testutils.NewMockOutputWriter()
|
|
||||||
outputWriter.WriteCallback = func(event *output.ResultEvent) {
|
|
||||||
fmt.Printf("Got Result: %v\n", event)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultOpts := types.DefaultOptions()
|
|
||||||
protocolstate.Init(defaultOpts)
|
|
||||||
protocolinit.Init(defaultOpts)
|
|
||||||
|
|
||||||
defaultOpts.IncludeIds = goflags.StringSlice{"cname-service", "tech-detect"}
|
|
||||||
defaultOpts.ExcludeTags = config.ReadIgnoreFile().Tags
|
|
||||||
|
|
||||||
interactOpts := interactsh.DefaultOptions(outputWriter, reportingClient, mockProgress)
|
|
||||||
interactClient, err := interactsh.New(interactOpts)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not create interact client: %s\n", err)
|
|
||||||
}
|
|
||||||
defer interactClient.Close()
|
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
catalog := disk.NewCatalog(filepath.Join(home, "nuclei-templates"))
|
|
||||||
executerOpts := protocols.ExecutorOptions{
|
|
||||||
Output: outputWriter,
|
|
||||||
Options: defaultOpts,
|
|
||||||
Progress: mockProgress,
|
|
||||||
Catalog: catalog,
|
|
||||||
IssuesClient: reportingClient,
|
|
||||||
RateLimiter: ratelimit.New(context.Background(), 150, time.Second),
|
|
||||||
Interactsh: interactClient,
|
|
||||||
HostErrorsCache: cache,
|
|
||||||
Colorizer: aurora.NewAurora(true),
|
|
||||||
ResumeCfg: types.NewResumeCfg(),
|
|
||||||
}
|
|
||||||
engine := core.New(defaultOpts)
|
|
||||||
engine.SetExecuterOptions(executerOpts)
|
|
||||||
|
|
||||||
workflowLoader, err := parsers.NewLoader(&executerOpts)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not create workflow loader: %s\n", err)
|
|
||||||
}
|
|
||||||
executerOpts.WorkflowLoader = workflowLoader
|
|
||||||
|
|
||||||
store, err := loader.New(loader.NewConfig(defaultOpts, catalog, executerOpts))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not create loader client: %s\n", err)
|
|
||||||
}
|
|
||||||
store.Load()
|
|
||||||
|
|
||||||
// flat input without probe
|
|
||||||
inputArgs := []*contextargs.MetaInput{{Input: "docs.hackerone.com"}}
|
|
||||||
input := &inputs.SimpleInputProvider{Inputs: inputArgs}
|
|
||||||
|
|
||||||
httpxOptions := httpx.DefaultOptions
|
|
||||||
httpxOptions.Timeout = 5 * time.Second
|
|
||||||
httpxClient, err := httpx.New(&httpxOptions)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use httpx to probe the URL => https://scanme.sh
|
|
||||||
input.SetWithProbe("scanme.sh", httpxClient)
|
|
||||||
|
|
||||||
_ = engine.Execute(store.Templates(), input)
|
|
||||||
engine.WorkPool().Wait() // Wait for the scan to finish
|
|
||||||
}
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import nuclei "github.com/projectdiscovery/nuclei/v2/lib"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ne, err := nuclei.NewNucleiEngine(
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{Tags: []string{"oast"}}),
|
||||||
|
nuclei.EnableStatsWithOpts(nuclei.StatsOptions{MetricServerPort: 6064}), // optionally enable metrics server for better observability
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// load targets and optionally probe non http/https targets
|
||||||
|
ne.LoadTargets([]string{"http://honey.scanme.sh"}, false)
|
||||||
|
err = ne.ExecuteWithCallback(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ne.Close()
|
||||||
|
}
|
|
@ -103,3 +103,29 @@ func isEmptyDir(dir string) bool {
|
||||||
})
|
})
|
||||||
return !hasFiles
|
return !hasFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getUtmSource returns utm_source from environment variable
|
||||||
|
func getUtmSource() string {
|
||||||
|
value := ""
|
||||||
|
switch {
|
||||||
|
case os.Getenv("GH_ACTION") != "":
|
||||||
|
value = "ghci"
|
||||||
|
case os.Getenv("TRAVIS") != "":
|
||||||
|
value = "travis"
|
||||||
|
case os.Getenv("CIRCLECI") != "":
|
||||||
|
value = "circleci"
|
||||||
|
case os.Getenv("CI") != "":
|
||||||
|
value = "gitlabci" // this also includes bitbucket
|
||||||
|
case os.Getenv("GITHUB_ACTIONS") != "":
|
||||||
|
value = "ghci"
|
||||||
|
case os.Getenv("AWS_EXECUTION_ENV") != "":
|
||||||
|
value = os.Getenv("AWS_EXECUTION_ENV")
|
||||||
|
case os.Getenv("JENKINS_URL") != "":
|
||||||
|
value = "jenkins"
|
||||||
|
case os.Getenv("FUNCTION_TARGET") != "":
|
||||||
|
value = "gcf"
|
||||||
|
default:
|
||||||
|
value = "unknown"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||||
"github.com/projectdiscovery/retryablehttp-go"
|
"github.com/projectdiscovery/retryablehttp-go"
|
||||||
|
@ -34,7 +35,56 @@ type PdtmAPIResponse struct {
|
||||||
// and returns an error if it fails to check on success it returns nil and changes are
|
// and returns an error if it fails to check on success it returns nil and changes are
|
||||||
// made to the default config in config.DefaultConfig
|
// made to the default config in config.DefaultConfig
|
||||||
func NucleiVersionCheck() error {
|
func NucleiVersionCheck() error {
|
||||||
resp, err := retryableHttpClient.Get(pdtmNucleiVersionEndpoint + "?" + getpdtmParams())
|
return doVersionCheck(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will be updated by features of 1.21 release (which directly provides sync.Once(func()))
|
||||||
|
type sdkUpdateCheck struct {
|
||||||
|
sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
var sdkUpdateCheckInstance = &sdkUpdateCheck{}
|
||||||
|
|
||||||
|
// NucleiSDKVersionCheck checks for latest version of nuclei which running in sdk mode
|
||||||
|
// this only happens once per process regardless of how many times this function is called
|
||||||
|
func NucleiSDKVersionCheck() {
|
||||||
|
sdkUpdateCheckInstance.Do(func() {
|
||||||
|
_ = doVersionCheck(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getpdtmParams returns encoded query parameters sent to update check endpoint
|
||||||
|
func getpdtmParams(isSDK bool) string {
|
||||||
|
params := &url.Values{}
|
||||||
|
params.Add("os", runtime.GOOS)
|
||||||
|
params.Add("arch", runtime.GOARCH)
|
||||||
|
params.Add("go_version", runtime.Version())
|
||||||
|
params.Add("v", config.Version)
|
||||||
|
if isSDK {
|
||||||
|
params.Add("sdk", "true")
|
||||||
|
}
|
||||||
|
params.Add("utm_source", getUtmSource())
|
||||||
|
return params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIgnoreFile updates default ignore file by downloading latest ignore file
|
||||||
|
func UpdateIgnoreFile() error {
|
||||||
|
resp, err := retryableHttpClient.Get(pdtmNucleiIgnoreFileEndpoint + "?" + getpdtmParams(false))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bin, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return config.DefaultConfig.UpdateNucleiIgnoreHash()
|
||||||
|
}
|
||||||
|
|
||||||
|
func doVersionCheck(isSDK bool) error {
|
||||||
|
resp, err := retryableHttpClient.Get(pdtmNucleiVersionEndpoint + "?" + getpdtmParams(isSDK))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -63,29 +113,3 @@ func NucleiVersionCheck() error {
|
||||||
}
|
}
|
||||||
return config.DefaultConfig.WriteVersionCheckData(pdtmResp.IgnoreHash, nucleiversion, templateversion)
|
return config.DefaultConfig.WriteVersionCheckData(pdtmResp.IgnoreHash, nucleiversion, templateversion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getpdtmParams returns encoded query parameters sent to update check endpoint
|
|
||||||
func getpdtmParams() string {
|
|
||||||
params := &url.Values{}
|
|
||||||
params.Add("os", runtime.GOOS)
|
|
||||||
params.Add("arch", runtime.GOARCH)
|
|
||||||
params.Add("go_version", runtime.Version())
|
|
||||||
params.Add("v", config.Version)
|
|
||||||
return params.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateIgnoreFile updates default ignore file by downloading latest ignore file
|
|
||||||
func UpdateIgnoreFile() error {
|
|
||||||
resp, err := retryableHttpClient.Get(pdtmNucleiIgnoreFileEndpoint + "?" + getpdtmParams())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bin, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return config.DefaultConfig.UpdateNucleiIgnoreHash()
|
|
||||||
}
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ func ParseOptions(options *types.Options) {
|
||||||
}
|
}
|
||||||
// Validate the options passed by the user and if any
|
// Validate the options passed by the user and if any
|
||||||
// invalid options have been used, exit.
|
// invalid options have been used, exit.
|
||||||
if err := validateOptions(options); err != nil {
|
if err := ValidateOptions(options); err != nil {
|
||||||
gologger.Fatal().Msgf("Program exiting: %s\n", err)
|
gologger.Fatal().Msgf("Program exiting: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ func ParseOptions(options *types.Options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateOptions validates the configuration options passed
|
// validateOptions validates the configuration options passed
|
||||||
func validateOptions(options *types.Options) error {
|
func ValidateOptions(options *types.Options) error {
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
if err := validate.Struct(options); err != nil {
|
if err := validate.Struct(options); err != nil {
|
||||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||||
|
|
|
@ -260,7 +260,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||||
statsInterval = -1
|
statsInterval = -1
|
||||||
options.EnableProgressBar = true
|
options.EnableProgressBar = true
|
||||||
}
|
}
|
||||||
runner.progress, progressErr = progress.NewStatsTicker(statsInterval, options.EnableProgressBar, options.StatsJSON, options.Metrics, options.Cloud, options.MetricsPort)
|
runner.progress, progressErr = progress.NewStatsTicker(statsInterval, options.EnableProgressBar, options.StatsJSON, options.Cloud, options.MetricsPort)
|
||||||
if progressErr != nil {
|
if progressErr != nil {
|
||||||
return nil, progressErr
|
return nil, progressErr
|
||||||
}
|
}
|
||||||
|
@ -662,16 +662,6 @@ func (r *Runner) executeSmartWorkflowInput(executorOpts protocols.ExecutorOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
|
func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
|
||||||
var unclusteredRequests int64
|
|
||||||
for _, template := range store.Templates() {
|
|
||||||
// workflows will dynamically adjust the totals while running, as
|
|
||||||
// it can't be known in advance which requests will be called
|
|
||||||
if len(template.Workflows) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
unclusteredRequests += int64(template.TotalRequests) * r.hmapInputProvider.Count()
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.options.VerboseVerbose {
|
if r.options.VerboseVerbose {
|
||||||
for _, template := range store.Templates() {
|
for _, template := range store.Templates() {
|
||||||
r.logAvailableTemplate(template.Path)
|
r.logAvailableTemplate(template.Path)
|
||||||
|
@ -681,34 +671,15 @@ func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cluster the templates first because we want info on how many
|
finalTemplates := []*templates.Template{}
|
||||||
// templates did we cluster for showing to user in CLI
|
finalTemplates = append(finalTemplates, store.Templates()...)
|
||||||
originalTemplatesCount := len(store.Templates())
|
|
||||||
finalTemplates, clusterCount := templates.ClusterTemplates(store.Templates(), engine.ExecuterOptions())
|
|
||||||
finalTemplates = append(finalTemplates, store.Workflows()...)
|
finalTemplates = append(finalTemplates, store.Workflows()...)
|
||||||
|
|
||||||
var totalRequests int64
|
if len(finalTemplates) == 0 {
|
||||||
for _, t := range finalTemplates {
|
return nil, errors.New("no templates provided for scan")
|
||||||
if len(t.Workflows) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
totalRequests += int64(t.Executer.Requests()) * r.hmapInputProvider.Count()
|
|
||||||
}
|
|
||||||
if totalRequests < unclusteredRequests {
|
|
||||||
gologger.Info().Msgf("Templates clustered: %d (Reduced %d Requests)", clusterCount, unclusteredRequests-totalRequests)
|
|
||||||
}
|
|
||||||
workflowCount := len(store.Workflows())
|
|
||||||
templateCount := originalTemplatesCount + workflowCount
|
|
||||||
|
|
||||||
// 0 matches means no templates were found in the directory
|
|
||||||
if templateCount == 0 {
|
|
||||||
return &atomic.Bool{}, errors.New("no valid templates were found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracks global progress and captures stdout/stderr until p.Wait finishes
|
results := engine.ExecuteScanWithOpts(finalTemplates, r.hmapInputProvider, r.options.DisableClustering)
|
||||||
r.progress.Init(r.hmapInputProvider.Count(), templateCount, totalRequests)
|
|
||||||
|
|
||||||
results := engine.ExecuteScanWithOpts(finalTemplates, r.hmapInputProvider, true)
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
## Using Nuclei as Library
|
||||||
|
|
||||||
|
Nuclei was primarily built as a CLI tool, but with increasing choice of users wanting to use nuclei as library in their own automation, we have added a simplified Library/SDK of nuclei in v3
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
To add nuclei as a library to your go project, you can use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u github.com/projectdiscovery/nuclei/v2/lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add below import to your go file and let IDE handle the rest:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import nuclei "github.com/projectdiscovery/nuclei/v2/lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Example of using Nuclei Library/SDK
|
||||||
|
|
||||||
|
```go
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewNucleiEngine(
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{Severity: "critical"}), // run critical severity templates only
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// load targets and optionally probe non http/https targets
|
||||||
|
ne.LoadTargets([]string{"scanme.sh"}, false)
|
||||||
|
err = ne.ExecuteWithCallback(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ne.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Example of using Nuclei Library/SDK
|
||||||
|
|
||||||
|
For Various use cases like batching etc you might want to run nuclei in goroutines this can be done by using `nuclei.NewThreadSafeNucleiEngine`
|
||||||
|
|
||||||
|
```go
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewThreadSafeNucleiEngine()
|
||||||
|
if err != nil{
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// setup waitgroup to handle concurrency
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
// scan 1 = run dns templates on scanme.sh
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "http"}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// scan 2 = run http templates on honey.scanme.sh
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for all scans to finish
|
||||||
|
wg.Wait()
|
||||||
|
defer ne.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Documentation
|
||||||
|
|
||||||
|
For complete documentation of nuclei library, please refer to [godoc](https://pkg.go.dev/github.com/projectdiscovery/nuclei/v2/lib) which contains all available options and methods.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Note
|
||||||
|
|
||||||
|
| :exclamation: **Disclaimer** |
|
||||||
|
|---------------------------------|
|
||||||
|
| **This project is in active development**. Expect breaking changes with releases. Review the release changelog before updating. |
|
||||||
|
| This project was primarily built to be used as a standalone CLI tool. **Running nuclei as a service may pose security risks.** It's recommended to use with caution and additional security measures. |
|
|
@ -0,0 +1,298 @@
|
||||||
|
package nuclei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/gologger"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/vardump"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/headless/engine"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates/types"
|
||||||
|
"github.com/projectdiscovery/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// config contains all SDK configuration options
|
||||||
|
type TemplateFilters struct {
|
||||||
|
Severity string // filter by severities (accepts CSV values of info, low, medium, high, critical)
|
||||||
|
ExcludeSeverities string // filter by excluding severities (accepts CSV values of info, low, medium, high, critical)
|
||||||
|
ProtocolTypes string // filter by protocol types
|
||||||
|
ExcludeProtocolTypes string // filter by excluding protocol types
|
||||||
|
Authors []string // fiter by author
|
||||||
|
Tags []string // filter by tags present in template
|
||||||
|
ExcludeTags []string // filter by excluding tags present in template
|
||||||
|
IncludeTags []string // filter by including tags present in template
|
||||||
|
IDs []string // filter by template IDs
|
||||||
|
ExcludeIDs []string // filter by excluding template IDs
|
||||||
|
TemplateCondition []string // DSL condition/ expression
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTemplateFilters sets template filters and only templates matching the filters will be
|
||||||
|
// loaded and executed
|
||||||
|
func WithTemplateFilters(filters TemplateFilters) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
s := severity.Severities{}
|
||||||
|
if err := s.Set(filters.Severity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
es := severity.Severities{}
|
||||||
|
if err := es.Set(filters.ExcludeSeverities); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pt := types.ProtocolTypes{}
|
||||||
|
if err := pt.Set(filters.ProtocolTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ept := types.ProtocolTypes{}
|
||||||
|
if err := ept.Set(filters.ExcludeProtocolTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.opts.Authors = filters.Authors
|
||||||
|
e.opts.Tags = filters.Tags
|
||||||
|
e.opts.ExcludeTags = filters.ExcludeTags
|
||||||
|
e.opts.IncludeTags = filters.IncludeTags
|
||||||
|
e.opts.IncludeIds = filters.IDs
|
||||||
|
e.opts.ExcludeIds = filters.ExcludeIDs
|
||||||
|
e.opts.Severities = s
|
||||||
|
e.opts.ExcludeSeverities = es
|
||||||
|
e.opts.Protocols = pt
|
||||||
|
e.opts.ExcludeProtocols = ept
|
||||||
|
e.opts.IncludeConditions = filters.TemplateCondition
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InteractshOpts contains options for interactsh
|
||||||
|
type InteractshOpts interactsh.Options
|
||||||
|
|
||||||
|
// WithInteractshOptions sets interactsh options
|
||||||
|
func WithInteractshOptions(opts InteractshOpts) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithInteractshOptions")
|
||||||
|
}
|
||||||
|
optsPtr := &opts
|
||||||
|
e.interactshOpts = (*interactsh.Options)(optsPtr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrency options
|
||||||
|
type Concurrency struct {
|
||||||
|
TemplateConcurrency int // number of templates to run concurrently (per host in host-spray mode)
|
||||||
|
HostConcurrency int // number of hosts to scan concurrently (per template in template-spray mode)
|
||||||
|
HeadlessHostConcurrency int // number of hosts to scan concurrently for headless templates (per template in template-spray mode)
|
||||||
|
HeadlessTemplateConcurrency int // number of templates to run concurrently for headless templates (per host in host-spray mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithConcurrency sets concurrency options
|
||||||
|
func WithConcurrency(opts Concurrency) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
e.opts.TemplateThreads = opts.TemplateConcurrency
|
||||||
|
e.opts.BulkSize = opts.HostConcurrency
|
||||||
|
e.opts.HeadlessBulkSize = opts.HeadlessHostConcurrency
|
||||||
|
e.opts.HeadlessTemplateThreads = opts.HeadlessTemplateConcurrency
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithGlobalRateLimit sets global rate (i.e all hosts combined) limit options
|
||||||
|
func WithGlobalRateLimit(maxTokens int, duration time.Duration) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
e.rateLimiter = ratelimit.New(context.Background(), uint(maxTokens), duration)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadlessOpts contains options for headless templates
|
||||||
|
type HeadlessOpts struct {
|
||||||
|
PageTimeout int // timeout for page load
|
||||||
|
ShowBrowser bool
|
||||||
|
HeadlessOptions []string
|
||||||
|
UseChrome bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableHeadless allows execution of headless templates
|
||||||
|
// *Use With Caution*: Enabling headless mode may open up attack surface due to browser usage
|
||||||
|
// and can be prone to exploitation by custom unverified templates if not properly configured
|
||||||
|
func EnableHeadlessWithOpts(hopts *HeadlessOpts) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
e.opts.Headless = true
|
||||||
|
if hopts != nil {
|
||||||
|
e.opts.HeadlessOptionalArguments = hopts.HeadlessOptions
|
||||||
|
e.opts.PageTimeout = hopts.PageTimeout
|
||||||
|
e.opts.ShowBrowser = hopts.ShowBrowser
|
||||||
|
e.opts.UseInstalledChrome = hopts.UseChrome
|
||||||
|
}
|
||||||
|
if engine.MustDisableSandbox() {
|
||||||
|
gologger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox\n")
|
||||||
|
}
|
||||||
|
browser, err := engine.New(e.opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.executerOpts.Browser = browser
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatsOptions
|
||||||
|
type StatsOptions struct {
|
||||||
|
Interval int
|
||||||
|
JSON bool
|
||||||
|
MetricServerPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableStats enables Stats collection with defined interval(in sec) and callback
|
||||||
|
// Note: callback is executed in a separate goroutine
|
||||||
|
func EnableStatsWithOpts(opts StatsOptions) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("EnableStatsWithOpts")
|
||||||
|
}
|
||||||
|
if opts.Interval == 0 {
|
||||||
|
opts.Interval = 5 //sec
|
||||||
|
}
|
||||||
|
e.opts.StatsInterval = opts.Interval
|
||||||
|
e.enableStats = true
|
||||||
|
e.opts.StatsJSON = opts.JSON
|
||||||
|
e.opts.MetricsPort = opts.MetricServerPort
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerbosityOptions
|
||||||
|
type VerbosityOptions struct {
|
||||||
|
Verbose bool // show verbose output
|
||||||
|
Silent bool // show only results
|
||||||
|
Debug bool // show debug output
|
||||||
|
DebugRequest bool // show request in debug output
|
||||||
|
DebugResponse bool // show response in debug output
|
||||||
|
ShowVarDump bool // show variable dumps in output
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVerbosity allows setting verbosity options of (internal) nuclei engine
|
||||||
|
// and does not affect SDK output
|
||||||
|
func WithVerbosity(opts VerbosityOptions) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithVerbosity")
|
||||||
|
}
|
||||||
|
e.opts.Verbose = opts.Verbose
|
||||||
|
e.opts.Silent = opts.Silent
|
||||||
|
e.opts.Debug = opts.Debug
|
||||||
|
e.opts.DebugRequests = opts.DebugRequest
|
||||||
|
e.opts.DebugResponse = opts.DebugResponse
|
||||||
|
if opts.ShowVarDump {
|
||||||
|
vardump.EnableVarDump = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkConfig contains network config options
|
||||||
|
// ex: retries , httpx probe , timeout etc
|
||||||
|
type NetworkConfig struct {
|
||||||
|
Timeout int // Timeout in seconds
|
||||||
|
Retries int // Number of retries
|
||||||
|
LeaveDefaultPorts bool // Leave default ports for http/https
|
||||||
|
MaxHostError int // Maximum number of host errors to allow before skipping that host
|
||||||
|
TrackError []string // Adds given errors to max host error watchlist
|
||||||
|
DisableMaxHostErr bool // Disable max host error optimization (Hosts are not skipped even if they are not responding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNetworkConfig allows setting network config options
|
||||||
|
func WithNetworkConfig(opts NetworkConfig) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithNetworkConfig")
|
||||||
|
}
|
||||||
|
e.opts.Timeout = opts.Timeout
|
||||||
|
e.opts.Retries = opts.Retries
|
||||||
|
e.opts.LeaveDefaultPorts = opts.LeaveDefaultPorts
|
||||||
|
e.hostErrCache = hosterrorscache.New(opts.MaxHostError, hosterrorscache.DefaultMaxHostsCount, opts.TrackError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithProxy allows setting proxy options
|
||||||
|
func WithProxy(proxy []string, proxyInternalRequests bool) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithProxy")
|
||||||
|
}
|
||||||
|
e.opts.Proxy = proxy
|
||||||
|
e.opts.ProxyInternal = proxyInternalRequests
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithScanStrategy allows setting scan strategy options
|
||||||
|
func WithScanStrategy(strategy string) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
e.opts.ScanStrategy = strategy
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputWriter
|
||||||
|
type OutputWriter output.Writer
|
||||||
|
|
||||||
|
// UseWriter allows setting custom output writer
|
||||||
|
// by default a mock writer is used with user defined callback
|
||||||
|
// if outputWriter is used callback will be ignored
|
||||||
|
func UseOutputWriter(writer OutputWriter) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("UseOutputWriter")
|
||||||
|
}
|
||||||
|
e.customWriter = writer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatsWriter
|
||||||
|
type StatsWriter progress.Progress
|
||||||
|
|
||||||
|
// UseStatsWriter allows setting a custom stats writer
|
||||||
|
// which can be used to write stats somewhere (ex: send to webserver etc)
|
||||||
|
func UseStatsWriter(writer StatsWriter) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("UseStatsWriter")
|
||||||
|
}
|
||||||
|
e.customProgress = writer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTemplateUpdateCallback allows setting a callback which will be called
|
||||||
|
// when nuclei templates are outdated
|
||||||
|
// Note: Nuclei-templates are crucial part of nuclei and using outdated templates or nuclei sdk is not recommended
|
||||||
|
// as it may cause unexpected results due to compatibility issues
|
||||||
|
func WithTemplateUpdateCallback(disableTemplatesAutoUpgrade bool, callback func(newVersion string)) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithTemplateUpdateCallback")
|
||||||
|
}
|
||||||
|
e.disableTemplatesAutoUpgrade = disableTemplatesAutoUpgrade
|
||||||
|
e.onUpdateAvailableCallback = callback
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSandboxOptions allows setting supported sandbox options
|
||||||
|
func WithSandboxOptions(allowLocalFileAccess bool, restrictLocalNetworkAccess bool) NucleiSDKOptions {
|
||||||
|
return func(e *NucleiEngine) error {
|
||||||
|
if e.mode == threadSafe {
|
||||||
|
return ErrOptionsNotSupported.Msgf("WithSandboxOptions")
|
||||||
|
}
|
||||||
|
e.opts.AllowLocalFileAccess = allowLocalFileAccess
|
||||||
|
e.opts.RestrictLocalNetworkAccess = restrictLocalNetworkAccess
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
//go:build !race
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
package nuclei_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
nuclei "github.com/projectdiscovery/nuclei/v2/lib"
|
||||||
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A very simple example on how to use nuclei engine
|
||||||
|
func ExampleNucleiEngine() {
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewNucleiEngine(
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IDs: []string{"self-signed-ssl"}}), // only run self-signed-ssl template
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// load targets and optionally probe non http/https targets
|
||||||
|
ne.LoadTargets([]string{"scanme.sh"}, false)
|
||||||
|
// when callback is nil it nuclei will print JSON output to stdout
|
||||||
|
err = ne.ExecuteWithCallback(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ne.Close()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// [self-signed-ssl] scanme.sh:443
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleThreadSafeNucleiEngine() {
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewThreadSafeNucleiEngine()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// setup sizedWaitgroup to handle concurrency
|
||||||
|
// here we are using sizedWaitgroup to limit concurrency to 1
|
||||||
|
sg := sizedwaitgroup.New(1)
|
||||||
|
|
||||||
|
// scan 1 = run dns templates on scanme.sh
|
||||||
|
sg.Add()
|
||||||
|
go func() {
|
||||||
|
defer sg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"},
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IDs: []string{"nameserver-fingerprint"}}), // only run self-signed-ssl template
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// scan 2 = run dns templates on honey.scanme.sh
|
||||||
|
sg.Add()
|
||||||
|
go func() {
|
||||||
|
defer sg.Done()
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for all scans to finish
|
||||||
|
sg.Wait()
|
||||||
|
defer ne.Close()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// [nameserver-fingerprint] scanme.sh
|
||||||
|
// [dns-saas-service-detection] honey.scanme.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// this file only contains testtables examples https://go.dev/blog/examples
|
||||||
|
// and actual functionality test are in sdk_test.go
|
||||||
|
if os.Getenv("GH_ACTION") != "" {
|
||||||
|
// no need to run this test on github actions
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.Run()
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package nuclei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||||
|
uncoverNuclei "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/uncover"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
||||||
|
"github.com/projectdiscovery/uncover"
|
||||||
|
)
|
||||||
|
|
||||||
|
// helper.go file proxy execution of all nuclei functions that are nested deep inside multiple packages
|
||||||
|
// but are helpful / useful while using nuclei as a library
|
||||||
|
|
||||||
|
// GetTargetsFromUncover returns targets from uncover in given format .
|
||||||
|
// supported formats are any string with [ip,host,port,url] placeholders
|
||||||
|
func GetTargetsFromUncover(ctx context.Context, outputFormat string, opts *uncover.Options) (chan string, error) {
|
||||||
|
return uncoverNuclei.GetTargetsFromUncover(ctx, outputFormat, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTargetsFromTemplateMetadata returns all targets by querying engine metadata (ex: fofo-query,shodan-query) etc from given templates .
|
||||||
|
// supported formats are any string with [ip,host,port,url] placeholders
|
||||||
|
func GetTargetsFromTemplateMetadata(ctx context.Context, templates []*templates.Template, outputFormat string, opts *uncover.Options) chan string {
|
||||||
|
return uncoverNuclei.GetUncoverTargetsFromMetadata(ctx, templates, outputFormat, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig is instance of default nuclei configs
|
||||||
|
// any mutations to this config will be reflected in all nuclei instances (saves some config to disk)
|
||||||
|
var DefaultConfig *config.Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultConfig = config.DefaultConfig
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
package nuclei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
|
"github.com/projectdiscovery/ratelimit"
|
||||||
|
errorutil "github.com/projectdiscovery/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
engine *core.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// createEphemeralObjects creates ephemeral nuclei objects/instances/types
|
||||||
|
func createEphemeralObjects(base *NucleiEngine, opts *types.Options) (*unsafeOptions, error) {
|
||||||
|
u := &unsafeOptions{}
|
||||||
|
u.executerOpts = protocols.ExecutorOptions{
|
||||||
|
Output: base.customWriter,
|
||||||
|
Options: opts,
|
||||||
|
Progress: base.customProgress,
|
||||||
|
Catalog: base.catalog,
|
||||||
|
IssuesClient: base.rc,
|
||||||
|
RateLimiter: base.rateLimiter,
|
||||||
|
Interactsh: base.interactshClient,
|
||||||
|
HostErrorsCache: base.hostErrCache,
|
||||||
|
Colorizer: aurora.NewAurora(true),
|
||||||
|
ResumeCfg: types.NewResumeCfg(),
|
||||||
|
}
|
||||||
|
if opts.RateLimitMinute > 0 {
|
||||||
|
u.executerOpts.RateLimiter = ratelimit.New(context.Background(), uint(opts.RateLimitMinute), time.Minute)
|
||||||
|
} else if opts.RateLimit > 0 {
|
||||||
|
u.executerOpts.RateLimiter = ratelimit.New(context.Background(), uint(opts.RateLimit), time.Second)
|
||||||
|
} else {
|
||||||
|
u.executerOpts.RateLimiter = ratelimit.NewUnlimited(context.Background())
|
||||||
|
}
|
||||||
|
u.engine = core.New(opts)
|
||||||
|
u.engine.SetExecuterOptions(u.executerOpts)
|
||||||
|
return u, 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 NewThreadSafeNucleiEngine(opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) {
|
||||||
|
// default options
|
||||||
|
e := &NucleiEngine{
|
||||||
|
opts: types.DefaultOptions(),
|
||||||
|
mode: threadSafe,
|
||||||
|
}
|
||||||
|
for _, option := range opts {
|
||||||
|
if err := option(e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ThreadSafeNucleiEngine{eng: e}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func (e *ThreadSafeNucleiEngine) GlobalResultCallback(callback func(event *output.ResultEvent)) {
|
||||||
|
e.eng.resultCallbacks = []func(*output.ResultEvent){callback}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWithCallback executes templates on targets and calls callback on each result(only if results are found)
|
||||||
|
// This method can be called concurrently and it will use some global resources but can be runned parllely
|
||||||
|
// by invoking this method with different options and targets
|
||||||
|
// Note: Not all options are thread-safe. this method will throw error if you try to use non-thread-safe options
|
||||||
|
func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOpts(targets []string, opts ...NucleiSDKOptions) error {
|
||||||
|
baseOpts := *e.eng.opts
|
||||||
|
tmpEngine := &NucleiEngine{opts: &baseOpts, mode: threadSafe}
|
||||||
|
for _, option := range opts {
|
||||||
|
if err := option(tmpEngine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create ephemeral nuclei objects/instances/types using base nuclei engine
|
||||||
|
unsafeOpts, err := createEphemeralObjects(e.eng, tmpEngine.opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// load templates
|
||||||
|
workflowLoader, err := parsers.NewLoader(&unsafeOpts.executerOpts)
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.New("Could not create workflow loader: %s\n", err)
|
||||||
|
}
|
||||||
|
unsafeOpts.executerOpts.WorkflowLoader = workflowLoader
|
||||||
|
|
||||||
|
store, err := loader.New(loader.NewConfig(tmpEngine.opts, e.eng.catalog, unsafeOpts.executerOpts))
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.New("Could not create loader client: %s\n", err)
|
||||||
|
}
|
||||||
|
store.Load()
|
||||||
|
|
||||||
|
inputProvider := &inputs.SimpleInputProvider{
|
||||||
|
Inputs: []*contextargs.MetaInput{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// load targets
|
||||||
|
for _, target := range targets {
|
||||||
|
inputProvider.Set(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(store.Templates(), inputProvider, false)
|
||||||
|
|
||||||
|
engine.WorkPool().Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all resources used by nuclei engine
|
||||||
|
func (e *ThreadSafeNucleiEngine) Close() {
|
||||||
|
e.eng.Close()
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
package nuclei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/httpx/common/httpx"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/headless/engine"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
|
"github.com/projectdiscovery/ratelimit"
|
||||||
|
"github.com/projectdiscovery/retryablehttp-go"
|
||||||
|
errorutil "github.com/projectdiscovery/utils/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NucleiSDKOptions contains options for nuclei SDK
|
||||||
|
type NucleiSDKOptions func(e *NucleiEngine) error
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotImplemented is returned when a feature is not implemented
|
||||||
|
ErrNotImplemented = errorutil.New("Not implemented")
|
||||||
|
// ErrNoTemplatesAvailable is returned when no templates are available to execute
|
||||||
|
ErrNoTemplatesAvailable = errorutil.New("No templates available")
|
||||||
|
// ErrNoTargetsAvailable is returned when no targets are available to scan
|
||||||
|
ErrNoTargetsAvailable = errorutil.New("No targets available")
|
||||||
|
// ErrOptionsNotSupported is returned when an option is not supported in thread safe mode
|
||||||
|
ErrOptionsNotSupported = errorutil.NewWithFmt("Option %v not supported in thread safe mode")
|
||||||
|
)
|
||||||
|
|
||||||
|
type engineMode uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
singleInstance engineMode = iota
|
||||||
|
threadSafe
|
||||||
|
)
|
||||||
|
|
||||||
|
// NucleiEngine is the Engine/Client for nuclei which
|
||||||
|
// runs scans using templates and returns results
|
||||||
|
type NucleiEngine struct {
|
||||||
|
// user options
|
||||||
|
resultCallbacks []func(event *output.ResultEvent)
|
||||||
|
onFailureCallback func(event *output.InternalEvent)
|
||||||
|
disableTemplatesAutoUpgrade bool
|
||||||
|
enableStats bool
|
||||||
|
onUpdateAvailableCallback func(newVersion string)
|
||||||
|
|
||||||
|
// ready-status fields
|
||||||
|
templatesLoaded bool
|
||||||
|
|
||||||
|
// unexported core fields
|
||||||
|
interactshClient *interactsh.Client
|
||||||
|
catalog *disk.DiskCatalog
|
||||||
|
rateLimiter *ratelimit.Limiter
|
||||||
|
store *loader.Store
|
||||||
|
httpxClient *httpx.HTTPX
|
||||||
|
inputProvider *inputs.SimpleInputProvider
|
||||||
|
engine *core.Engine
|
||||||
|
mode engineMode
|
||||||
|
browserInstance *engine.Browser
|
||||||
|
httpClient *retryablehttp.Client
|
||||||
|
|
||||||
|
// unexported meta options
|
||||||
|
opts *types.Options
|
||||||
|
interactshOpts *interactsh.Options
|
||||||
|
hostErrCache *hosterrorscache.Cache
|
||||||
|
customWriter output.Writer
|
||||||
|
customProgress progress.Progress
|
||||||
|
rc reporting.Client
|
||||||
|
executerOpts protocols.ExecutorOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAllTemplates loads all nuclei template based on given options
|
||||||
|
func (e *NucleiEngine) LoadAllTemplates() error {
|
||||||
|
workflowLoader, err := parsers.NewLoader(&e.executerOpts)
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.New("Could not create workflow loader: %s\n", err)
|
||||||
|
}
|
||||||
|
e.executerOpts.WorkflowLoader = workflowLoader
|
||||||
|
|
||||||
|
e.store, err = loader.New(loader.NewConfig(e.opts, e.catalog, e.executerOpts))
|
||||||
|
if err != nil {
|
||||||
|
return errorutil.New("Could not create loader client: %s\n", err)
|
||||||
|
}
|
||||||
|
e.store.Load()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemplates returns all nuclei templates that are loaded
|
||||||
|
func (e *NucleiEngine) GetTemplates() []*templates.Template {
|
||||||
|
if !e.templatesLoaded {
|
||||||
|
_ = e.LoadAllTemplates()
|
||||||
|
}
|
||||||
|
return e.store.Templates()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTargets(urls/domains/ips only) adds targets to the nuclei engine
|
||||||
|
func (e *NucleiEngine) LoadTargets(targets []string, probeNonHttp bool) {
|
||||||
|
for _, target := range targets {
|
||||||
|
if probeNonHttp {
|
||||||
|
e.inputProvider.SetWithProbe(target, e.httpxClient)
|
||||||
|
} else {
|
||||||
|
e.inputProvider.Set(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTargetsFromReader adds targets(urls/domains/ips only) from reader to the nuclei engine
|
||||||
|
func (e *NucleiEngine) LoadTargetsFromReader(reader io.Reader, probeNonHttp bool) {
|
||||||
|
buff := bufio.NewScanner(reader)
|
||||||
|
for buff.Scan() {
|
||||||
|
if probeNonHttp {
|
||||||
|
e.inputProvider.SetWithProbe(buff.Text(), e.httpxClient)
|
||||||
|
} else {
|
||||||
|
e.inputProvider.Set(buff.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all resources used by nuclei engine
|
||||||
|
func (e *NucleiEngine) Close() {
|
||||||
|
e.interactshClient.Close()
|
||||||
|
e.rc.Close()
|
||||||
|
e.customWriter.Close()
|
||||||
|
e.hostErrCache.Close()
|
||||||
|
e.executerOpts.RateLimiter.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWithCallback executes templates on targets and calls callback on each result(only if results are found)
|
||||||
|
func (e *NucleiEngine) ExecuteWithCallback(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 _, callback := range callback {
|
||||||
|
if callback != nil {
|
||||||
|
filtered = append(filtered, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.resultCallbacks = append(e.resultCallbacks, filtered...)
|
||||||
|
|
||||||
|
_ = e.engine.ExecuteScanWithOpts(e.store.Templates(), e.inputProvider, false)
|
||||||
|
defer e.engine.WorkPool().Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNucleiEngine creates a new nuclei engine instance
|
||||||
|
func NewNucleiEngine(options ...NucleiSDKOptions) (*NucleiEngine, error) {
|
||||||
|
// default options
|
||||||
|
e := &NucleiEngine{
|
||||||
|
opts: types.DefaultOptions(),
|
||||||
|
mode: singleInstance,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
if err := option(e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package nuclei
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
|
"github.com/projectdiscovery/gologger"
|
||||||
|
"github.com/projectdiscovery/gologger/levels"
|
||||||
|
"github.com/projectdiscovery/httpx/common/httpx"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/internal/installer"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/internal/runner"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/progress"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/httpclientpool"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/testutils"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
|
"github.com/projectdiscovery/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// applyRequiredDefaults to options
|
||||||
|
func (e *NucleiEngine) applyRequiredDefaults() {
|
||||||
|
if e.customWriter == nil {
|
||||||
|
mockoutput := testutils.NewMockOutputWriter()
|
||||||
|
mockoutput.WriteCallback = func(event *output.ResultEvent) {
|
||||||
|
if len(e.resultCallbacks) > 0 {
|
||||||
|
for _, callback := range e.resultCallbacks {
|
||||||
|
if callback != nil {
|
||||||
|
callback(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString(fmt.Sprintf("[%v] ", event.TemplateID))
|
||||||
|
if event.Matched != "" {
|
||||||
|
sb.WriteString(event.Matched)
|
||||||
|
} else {
|
||||||
|
sb.WriteString(event.Host)
|
||||||
|
}
|
||||||
|
fmt.Println(sb.String())
|
||||||
|
}
|
||||||
|
if e.onFailureCallback != nil {
|
||||||
|
mockoutput.FailureCallback = e.onFailureCallback
|
||||||
|
}
|
||||||
|
e.customWriter = mockoutput
|
||||||
|
}
|
||||||
|
if e.customProgress == nil {
|
||||||
|
e.customProgress = &testutils.MockProgressClient{}
|
||||||
|
}
|
||||||
|
if e.hostErrCache == nil {
|
||||||
|
e.hostErrCache = hosterrorscache.New(30, hosterrorscache.DefaultMaxHostsCount, nil)
|
||||||
|
}
|
||||||
|
// setup interactsh
|
||||||
|
if e.interactshOpts != nil {
|
||||||
|
e.interactshOpts.Output = e.customWriter
|
||||||
|
e.interactshOpts.Progress = e.customProgress
|
||||||
|
} else {
|
||||||
|
e.interactshOpts = interactsh.DefaultOptions(e.customWriter, e.rc, e.customProgress)
|
||||||
|
}
|
||||||
|
if e.rateLimiter == nil {
|
||||||
|
e.rateLimiter = ratelimit.New(context.Background(), 150, time.Second)
|
||||||
|
}
|
||||||
|
// these templates are known to have weak matchers
|
||||||
|
// and idea is to disable them to avoid false positives
|
||||||
|
e.opts.ExcludeTags = config.ReadIgnoreFile().Tags
|
||||||
|
|
||||||
|
e.inputProvider = &inputs.SimpleInputProvider{
|
||||||
|
Inputs: []*contextargs.MetaInput{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init
|
||||||
|
func (e *NucleiEngine) init() error {
|
||||||
|
if e.opts.Verbose {
|
||||||
|
gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose)
|
||||||
|
} else if e.opts.Debug {
|
||||||
|
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
|
||||||
|
} else if e.opts.Silent {
|
||||||
|
gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runner.ValidateOptions(e.opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.opts.ProxyInternal && types.ProxyURL != "" || types.ProxySocksURL != "" {
|
||||||
|
httpclient, err := httpclientpool.Get(e.opts, &httpclientpool.Configuration{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.httpClient = httpclient
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = protocolstate.Init(e.opts)
|
||||||
|
_ = protocolinit.Init(e.opts)
|
||||||
|
e.applyRequiredDefaults()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// setup progressbar
|
||||||
|
if e.enableStats {
|
||||||
|
progressInstance, progressErr := progress.NewStatsTicker(e.opts.StatsInterval, e.enableStats, e.opts.StatsJSON, false, e.opts.MetricsPort)
|
||||||
|
if progressErr != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.customProgress = progressInstance
|
||||||
|
e.interactshOpts.Progress = progressInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := reporting.CreateConfigIfNotExists(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// we don't support reporting config in sdk mode
|
||||||
|
if e.rc, err = reporting.New(&reporting.Options{}, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.interactshOpts.IssuesClient = e.rc
|
||||||
|
if e.httpClient != nil {
|
||||||
|
e.interactshOpts.HTTPClient = e.httpClient
|
||||||
|
}
|
||||||
|
if e.interactshClient, err = interactsh.New(e.interactshOpts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||||
|
|
||||||
|
e.executerOpts = protocols.ExecutorOptions{
|
||||||
|
Output: e.customWriter,
|
||||||
|
Options: e.opts,
|
||||||
|
Progress: e.customProgress,
|
||||||
|
Catalog: e.catalog,
|
||||||
|
IssuesClient: e.rc,
|
||||||
|
RateLimiter: e.rateLimiter,
|
||||||
|
Interactsh: e.interactshClient,
|
||||||
|
HostErrorsCache: e.hostErrCache,
|
||||||
|
Colorizer: aurora.NewAurora(true),
|
||||||
|
ResumeCfg: types.NewResumeCfg(),
|
||||||
|
Browser: e.browserInstance,
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.opts.RateLimitMinute > 0 {
|
||||||
|
e.executerOpts.RateLimiter = ratelimit.New(context.Background(), uint(e.opts.RateLimitMinute), time.Minute)
|
||||||
|
} else if e.opts.RateLimit > 0 {
|
||||||
|
e.executerOpts.RateLimiter = ratelimit.New(context.Background(), uint(e.opts.RateLimit), time.Second)
|
||||||
|
} else {
|
||||||
|
e.executerOpts.RateLimiter = ratelimit.NewUnlimited(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
e.engine = core.New(e.opts)
|
||||||
|
e.engine.SetExecuterOptions(e.executerOpts)
|
||||||
|
|
||||||
|
httpxOptions := httpx.DefaultOptions
|
||||||
|
httpxOptions.Timeout = 5 * time.Second
|
||||||
|
if e.httpxClient, err = httpx.New(&httpxOptions); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only Happens once regardless how many times this function is called
|
||||||
|
// This will update ignore file to filter out templates with weak matchers to avoid false positives
|
||||||
|
// and also upgrade templates to latest version if available
|
||||||
|
installer.NucleiSDKVersionCheck()
|
||||||
|
|
||||||
|
return e.processUpdateCheckResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
type syncOnce struct {
|
||||||
|
sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateCheckInstance = &syncOnce{}
|
||||||
|
|
||||||
|
// processUpdateCheckResults processes update check results
|
||||||
|
func (e *NucleiEngine) processUpdateCheckResults() error {
|
||||||
|
var err error
|
||||||
|
updateCheckInstance.Do(func() {
|
||||||
|
if e.onUpdateAvailableCallback != nil {
|
||||||
|
e.onUpdateAvailableCallback(config.DefaultConfig.LatestNucleiTemplatesVersion)
|
||||||
|
}
|
||||||
|
tm := installer.TemplateManager{}
|
||||||
|
err = tm.UpdateIfOutdated()
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package nuclei_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
nuclei "github.com/projectdiscovery/nuclei/v2/lib"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimpleNuclei(t *testing.T) {
|
||||||
|
ne, err := nuclei.NewNucleiEngine(
|
||||||
|
nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}),
|
||||||
|
nuclei.EnableStatsWithOpts(nuclei.StatsOptions{JSON: true}),
|
||||||
|
)
|
||||||
|
require.Nil(t, err)
|
||||||
|
ne.LoadTargets([]string{"scanme.sh"}, false) // probe non http/https target is set to false here
|
||||||
|
// when callback is nil it nuclei will print JSON output to stdout
|
||||||
|
err = ne.ExecuteWithCallback(nil)
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer ne.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThreadSafeNuclei(t *testing.T) {
|
||||||
|
// create nuclei engine with options
|
||||||
|
ne, err := nuclei.NewThreadSafeNucleiEngine()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// scan 1 = run dns templates on scanme.sh
|
||||||
|
t.Run("scanme.sh", func(t *testing.T) {
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
|
||||||
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// scan 2 = run dns templates on honey.scanme.sh
|
||||||
|
t.Run("honey.scanme.sh", func(t *testing.T) {
|
||||||
|
err = ne.ExecuteNucleiWithOpts([]string{"honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
|
||||||
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// wait for all scans to finish
|
||||||
|
defer ne.Close()
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/gologger"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
||||||
|
@ -34,13 +35,36 @@ func (e *Engine) ExecuteScanWithOpts(templatesList []*templates.Template, target
|
||||||
results := &atomic.Bool{}
|
results := &atomic.Bool{}
|
||||||
selfcontainedWg := &sync.WaitGroup{}
|
selfcontainedWg := &sync.WaitGroup{}
|
||||||
|
|
||||||
|
totalReqBeforeCluster := getRequestCount(templatesList) * int(target.Count())
|
||||||
|
|
||||||
|
// attempt to cluster templates if noCluster is false
|
||||||
var finalTemplates []*templates.Template
|
var finalTemplates []*templates.Template
|
||||||
|
clusterCount := 0
|
||||||
if !noCluster {
|
if !noCluster {
|
||||||
finalTemplates, _ = templates.ClusterTemplates(templatesList, e.executerOpts)
|
finalTemplates, clusterCount = templates.ClusterTemplates(templatesList, e.executerOpts)
|
||||||
} else {
|
} else {
|
||||||
finalTemplates = templatesList
|
finalTemplates = templatesList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalReqAfterClustering := getRequestCount(finalTemplates) * int(target.Count())
|
||||||
|
|
||||||
|
if !noCluster && totalReqAfterClustering < totalReqBeforeCluster {
|
||||||
|
gologger.Info().Msgf("Templates clustered: %d (Reduced %d Requests)", clusterCount, totalReqBeforeCluster-totalReqAfterClustering)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 matches means no templates were found in the directory
|
||||||
|
if len(finalTemplates) == 0 {
|
||||||
|
return &atomic.Bool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.executerOpts.Progress != nil {
|
||||||
|
// Notes:
|
||||||
|
// workflow requests are not counted as they can be conditional
|
||||||
|
// templateList count is user requested templates count (before clustering)
|
||||||
|
// totalReqAfterClustering is total requests count after clustering
|
||||||
|
e.executerOpts.Progress.Init(target.Count(), len(templatesList), int64(totalReqAfterClustering))
|
||||||
|
}
|
||||||
|
|
||||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||||
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
||||||
// based on no of hosts , templates , stream and other optimization parameters
|
// based on no of hosts , templates , stream and other optimization parameters
|
||||||
|
@ -122,3 +146,17 @@ func (e *Engine) executeHostSpray(templatesList []*templates.Template, target In
|
||||||
wp.Wait()
|
wp.Wait()
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns total requests count
|
||||||
|
func getRequestCount(templates []*templates.Template) int {
|
||||||
|
count := 0
|
||||||
|
for _, template := range templates {
|
||||||
|
// ignore requests in workflows as total requests in workflow
|
||||||
|
// depends on what templates will be called in workflow
|
||||||
|
if len(template.Workflows) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count += template.TotalRequests
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWorkflowsSimple(t *testing.T) {
|
func TestWorkflowsSimple(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
{Executers: []*workflows.ProtocolExecuterPair{{
|
{Executers: []*workflows.ProtocolExecuterPair{{
|
||||||
|
@ -29,7 +29,7 @@ func TestWorkflowsSimple(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkflowsSimpleMultiple(t *testing.T) {
|
func TestWorkflowsSimpleMultiple(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
var firstInput, secondInput string
|
var firstInput, secondInput string
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
|
@ -54,7 +54,7 @@ func TestWorkflowsSimpleMultiple(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkflowsSubtemplates(t *testing.T) {
|
func TestWorkflowsSubtemplates(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
var firstInput, secondInput string
|
var firstInput, secondInput string
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
|
@ -80,7 +80,7 @@ func TestWorkflowsSubtemplates(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkflowsSubtemplatesNoMatch(t *testing.T) {
|
func TestWorkflowsSubtemplatesNoMatch(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
var firstInput, secondInput string
|
var firstInput, secondInput string
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
|
@ -104,7 +104,7 @@ func TestWorkflowsSubtemplatesNoMatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkflowsSubtemplatesWithMatcher(t *testing.T) {
|
func TestWorkflowsSubtemplatesWithMatcher(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
var firstInput, secondInput string
|
var firstInput, secondInput string
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
|
@ -133,7 +133,7 @@ func TestWorkflowsSubtemplatesWithMatcher(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkflowsSubtemplatesWithMatcherNoMatch(t *testing.T) {
|
func TestWorkflowsSubtemplatesWithMatcherNoMatch(t *testing.T) {
|
||||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
var firstInput, secondInput string
|
var firstInput, secondInput string
|
||||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 10}}, Workflows: []*workflows.WorkflowTemplate{
|
||||||
|
|
|
@ -4,10 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -44,13 +41,12 @@ type StatsTicker struct {
|
||||||
cloud bool
|
cloud bool
|
||||||
active bool
|
active bool
|
||||||
outputJSON bool
|
outputJSON bool
|
||||||
server *http.Server
|
|
||||||
stats clistats.StatisticsClient
|
stats clistats.StatisticsClient
|
||||||
tickDuration time.Duration
|
tickDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStatsTicker creates and returns a new progress tracking object.
|
// NewStatsTicker creates and returns a new progress tracking object.
|
||||||
func NewStatsTicker(duration int, active, outputJSON, metrics, cloud bool, port int) (Progress, error) {
|
func NewStatsTicker(duration int, active, outputJSON, cloud bool, port int) (Progress, error) {
|
||||||
var tickDuration time.Duration
|
var tickDuration time.Duration
|
||||||
if active && duration != -1 {
|
if active && duration != -1 {
|
||||||
tickDuration = time.Duration(duration) * time.Second
|
tickDuration = time.Duration(duration) * time.Second
|
||||||
|
@ -60,7 +56,12 @@ func NewStatsTicker(duration int, active, outputJSON, metrics, cloud bool, port
|
||||||
|
|
||||||
progress := &StatsTicker{}
|
progress := &StatsTicker{}
|
||||||
|
|
||||||
stats, err := clistats.New()
|
statsOpts := &clistats.DefaultOptions
|
||||||
|
statsOpts.ListenPort = port
|
||||||
|
// metrics port is enabled by default and is not configurable with new version of clistats
|
||||||
|
// by default 63636 is used and than can be modified with -mp flag
|
||||||
|
|
||||||
|
stats, err := clistats.NewWithOptions(context.TODO(), statsOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -70,21 +71,6 @@ func NewStatsTicker(duration int, active, outputJSON, metrics, cloud bool, port
|
||||||
progress.tickDuration = tickDuration
|
progress.tickDuration = tickDuration
|
||||||
progress.outputJSON = outputJSON
|
progress.outputJSON = outputJSON
|
||||||
|
|
||||||
if metrics {
|
|
||||||
http.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
metrics := progress.getMetrics()
|
|
||||||
_ = json.NewEncoder(w).Encode(metrics)
|
|
||||||
})
|
|
||||||
progress.server = &http.Server{
|
|
||||||
Addr: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)),
|
|
||||||
Handler: http.DefaultServeMux,
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
if err := progress.server.ListenAndServe(); err != nil {
|
|
||||||
gologger.Warning().Msgf("Could not serve metrics: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return progress, nil
|
return progress, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +96,7 @@ func (p *StatsTicker) Init(hostCount int64, rulesCount int, requestCount int64)
|
||||||
gologger.Warning().Msgf("Couldn't start statistics: %s", err)
|
gologger.Warning().Msgf("Couldn't start statistics: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: this is needed and is responsible for the tick event
|
||||||
p.stats.GetStatResponse(p.tickDuration, func(s string, err error) error {
|
p.stats.GetStatResponse(p.tickDuration, func(s string, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gologger.Warning().Msgf("Could not read statistics: %s\n", err)
|
gologger.Warning().Msgf("Could not read statistics: %s\n", err)
|
||||||
|
@ -265,11 +252,6 @@ func metricsMap(stats clistats.StatisticsClient) map[string]interface{} {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMetrics returns a map of important metrics for client
|
|
||||||
func (p *StatsTicker) getMetrics() map[string]interface{} {
|
|
||||||
return metricsMap(p.stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmtDuration formats the duration for the time elapsed
|
// fmtDuration formats the duration for the time elapsed
|
||||||
func fmtDuration(d time.Duration) string {
|
func fmtDuration(d time.Duration) string {
|
||||||
d = d.Round(time.Second)
|
d = d.Round(time.Second)
|
||||||
|
@ -294,7 +276,4 @@ func (p *StatsTicker) Stop() {
|
||||||
gologger.Warning().Msgf("Couldn't stop statistics: %s", err)
|
gologger.Warning().Msgf("Couldn't stop statistics: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.server != nil {
|
|
||||||
_ = p.server.Shutdown(context.Background())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ var executerOpts protocols.ExecutorOptions
|
||||||
func setup() {
|
func setup() {
|
||||||
options := testutils.DefaultOptions
|
options := testutils.DefaultOptions
|
||||||
testutils.Init(options)
|
testutils.Init(options)
|
||||||
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
executerOpts = protocols.ExecutorOptions{
|
executerOpts = protocols.ExecutorOptions{
|
||||||
Output: testutils.NewMockOutputWriter(),
|
Output: testutils.NewMockOutputWriter(),
|
||||||
|
|
|
@ -77,7 +77,7 @@ type TemplateInfo struct {
|
||||||
|
|
||||||
// NewMockExecuterOptions creates a new mock executeroptions struct
|
// NewMockExecuterOptions creates a new mock executeroptions struct
|
||||||
func NewMockExecuterOptions(options *types.Options, info *TemplateInfo) *protocols.ExecutorOptions {
|
func NewMockExecuterOptions(options *types.Options, info *TemplateInfo) *protocols.ExecutorOptions {
|
||||||
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
executerOpts := &protocols.ExecutorOptions{
|
executerOpts := &protocols.ExecutorOptions{
|
||||||
TemplateID: info.ID,
|
TemplateID: info.ID,
|
||||||
TemplateInfo: info.Info,
|
TemplateInfo: info.Info,
|
||||||
|
@ -105,6 +105,7 @@ func (n *NoopWriter) Write(data []byte, level levels.Level) {}
|
||||||
type MockOutputWriter struct {
|
type MockOutputWriter struct {
|
||||||
aurora aurora.Aurora
|
aurora aurora.Aurora
|
||||||
RequestCallback func(templateID, url, requestType string, err error)
|
RequestCallback func(templateID, url, requestType string, err error)
|
||||||
|
FailureCallback func(result *output.InternalEvent)
|
||||||
WriteCallback func(o *output.ResultEvent)
|
WriteCallback func(o *output.ResultEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +139,9 @@ func (m *MockOutputWriter) Request(templateID, url, requestType string, err erro
|
||||||
|
|
||||||
// WriteFailure writes the event to file and/or screen.
|
// WriteFailure writes the event to file and/or screen.
|
||||||
func (m *MockOutputWriter) WriteFailure(result output.InternalEvent) error {
|
func (m *MockOutputWriter) WriteFailure(result output.InternalEvent) error {
|
||||||
|
if m.FailureCallback != nil && result != nil {
|
||||||
|
m.FailureCallback(&result)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (m *MockOutputWriter) WriteStoreDebugData(host, templateID, eventType string, data string) {
|
func (m *MockOutputWriter) WriteStoreDebugData(host, templateID, eventType string, data string) {
|
||||||
|
|
|
@ -23,7 +23,7 @@ var executerOpts protocols.ExecutorOptions
|
||||||
func setup() {
|
func setup() {
|
||||||
options := testutils.DefaultOptions
|
options := testutils.DefaultOptions
|
||||||
testutils.Init(options)
|
testutils.Init(options)
|
||||||
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
executerOpts = protocols.ExecutorOptions{
|
executerOpts = protocols.ExecutorOptions{
|
||||||
Output: testutils.NewMockOutputWriter(),
|
Output: testutils.NewMockOutputWriter(),
|
||||||
|
|
|
@ -23,7 +23,7 @@ var executerOpts protocols.ExecutorOptions
|
||||||
func setup() {
|
func setup() {
|
||||||
options := testutils.DefaultOptions
|
options := testutils.DefaultOptions
|
||||||
testutils.Init(options)
|
testutils.Init(options)
|
||||||
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, false, 0)
|
progressImpl, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||||
|
|
||||||
executerOpts = protocols.ExecutorOptions{
|
executerOpts = protocols.ExecutorOptions{
|
||||||
Output: testutils.NewMockOutputWriter(),
|
Output: testutils.NewMockOutputWriter(),
|
||||||
|
|
|
@ -215,7 +215,7 @@ type Options struct {
|
||||||
SystemResolvers bool
|
SystemResolvers bool
|
||||||
// ShowActions displays a list of all headless actions
|
// ShowActions displays a list of all headless actions
|
||||||
ShowActions bool
|
ShowActions bool
|
||||||
// Metrics enables display of metrics via an http endpoint
|
// Deprecated: Enabled by default through clistats . Metrics enables display of metrics via an http endpoint
|
||||||
Metrics bool
|
Metrics bool
|
||||||
// Debug mode allows debugging request/responses for the engine
|
// Debug mode allows debugging request/responses for the engine
|
||||||
Debug bool
|
Debug bool
|
||||||
|
|
Loading…
Reference in New Issue