mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 19:05:26 +00:00
* introducing execution id * wip * . * adding separate execution context id * lint * vet * fixing pg dialers * test ignore * fixing loader FD limit * test * fd fix * wip: remove CloseProcesses() from dev merge * wip: fix merge issue * protocolstate: stop memguarding on last dialer delete * avoid data race in dialers.RawHTTPClient * use shared logger and avoid race conditions * use shared logger and avoid race conditions * go mod * patch executionId into compiled template cache * clean up comment in Parse * go mod update * bump echarts * address merge issues * fix use of gologger * switch cmd/nuclei to options.Logger * address merge issues with go.mod * go vet: address copy of lock with new Copy function * fixing tests * disable speed control * fix nil ExecuterOptions * removing deprecated code * fixing result print * default logger * cli default logger * filter warning from results * fix performance test * hardcoding path * disable upload * refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg` Signed-off-by: Dwi Siswanto <git@dw1.io> * Revert "disable upload" This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682. * Revert "hardcoding path" This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00. --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
297 lines
8.8 KiB
Go
297 lines
8.8 KiB
Go
package server
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/alitto/pond"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/projectdiscovery/gologger"
|
|
"github.com/projectdiscovery/nuclei/v3/internal/server/scope"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
|
"github.com/projectdiscovery/utils/env"
|
|
)
|
|
|
|
// DASTServer is a server that performs execution of fuzzing templates
|
|
// on user input passed to the API.
|
|
type DASTServer struct {
|
|
echo *echo.Echo
|
|
options *Options
|
|
tasksPool *pond.WorkerPool
|
|
deduplicator *requestDeduplicator
|
|
scopeManager *scope.Manager
|
|
startTime time.Time
|
|
|
|
// metrics
|
|
endpointsInQueue atomic.Int64
|
|
endpointsBeingTested atomic.Int64
|
|
|
|
nucleiExecutor *nucleiExecutor
|
|
}
|
|
|
|
// Options contains the configuration options for the server.
|
|
type Options struct {
|
|
// Address is the address to bind the server to
|
|
Address string
|
|
// Token is the token to use for authentication (optional)
|
|
Token string
|
|
// Templates is the list of templates to use for fuzzing
|
|
Templates []string
|
|
// Verbose is a flag that controls verbose output
|
|
Verbose bool
|
|
|
|
// Scope fields for fuzzer
|
|
InScope []string
|
|
OutScope []string
|
|
|
|
OutputWriter output.Writer
|
|
|
|
NucleiExecutorOptions *NucleiExecutorOptions
|
|
}
|
|
|
|
// New creates a new instance of the DAST server.
|
|
func New(options *Options) (*DASTServer, error) {
|
|
// If the user has specified no templates, use the default ones
|
|
// for DAST only.
|
|
if len(options.Templates) == 0 {
|
|
options.Templates = []string{"dast/"}
|
|
}
|
|
// Disable bulk mode and single threaded execution
|
|
// by auto adjusting in case of default values
|
|
if options.NucleiExecutorOptions.Options.BulkSize == 25 && options.NucleiExecutorOptions.Options.TemplateThreads == 25 {
|
|
options.NucleiExecutorOptions.Options.BulkSize = 1
|
|
options.NucleiExecutorOptions.Options.TemplateThreads = 1
|
|
}
|
|
maxWorkers := env.GetEnvOrDefault[int]("FUZZ_MAX_WORKERS", 1)
|
|
bufferSize := env.GetEnvOrDefault[int]("FUZZ_BUFFER_SIZE", 10000)
|
|
|
|
server := &DASTServer{
|
|
options: options,
|
|
tasksPool: pond.New(maxWorkers, bufferSize),
|
|
deduplicator: newRequestDeduplicator(),
|
|
startTime: time.Now(),
|
|
}
|
|
server.setupHandlers(false)
|
|
|
|
executor, err := newNucleiExecutor(options.NucleiExecutorOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server.nucleiExecutor = executor
|
|
|
|
scopeManager, err := scope.NewManager(
|
|
options.InScope,
|
|
options.OutScope,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
server.scopeManager = scopeManager
|
|
|
|
var builder strings.Builder
|
|
gologger.Debug().Msgf("Using %d parallel tasks with %d buffer", maxWorkers, bufferSize)
|
|
if options.Token != "" {
|
|
builder.WriteString(" (with token)")
|
|
}
|
|
gologger.Info().Msgf("DAST Server API: %s", server.buildURL("/fuzz"))
|
|
gologger.Info().Msgf("DAST Server Stats URL: %s", server.buildURL("/stats"))
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func NewStatsServer(fuzzStatsDB *stats.Tracker) (*DASTServer, error) {
|
|
server := &DASTServer{
|
|
nucleiExecutor: &nucleiExecutor{
|
|
executorOpts: &protocols.ExecutorOptions{
|
|
FuzzStatsDB: fuzzStatsDB,
|
|
},
|
|
},
|
|
}
|
|
server.setupHandlers(true)
|
|
gologger.Info().Msgf("Stats UI URL: %s", server.buildURL("/stats"))
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func (s *DASTServer) Close() {
|
|
s.nucleiExecutor.Close()
|
|
_ = s.echo.Close()
|
|
s.tasksPool.StopAndWaitFor(1 * time.Minute)
|
|
}
|
|
|
|
func (s *DASTServer) buildURL(endpoint string) string {
|
|
values := make(url.Values)
|
|
if s.options.Token != "" {
|
|
values.Set("token", s.options.Token)
|
|
}
|
|
|
|
// Use url.URL struct to safely construct the URL
|
|
u := &url.URL{
|
|
Scheme: "http",
|
|
Host: s.options.Address,
|
|
Path: endpoint,
|
|
RawQuery: values.Encode(),
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
func (s *DASTServer) setupHandlers(onlyStats bool) {
|
|
e := echo.New()
|
|
e.Use(middleware.Recover())
|
|
if s.options.Verbose {
|
|
cfg := middleware.DefaultLoggerConfig
|
|
cfg.Skipper = func(c echo.Context) bool {
|
|
// Skip /stats and /stats.json
|
|
return c.Request().URL.Path == "/stats" || c.Request().URL.Path == "/stats.json"
|
|
}
|
|
e.Use(middleware.LoggerWithConfig(cfg))
|
|
}
|
|
e.Use(middleware.CORS())
|
|
|
|
if s.options.Token != "" {
|
|
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
|
|
KeyLookup: "query:token",
|
|
Validator: func(key string, c echo.Context) (bool, error) {
|
|
return key == s.options.Token, nil
|
|
},
|
|
}))
|
|
}
|
|
|
|
e.HideBanner = true
|
|
// POST /fuzz - Queue a request for fuzzing
|
|
if !onlyStats {
|
|
e.POST("/fuzz", s.handleRequest)
|
|
}
|
|
e.GET("/stats", s.handleStats)
|
|
e.GET("/stats.json", s.handleStatsJSON)
|
|
|
|
s.echo = e
|
|
}
|
|
|
|
func (s *DASTServer) Start() error {
|
|
if err := s.echo.Start(s.options.Address); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PostReuestsHandlerRequest is the request body for the /fuzz POST handler.
|
|
type PostRequestsHandlerRequest struct {
|
|
RawHTTP string `json:"raw_http"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (s *DASTServer) handleRequest(c echo.Context) error {
|
|
var req PostRequestsHandlerRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
fmt.Printf("Error binding request: %s\n", err)
|
|
return err
|
|
}
|
|
|
|
// Validate the request
|
|
if req.RawHTTP == "" || req.URL == "" {
|
|
fmt.Printf("Missing required fields\n")
|
|
return c.JSON(400, map[string]string{"error": "missing required fields"})
|
|
}
|
|
|
|
s.endpointsInQueue.Add(1)
|
|
s.tasksPool.Submit(func() {
|
|
s.consumeTaskRequest(req)
|
|
})
|
|
return c.NoContent(200)
|
|
}
|
|
|
|
type StatsResponse struct {
|
|
DASTServerInfo DASTServerInfo `json:"dast_server_info"`
|
|
DASTScanStatistics DASTScanStatistics `json:"dast_scan_statistics"`
|
|
DASTScanStatusStatistics map[string]int64 `json:"dast_scan_status_statistics"`
|
|
DASTScanSeverityBreakdown map[string]int64 `json:"dast_scan_severity_breakdown"`
|
|
DASTScanErrorStatistics map[string]int64 `json:"dast_scan_error_statistics"`
|
|
DASTScanStartTime time.Time `json:"dast_scan_start_time"`
|
|
}
|
|
|
|
type DASTServerInfo struct {
|
|
NucleiVersion string `json:"nuclei_version"`
|
|
NucleiTemplateVersion string `json:"nuclei_template_version"`
|
|
NucleiDastServerAPI string `json:"nuclei_dast_server_api"`
|
|
ServerAuthEnabled bool `json:"sever_auth_enabled"`
|
|
}
|
|
|
|
type DASTScanStatistics struct {
|
|
EndpointsInQueue int64 `json:"endpoints_in_queue"`
|
|
EndpointsBeingTested int64 `json:"endpoints_being_tested"`
|
|
TotalTemplatesLoaded int64 `json:"total_dast_templates_loaded"`
|
|
TotalTemplatesTested int64 `json:"total_dast_templates_tested"`
|
|
TotalMatchedResults int64 `json:"total_matched_results"`
|
|
TotalComponentsTested int64 `json:"total_components_tested"`
|
|
TotalEndpointsTested int64 `json:"total_endpoints_tested"`
|
|
TotalFuzzedRequests int64 `json:"total_fuzzed_requests"`
|
|
TotalErroredRequests int64 `json:"total_errored_requests"`
|
|
}
|
|
|
|
func (s *DASTServer) getStats() (StatsResponse, error) {
|
|
cfg := config.DefaultConfig
|
|
|
|
resp := StatsResponse{
|
|
DASTServerInfo: DASTServerInfo{
|
|
NucleiVersion: config.Version,
|
|
NucleiTemplateVersion: cfg.TemplateVersion,
|
|
NucleiDastServerAPI: s.buildURL("/fuzz"),
|
|
ServerAuthEnabled: s.options.Token != "",
|
|
},
|
|
DASTScanStartTime: s.startTime,
|
|
DASTScanStatistics: DASTScanStatistics{
|
|
EndpointsInQueue: s.endpointsInQueue.Load(),
|
|
EndpointsBeingTested: s.endpointsBeingTested.Load(),
|
|
TotalTemplatesLoaded: int64(len(s.nucleiExecutor.store.Templates())),
|
|
},
|
|
}
|
|
if s.nucleiExecutor.executorOpts.FuzzStatsDB != nil {
|
|
fuzzStats := s.nucleiExecutor.executorOpts.FuzzStatsDB.GetStats()
|
|
resp.DASTScanSeverityBreakdown = fuzzStats.SeverityCounts
|
|
resp.DASTScanStatusStatistics = fuzzStats.StatusCodes
|
|
resp.DASTScanStatistics.TotalMatchedResults = fuzzStats.TotalMatchedResults
|
|
resp.DASTScanStatistics.TotalComponentsTested = fuzzStats.TotalComponentsTested
|
|
resp.DASTScanStatistics.TotalEndpointsTested = fuzzStats.TotalEndpointsTested
|
|
resp.DASTScanStatistics.TotalFuzzedRequests = fuzzStats.TotalFuzzedRequests
|
|
resp.DASTScanStatistics.TotalTemplatesTested = fuzzStats.TotalTemplatesTested
|
|
resp.DASTScanStatistics.TotalErroredRequests = fuzzStats.TotalErroredRequests
|
|
resp.DASTScanErrorStatistics = fuzzStats.ErrorGroupedStats
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
//go:embed templates/index.html
|
|
var indexTemplate string
|
|
|
|
func (s *DASTServer) handleStats(c echo.Context) error {
|
|
stats, err := s.getStats()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
tmpl, err := template.New("index").Parse(indexTemplate)
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
return tmpl.Execute(c.Response().Writer, stats)
|
|
}
|
|
|
|
func (s *DASTServer) handleStatsJSON(c echo.Context) error {
|
|
resp, err := s.getStats()
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
return c.JSONPretty(200, resp, " ")
|
|
}
|