mirror of https://github.com/daffainfo/nuclei.git
Feature 18 cloud flags (#2708)
* Add cloud flags for nuclei. * Add flag to get output for a particular scan ID * Add some comments to the function. * Get timestamp and id for scan list * Fix linting errors * Check if type is enumeration. * Do not show deleted scans. * Do not use filter_result, create client once and use it everywhere with runner. * Fix the output of scan list to be better * Format the nuclei scan output list. * Remove unused constant * misc option update Co-authored-by: sandeep <sandeep@projectdiscovery.io>dev
parent
2006060574
commit
4cfde111f4
|
@ -280,6 +280,10 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||||
flagSet.BoolVar(&options.Cloud, "cloud", false, "run scan on nuclei cloud"),
|
flagSet.BoolVar(&options.Cloud, "cloud", false, "run scan on nuclei cloud"),
|
||||||
flagSet.StringVarEnv(&options.CloudURL, "cloud-server", "cs", "http://cloud-dev.nuclei.sh", "NUCLEI_CLOUD_SERVER", "nuclei cloud server to use"),
|
flagSet.StringVarEnv(&options.CloudURL, "cloud-server", "cs", "http://cloud-dev.nuclei.sh", "NUCLEI_CLOUD_SERVER", "nuclei cloud server to use"),
|
||||||
flagSet.StringVarEnv(&options.CloudAPIKey, "cloud-api-key", "ak", "", "NUCLEI_CLOUD_APIKEY", "api-key for the nuclei cloud server"),
|
flagSet.StringVarEnv(&options.CloudAPIKey, "cloud-api-key", "ak", "", "NUCLEI_CLOUD_APIKEY", "api-key for the nuclei cloud server"),
|
||||||
|
flagSet.BoolVarP(&options.ScanList, "list-scan", "ls", false, "list cloud scans."),
|
||||||
|
flagSet.BoolVarP(&options.NoStore, "no-store", "ns", false, "disable scan/output storage on cloud"),
|
||||||
|
flagSet.StringVarP(&options.DeleteScan, "delete-scan", "ds", "", "delete scan/output on cloud by scan id"),
|
||||||
|
flagSet.StringVarP(&options.ScanOutput, "scan-output", "so", "", "display scan output by scan id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
_ = flagSet.Parse()
|
_ = flagSet.Parse()
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DDMMYYYYhhmmss = "2006-01-02 15:04:05"
|
||||||
|
|
||||||
// runStandardEnumeration runs standard enumeration
|
// runStandardEnumeration runs standard enumeration
|
||||||
func (r *Runner) runStandardEnumeration(executerOpts protocols.ExecuterOptions, store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
|
func (r *Runner) runStandardEnumeration(executerOpts protocols.ExecuterOptions, store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
|
||||||
if r.options.AutomaticScan {
|
if r.options.AutomaticScan {
|
||||||
|
@ -22,13 +24,53 @@ func (r *Runner) runStandardEnumeration(executerOpts protocols.ExecuterOptions,
|
||||||
return r.executeTemplatesInput(store, engine)
|
return r.executeTemplatesInput(store, engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all the scan lists for a user/apikey.
|
||||||
|
func (r *Runner) getScanList() error {
|
||||||
|
items, err := r.cloudClient.GetScans()
|
||||||
|
loc, _ := time.LoadLocation("Local")
|
||||||
|
|
||||||
|
for _, v := range items {
|
||||||
|
status := "FINISHED"
|
||||||
|
t := v.FinishedAt
|
||||||
|
duration := t.Sub(v.CreatedAt)
|
||||||
|
if !v.Finished {
|
||||||
|
status = "RUNNING"
|
||||||
|
t = time.Now().UTC()
|
||||||
|
duration = t.Sub(v.CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := v.CreatedAt.In(loc).Format(DDMMYYYYhhmmss)
|
||||||
|
|
||||||
|
gologger.Silent().Msgf("%s [%s] [STATUS: %s] [MATCHED: %d] [TARGETS: %d] [TEMPLATES: %d] [DURATION: %s]\n", v.Id, val, status, v.Matches, v.Targets, v.Templates, duration)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) deleteScan(id string) error {
|
||||||
|
deleted, err := r.cloudClient.DeleteScan(id)
|
||||||
|
if !deleted.OK {
|
||||||
|
gologger.Info().Msgf("Error in deleting the scan %s.", id)
|
||||||
|
} else {
|
||||||
|
gologger.Info().Msgf("Scan deleted %s.", id)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) getResults(id string) error {
|
||||||
|
err := r.cloudClient.GetResults(id, func(re *output.ResultEvent) {
|
||||||
|
if outputErr := r.output.Write(re); outputErr != nil {
|
||||||
|
gologger.Warning().Msgf("Could not write output: %s", outputErr)
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// runCloudEnumeration runs cloud based enumeration
|
// runCloudEnumeration runs cloud based enumeration
|
||||||
func (r *Runner) runCloudEnumeration(store *loader.Store) (*atomic.Bool, error) {
|
func (r *Runner) runCloudEnumeration(store *loader.Store, nostore bool) (*atomic.Bool, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
gologger.Info().Msgf("Scan execution took %s", time.Since(now))
|
gologger.Info().Msgf("Scan execution took %s", time.Since(now))
|
||||||
}()
|
}()
|
||||||
client := nucleicloud.New(r.options.CloudURL, r.options.CloudAPIKey)
|
|
||||||
|
|
||||||
results := &atomic.Bool{}
|
results := &atomic.Bool{}
|
||||||
|
|
||||||
|
@ -41,9 +83,10 @@ func (r *Runner) runCloudEnumeration(store *loader.Store) (*atomic.Bool, error)
|
||||||
for _, template := range store.Templates() {
|
for _, template := range store.Templates() {
|
||||||
templates = append(templates, getTemplateRelativePath(template.Path))
|
templates = append(templates, getTemplateRelativePath(template.Path))
|
||||||
}
|
}
|
||||||
taskID, err := client.AddScan(&nucleicloud.AddScanRequest{
|
taskID, err := r.cloudClient.AddScan(&nucleicloud.AddScanRequest{
|
||||||
RawTargets: targets,
|
RawTargets: targets,
|
||||||
PublicTemplates: templates,
|
PublicTemplates: templates,
|
||||||
|
IsTemporary: nostore,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, err
|
return results, err
|
||||||
|
@ -51,7 +94,7 @@ func (r *Runner) runCloudEnumeration(store *loader.Store) (*atomic.Bool, error)
|
||||||
gologger.Info().Msgf("Created task with ID: %s", taskID)
|
gologger.Info().Msgf("Created task with ID: %s", taskID)
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
err = client.GetResults(taskID, func(re *output.ResultEvent) {
|
err = r.cloudClient.GetResults(taskID, func(re *output.ResultEvent) {
|
||||||
results.CompareAndSwap(false, true)
|
results.CompareAndSwap(false, true)
|
||||||
|
|
||||||
if outputErr := r.output.Write(re); outputErr != nil {
|
if outputErr := r.output.Write(re); outputErr != nil {
|
||||||
|
@ -62,7 +105,7 @@ func (r *Runner) runCloudEnumeration(store *loader.Store) (*atomic.Bool, error)
|
||||||
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
|
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}, true)
|
||||||
return results, err
|
return results, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ type Client struct {
|
||||||
const (
|
const (
|
||||||
pollInterval = 1 * time.Second
|
pollInterval = 1 * time.Second
|
||||||
defaultBaseURL = "http://webapp.localhost"
|
defaultBaseURL = "http://webapp.localhost"
|
||||||
|
resultSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// New returns a nuclei-cloud API client
|
// New returns a nuclei-cloud API client
|
||||||
|
@ -72,10 +73,11 @@ func (c *Client) AddScan(req *AddScanRequest) (string, error) {
|
||||||
|
|
||||||
// GetResults gets results from nuclei server for an ID
|
// GetResults gets results from nuclei server for an ID
|
||||||
// until there are no more results left to retrieve.
|
// until there are no more results left to retrieve.
|
||||||
func (c *Client) GetResults(ID string, callback func(*output.ResultEvent)) error {
|
func (c *Client) GetResults(ID string, callback func(*output.ResultEvent), checkProgress bool) error {
|
||||||
lastID := int64(0)
|
lastID := int64(0)
|
||||||
for {
|
for {
|
||||||
httpReq, err := retryablehttp.NewRequest(http.MethodGet, fmt.Sprintf("%s/results?id=%s&from=%d&size=100", c.baseURL, ID, lastID), nil)
|
uri := fmt.Sprintf("%s/results?id=%s&from=%d&size=%d", c.baseURL, ID, lastID, resultSize)
|
||||||
|
httpReq, err := retryablehttp.NewRequest(http.MethodGet, uri, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not make request")
|
return errors.Wrap(err, "could not make request")
|
||||||
}
|
}
|
||||||
|
@ -106,10 +108,76 @@ func (c *Client) GetResults(ID string, callback func(*output.ResultEvent)) error
|
||||||
}
|
}
|
||||||
callback(&result)
|
callback(&result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//This is checked during scan is added else if no item found break out of loop.
|
||||||
|
if checkProgress {
|
||||||
if items.Finished && len(items.Items) == 0 {
|
if items.Finished && len(items.Items) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
} else if len(items.Items) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(pollInterval)
|
time.Sleep(pollInterval)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetScans() ([]GetScanRequest, error) {
|
||||||
|
var items []GetScanRequest
|
||||||
|
httpReq, err := retryablehttp.NewRequest(http.MethodGet, fmt.Sprintf("%s/scan", c.baseURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return items, errors.Wrap(err, "could not make request")
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("X-API-Key", c.apiKey)
|
||||||
|
|
||||||
|
resp, err := c.httpclient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return items, errors.Wrap(err, "could not make request.")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return items, errors.Wrap(err, "could not do get response.")
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
return items, errors.Errorf("could not do request %d: %s", resp.StatusCode, string(data))
|
||||||
|
}
|
||||||
|
if err := jsoniter.NewDecoder(resp.Body).Decode(&items); err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return items, errors.Wrap(err, "could not decode results")
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Delete a scan and it's issues by the scan id.
|
||||||
|
func (c *Client) DeleteScan(id string) (DeleteScanResults, error) {
|
||||||
|
deletescan := DeleteScanResults{}
|
||||||
|
httpReq, err := retryablehttp.NewRequest(http.MethodDelete, fmt.Sprintf("%s/scan?id=%s", c.baseURL, id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return deletescan, errors.Wrap(err, "could not make request")
|
||||||
|
}
|
||||||
|
httpReq.Header.Set("X-API-Key", c.apiKey)
|
||||||
|
|
||||||
|
resp, err := c.httpclient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return deletescan, errors.Wrap(err, "could not make request")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return deletescan, errors.Wrap(err, "could not do get result request")
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
data, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
return deletescan, errors.Errorf("could not do request %d: %s", resp.StatusCode, string(data))
|
||||||
|
}
|
||||||
|
if err := jsoniter.NewDecoder(resp.Body).Decode(&deletescan); err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return deletescan, errors.Wrap(err, "could not delete scan")
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
return deletescan, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package nucleicloud
|
package nucleicloud
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// AddScanRequest is a nuclei scan input item.
|
// AddScanRequest is a nuclei scan input item.
|
||||||
type AddScanRequest struct {
|
type AddScanRequest struct {
|
||||||
// RawTargets is a list of raw target URLs for the scan.
|
// RawTargets is a list of raw target URLs for the scan.
|
||||||
|
@ -9,6 +11,7 @@ type AddScanRequest struct {
|
||||||
// PrivateTemplates is a map of template-name->contents that
|
// PrivateTemplates is a map of template-name->contents that
|
||||||
// are private to the user executing the scan. (TODO: TBD)
|
// are private to the user executing the scan. (TODO: TBD)
|
||||||
PrivateTemplates map[string]string `json:"private_templates,omitempty"`
|
PrivateTemplates map[string]string `json:"private_templates,omitempty"`
|
||||||
|
IsTemporary bool `json:"is_temporary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetResultsResponse struct {
|
type GetResultsResponse struct {
|
||||||
|
@ -16,7 +19,23 @@ type GetResultsResponse struct {
|
||||||
Items []GetResultsResponseItem `json:"items"`
|
Items []GetResultsResponseItem `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetScanRequest struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Total int32 `json:"total"`
|
||||||
|
Current int32 `json:"current"`
|
||||||
|
Finished bool `json:"finished"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
FinishedAt time.Time `json:"finished_at"`
|
||||||
|
Targets int32 `json:"targets"`
|
||||||
|
Templates int32 `json:"templates"`
|
||||||
|
Matches int64 `json:"matches"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetResultsResponseItem struct {
|
type GetResultsResponseItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Raw string `json:"raw"`
|
Raw string `json:"raw"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteScanResults struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/internal/runner/nucleicloud"
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -70,6 +72,7 @@ type Runner struct {
|
||||||
hostErrors hosterrorscache.CacheInterface
|
hostErrors hosterrorscache.CacheInterface
|
||||||
resumeCfg *types.ResumeCfg
|
resumeCfg *types.ResumeCfg
|
||||||
pprofServer *http.Server
|
pprofServer *http.Server
|
||||||
|
cloudClient *nucleicloud.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
const pprofServerAddress = "127.0.0.1:8086"
|
const pprofServerAddress = "127.0.0.1:8086"
|
||||||
|
@ -85,6 +88,10 @@ func New(options *types.Options) (*Runner, error) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.Cloud {
|
||||||
|
runner.cloudClient = nucleicloud.New(options.CloudURL, options.CloudAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
if options.UpdateNuclei {
|
if options.UpdateNuclei {
|
||||||
if err := updateNucleiVersionToLatest(runner.options.Verbose); err != nil {
|
if err := updateNucleiVersionToLatest(runner.options.Verbose); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -418,12 +425,27 @@ func (r *Runner) RunEnumeration() error {
|
||||||
executerOpts.InputHelper.InputsHTTP = inputHelpers
|
executerOpts.InputHelper.InputsHTTP = inputHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enumeration := false
|
||||||
var results *atomic.Bool
|
var results *atomic.Bool
|
||||||
if r.options.Cloud {
|
if r.options.Cloud {
|
||||||
|
if r.options.ScanList {
|
||||||
|
err = r.getScanList()
|
||||||
|
} else if r.options.DeleteScan != "" {
|
||||||
|
err = r.deleteScan(r.options.DeleteScan)
|
||||||
|
} else if r.options.ScanOutput != "" {
|
||||||
|
err = r.getResults(r.options.ScanOutput)
|
||||||
|
} else {
|
||||||
gologger.Info().Msgf("Running scan on cloud with URL %s", r.options.CloudURL)
|
gologger.Info().Msgf("Running scan on cloud with URL %s", r.options.CloudURL)
|
||||||
results, err = r.runCloudEnumeration(store)
|
results, err = r.runCloudEnumeration(store, r.options.NoStore)
|
||||||
|
enumeration = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
results, err = r.runStandardEnumeration(executerOpts, store, engine)
|
results, err = r.runStandardEnumeration(executerOpts, store, engine)
|
||||||
|
enumeration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enumeration {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.interactsh != nil {
|
if r.interactsh != nil {
|
||||||
|
|
|
@ -95,6 +95,14 @@ type Options struct {
|
||||||
CloudURL string
|
CloudURL string
|
||||||
// CloudAPIKey is the api-key for the nuclei cloud endpoint
|
// CloudAPIKey is the api-key for the nuclei cloud endpoint
|
||||||
CloudAPIKey string
|
CloudAPIKey string
|
||||||
|
// Scanlist feature to get all the scan ids for a user
|
||||||
|
ScanList bool
|
||||||
|
// Nostore
|
||||||
|
NoStore bool
|
||||||
|
// Delete scan
|
||||||
|
DeleteScan string
|
||||||
|
// Get issues for a scan
|
||||||
|
ScanOutput string
|
||||||
// ResolversFile is a file containing resolvers for nuclei.
|
// ResolversFile is a file containing resolvers for nuclei.
|
||||||
ResolversFile string
|
ResolversFile string
|
||||||
// StatsInterval is the number of seconds to display stats after
|
// StatsInterval is the number of seconds to display stats after
|
||||||
|
|
Loading…
Reference in New Issue