From 23825c77b2869dd3dce9f15f4d4b4bde3ec2c63a Mon Sep 17 00:00:00 2001
From: Piotr Idzik <65706193+vil02@users.noreply.github.com>
Date: Fri, 4 Oct 2024 23:07:07 +0200
Subject: [PATCH 01/20] style: do not use backticks (#5687)
---
gh_retry.sh | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/gh_retry.sh b/gh_retry.sh
index bf212e1290..43542276b3 100755
--- a/gh_retry.sh
+++ b/gh_retry.sh
@@ -21,9 +21,9 @@ WORKFLOW="Build Test"
GREP_ERROR_PATTERN='Test "http/interactsh.yaml" failed'
#Set fonts for Help.
-NORM=`tput sgr0`
-BOLD=`tput bold`
-REV=`tput smso`
+NORM=$(tput sgr0)
+BOLD=$(tput bold)
+REV=$(tput smso)
HELP()
{
@@ -73,7 +73,7 @@ function print_bold() {
function retry_failed_jobs() {
print_bold "Checking failed workflows for branch $BRANCH before $BEFORE"
- date=`date +%Y-%m-%d'T'%H:%M'Z' -d "$BEFORE"`
+ date=$(date +%Y-%m-%d'T'%H:%M'Z' -d "$BEFORE")
workflowIds=$(gh run list --limit "$LIMIT" --json headBranch,status,name,conclusion,databaseId,updatedAt | jq -c '.[] |
select ( .headBranch==$branch ) |
From 8b9acb292780cb8647db27b517cf5d2c0fa782f2 Mon Sep 17 00:00:00 2001
From: Ramana Reddy <90540245+RamanaReddy0M@users.noreply.github.com>
Date: Mon, 7 Oct 2024 18:11:03 +0530
Subject: [PATCH 02/20] return bool resp on successful ldap authentication
(#5682)
---
pkg/js/libs/ldap/ldap.go | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go
index 3961fa7b7e..e10015acfa 100644
--- a/pkg/js/libs/ldap/ldap.go
+++ b/pkg/js/libs/ldap/ldap.go
@@ -155,7 +155,7 @@ func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object {
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
// client.Authenticate('user', 'password');
// ```
-func (c *Client) Authenticate(username, password string) {
+func (c *Client) Authenticate(username, password string) bool {
c.nj.Require(c.conn != nil, "no existing connection")
if c.BaseDN == "" {
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
@@ -163,19 +163,21 @@ func (c *Client) Authenticate(username, password string) {
if err := c.conn.NTLMBind(c.Realm, username, password); err == nil {
// if bind with NTLMBind(), there is nothing
// else to do, you are authenticated
- return
+ return true
}
+ var err error
switch password {
case "":
- if err := c.conn.UnauthenticatedBind(username); err != nil {
+ if err = c.conn.UnauthenticatedBind(username); err != nil {
c.nj.ThrowError(err)
}
default:
- if err := c.conn.Bind(username, password); err != nil {
+ if err = c.conn.Bind(username, password); err != nil {
c.nj.ThrowError(err)
}
}
+ return err == nil
}
// AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash
@@ -185,14 +187,16 @@ func (c *Client) Authenticate(username, password string) {
// const client = new ldap.Client('ldap://ldap.example.com', 'acme.com');
// client.AuthenticateWithNTLMHash('pdtm', 'hash');
// ```
-func (c *Client) AuthenticateWithNTLMHash(username, hash string) {
+func (c *Client) AuthenticateWithNTLMHash(username, hash string) bool {
c.nj.Require(c.conn != nil, "no existing connection")
if c.BaseDN == "" {
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
}
- if err := c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil {
+ var err error
+ if err = c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil {
c.nj.ThrowError(err)
}
+ return err == nil
}
// Search accepts whatever filter and returns a list of maps having provided attributes
From 7ba5d51b0002410d30f163c7fdd866bf23e7dc5c Mon Sep 17 00:00:00 2001
From: Ramana Reddy <90540245+RamanaReddy0M@users.noreply.github.com>
Date: Mon, 7 Oct 2024 18:12:07 +0530
Subject: [PATCH 03/20] fix: ldap metadata collection err (#5683)
---
pkg/js/libs/ldap/ldap.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go
index e10015acfa..a8f0eb0c8e 100644
--- a/pkg/js/libs/ldap/ldap.go
+++ b/pkg/js/libs/ldap/ldap.go
@@ -283,9 +283,10 @@ func (c *Client) CollectMetadata() Metadata {
}
metadata.BaseDN = c.BaseDN
+ // Use scope as Base since Root DSE doesn't have subentries
srMetadata := ldap.NewSearchRequest(
"",
- ldap.ScopeWholeSubtree,
+ ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=*)",
From f0624820d314d70b86742765e6a335ebb54c6dda Mon Sep 17 00:00:00 2001
From: Dogan Can Bakir <65292895+dogancanbakir@users.noreply.github.com>
Date: Thu, 10 Oct 2024 00:34:23 +0300
Subject: [PATCH 04/20] update ssl part definitions (#5710)
---
pkg/protocols/ssl/ssl.go | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/pkg/protocols/ssl/ssl.go b/pkg/protocols/ssl/ssl.go
index 681043d3bf..554a4f5c10 100644
--- a/pkg/protocols/ssl/ssl.go
+++ b/pkg/protocols/ssl/ssl.go
@@ -347,11 +347,33 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
// description. Multiple definitions are separated by commas.
// Definitions not having a name (generated on runtime) are prefixed & suffixed by <>.
var RequestPartDefinitions = map[string]string{
- "type": "Type is the type of request made",
- "response": "JSON SSL protocol handshake details",
- "not_after": "Timestamp after which the remote cert expires",
- "host": "Host is the input to the template",
- "matched": "Matched is the input which was matched upon",
+ "template-id": "ID of the template executed",
+ "template-info": "Info Block of the template executed",
+ "template-path": "Path of the template executed",
+ "host": "Host is the input to the template",
+ "port": "Port is the port of the host",
+ "matched": "Matched is the input which was matched upon",
+ "type": "Type is the type of request made",
+ "timestamp": "Timestamp is the time when the request was made",
+ "response": "JSON SSL protocol handshake details",
+ "cipher": "Cipher is the encryption algorithm used",
+ "domains": "Domains are the list of domain names in the certificate",
+ "fingerprint_hash": "Fingerprint hash is the unique identifier of the certificate",
+ "ip": "IP is the IP address of the server",
+ "issuer_cn": "Issuer CN is the common name of the certificate issuer",
+ "issuer_dn": "Issuer DN is the distinguished name of the certificate issuer",
+ "issuer_org": "Issuer organization is the organization of the certificate issuer",
+ "not_after": "Timestamp after which the remote cert expires",
+ "not_before": "Timestamp before which the certificate is not valid",
+ "probe_status": "Probe status indicates if the probe was successful",
+ "serial": "Serial is the serial number of the certificate",
+ "sni": "SNI is the server name indication used in the handshake",
+ "subject_an": "Subject AN is the list of subject alternative names",
+ "subject_cn": "Subject CN is the common name of the certificate subject",
+ "subject_dn": "Subject DN is the distinguished name of the certificate subject",
+ "subject_org": "Subject organization is the organization of the certificate subject",
+ "tls_connection": "TLS connection is the type of TLS connection used",
+ "tls_version": "TLS version is the version of the TLS protocol used",
}
// Match performs matching operation for a matcher on model and returns:
From 690089e1ce723cc84ff89a0c1cd1ea7ba64f6d15 Mon Sep 17 00:00:00 2001
From: ghost
Date: Wed, 9 Oct 2024 21:36:38 +0000
Subject: [PATCH 05/20] Auto Generate Syntax Docs + JSONSchema [Wed Oct 9
21:36:38 UTC 2024] :robot:
---
SYNTAX-REFERENCE.md | 26 ++++++++-
pkg/templates/templates_doc.go | 96 ++++++++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 6 deletions(-)
diff --git a/SYNTAX-REFERENCE.md b/SYNTAX-REFERENCE.md
index 9ec413d3ed..394f0a0684 100755
--- a/SYNTAX-REFERENCE.md
+++ b/SYNTAX-REFERENCE.md
@@ -3764,11 +3764,33 @@ Appears in:
Part Definitions:
+- template-id
- ID of the template executed
+- template-info
- Info Block of the template executed
+- template-path
- Path of the template executed
+- host
- Host is the input to the template
+- port
- Port is the port of the host
+- matched
- Matched is the input which was matched upon
- type
- Type is the type of request made
+- timestamp
- Timestamp is the time when the request was made
- response
- JSON SSL protocol handshake details
+- cipher
- Cipher is the encryption algorithm used
+- domains
- Domains are the list of domain names in the certificate
+- fingerprint_hash
- Fingerprint hash is the unique identifier of the certificate
+- ip
- IP is the IP address of the server
+- issuer_cn
- Issuer CN is the common name of the certificate issuer
+- issuer_dn
- Issuer DN is the distinguished name of the certificate issuer
+- issuer_org
- Issuer organization is the organization of the certificate issuer
- not_after
- Timestamp after which the remote cert expires
-- host
- Host is the input to the template
-- matched
- Matched is the input which was matched upon
+- not_before
- Timestamp before which the certificate is not valid
+- probe_status
- Probe status indicates if the probe was successful
+- serial
- Serial is the serial number of the certificate
+- sni
- SNI is the server name indication used in the handshake
+- subject_an
- Subject AN is the list of subject alternative names
+- subject_cn
- Subject CN is the common name of the certificate subject
+- subject_dn
- Subject DN is the distinguished name of the certificate subject
+- subject_org
- Subject organization is the organization of the certificate subject
+- tls_connection
- TLS connection is the type of TLS connection used
+- tls_version
- TLS version is the version of the TLS protocol used
diff --git a/pkg/templates/templates_doc.go b/pkg/templates/templates_doc.go
index 827e583d3b..b1422c8333 100644
--- a/pkg/templates/templates_doc.go
+++ b/pkg/templates/templates_doc.go
@@ -1656,25 +1656,113 @@ func init() {
},
}
SSLRequestDoc.PartDefinitions = []encoder.KeyValue{
+ {
+ Key: "template-id",
+ Value: "ID of the template executed",
+ },
+ {
+ Key: "template-info",
+ Value: "Info Block of the template executed",
+ },
+ {
+ Key: "template-path",
+ Value: "Path of the template executed",
+ },
+ {
+ Key: "host",
+ Value: "Host is the input to the template",
+ },
+ {
+ Key: "port",
+ Value: "Port is the port of the host",
+ },
+ {
+ Key: "matched",
+ Value: "Matched is the input which was matched upon",
+ },
{
Key: "type",
Value: "Type is the type of request made",
},
+ {
+ Key: "timestamp",
+ Value: "Timestamp is the time when the request was made",
+ },
{
Key: "response",
Value: "JSON SSL protocol handshake details",
},
+ {
+ Key: "cipher",
+ Value: "Cipher is the encryption algorithm used",
+ },
+ {
+ Key: "domains",
+ Value: "Domains are the list of domain names in the certificate",
+ },
+ {
+ Key: "fingerprint_hash",
+ Value: "Fingerprint hash is the unique identifier of the certificate",
+ },
+ {
+ Key: "ip",
+ Value: "IP is the IP address of the server",
+ },
+ {
+ Key: "issuer_cn",
+ Value: "Issuer CN is the common name of the certificate issuer",
+ },
+ {
+ Key: "issuer_dn",
+ Value: "Issuer DN is the distinguished name of the certificate issuer",
+ },
+ {
+ Key: "issuer_org",
+ Value: "Issuer organization is the organization of the certificate issuer",
+ },
{
Key: "not_after",
Value: "Timestamp after which the remote cert expires",
},
{
- Key: "host",
- Value: "Host is the input to the template",
+ Key: "not_before",
+ Value: "Timestamp before which the certificate is not valid",
},
{
- Key: "matched",
- Value: "Matched is the input which was matched upon",
+ Key: "probe_status",
+ Value: "Probe status indicates if the probe was successful",
+ },
+ {
+ Key: "serial",
+ Value: "Serial is the serial number of the certificate",
+ },
+ {
+ Key: "sni",
+ Value: "SNI is the server name indication used in the handshake",
+ },
+ {
+ Key: "subject_an",
+ Value: "Subject AN is the list of subject alternative names",
+ },
+ {
+ Key: "subject_cn",
+ Value: "Subject CN is the common name of the certificate subject",
+ },
+ {
+ Key: "subject_dn",
+ Value: "Subject DN is the distinguished name of the certificate subject",
+ },
+ {
+ Key: "subject_org",
+ Value: "Subject organization is the organization of the certificate subject",
+ },
+ {
+ Key: "tls_connection",
+ Value: "TLS connection is the type of TLS connection used",
+ },
+ {
+ Key: "tls_version",
+ Value: "TLS version is the version of the TLS protocol used",
},
}
SSLRequestDoc.Fields = make([]encoder.Doc, 9)
From 82680980a5a2504eb0fa668de82338b5c13fba09 Mon Sep 17 00:00:00 2001
From: Ice3man
Date: Thu, 10 Oct 2024 20:22:22 +0530
Subject: [PATCH 06/20] bugfix: fix multipart panic + support for filename +
content-type (#5702)
* bugfix: fix multipart files panic + support for filename + content-type propagation
* misc changes
---
pkg/fuzz/dataformat/multipart.go | 44 +++++++++++++++++++++++++++++++-
1 file changed, 43 insertions(+), 1 deletion(-)
diff --git a/pkg/fuzz/dataformat/multipart.go b/pkg/fuzz/dataformat/multipart.go
index d7e40af10c..227025d22b 100644
--- a/pkg/fuzz/dataformat/multipart.go
+++ b/pkg/fuzz/dataformat/multipart.go
@@ -6,12 +6,19 @@ import (
"io"
"mime"
"mime/multipart"
+ "net/textproto"
mapsutil "github.com/projectdiscovery/utils/maps"
)
type MultiPartForm struct {
- boundary string
+ boundary string
+ filesMetadata map[string]FileMetadata
+}
+
+type FileMetadata struct {
+ ContentType string
+ Filename string
}
var (
@@ -41,11 +48,40 @@ func (m *MultiPartForm) Encode(data KV) (string, error) {
data.Iterate(func(key string, value any) bool {
var fw io.Writer
var err error
+
+ if filesArray, ok := value.([]interface{}); ok {
+ fileMetadata, ok := m.filesMetadata[key]
+ if !ok {
+ Itererr = fmt.Errorf("file metadata not found for key %s", key)
+ return false
+ }
+
+ for _, file := range filesArray {
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Disposition",
+ fmt.Sprintf(`form-data; name=%q; filename=%q`,
+ key, fileMetadata.Filename))
+ h.Set("Content-Type", fileMetadata.ContentType)
+
+ if fw, err = w.CreatePart(h); err != nil {
+ Itererr = err
+ return false
+ }
+
+ if _, err = fw.Write([]byte(file.(string))); err != nil {
+ Itererr = err
+ return false
+ }
+ }
+ return true
+ }
+
// Add field
if fw, err = w.CreateFormField(key); err != nil {
Itererr = err
return false
}
+
if _, err = fw.Write([]byte(value.(string))); err != nil {
Itererr = err
return false
@@ -98,6 +134,7 @@ func (m *MultiPartForm) Decode(data string) (KV, error) {
result.Set(key, values[0])
}
}
+ m.filesMetadata = make(map[string]FileMetadata)
for key, files := range form.File {
fileContents := []interface{}{}
for _, fileHeader := range files {
@@ -112,6 +149,11 @@ func (m *MultiPartForm) Decode(data string) (KV, error) {
return KV{}, err
}
fileContents = append(fileContents, buffer.String())
+
+ m.filesMetadata[key] = FileMetadata{
+ ContentType: fileHeader.Header.Get("Content-Type"),
+ Filename: fileHeader.Filename,
+ }
}
result.Set(key, fileContents)
}
From 1cd42c46c7b4b33e63774983bfea59a543c2b826 Mon Sep 17 00:00:00 2001
From: Dwi Siswanto
Date: Fri, 11 Oct 2024 20:43:02 +0700
Subject: [PATCH 07/20] chore: update `auto_assign` (#5720)
add me to `addReviewers` list
Signed-off-by: Dwi Siswanto
---
.github/auto_assign.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml
index 0c65e536ca..3d6642fc59 100644
--- a/.github/auto_assign.yml
+++ b/.github/auto_assign.yml
@@ -1,6 +1,7 @@
addReviewers: true
reviewers:
- dogancanbakir
+ - dwisiswant0
numberOfReviewers: 1
skipKeywords:
From 3f0de96726b0dac0304132733e17a1e2d56e8a9f Mon Sep 17 00:00:00 2001
From: Keith Chason
Date: Sun, 13 Oct 2024 12:14:33 -0400
Subject: [PATCH 08/20] MongoDB Reporting (#5688)
* Initial setup of Mongo reporting
* Fix slice pop logic
* Switch to config-file logic
* Parse database name from connection string
* Switch to url.Parse for connection string parsing
* Address return/logging feedback
---
cmd/nuclei/issue-tracker-config.yaml | 11 ++
go.mod | 6 +
go.sum | 12 ++
pkg/reporting/exporters/mongo/mongo.go | 155 +++++++++++++++++++++++++
pkg/reporting/options.go | 3 +
pkg/reporting/reporting.go | 9 ++
6 files changed, 196 insertions(+)
create mode 100644 pkg/reporting/exporters/mongo/mongo.go
diff --git a/cmd/nuclei/issue-tracker-config.yaml b/cmd/nuclei/issue-tracker-config.yaml
index b7e0e6dafc..07ccf828f2 100644
--- a/cmd/nuclei/issue-tracker-config.yaml
+++ b/cmd/nuclei/issue-tracker-config.yaml
@@ -162,3 +162,14 @@
# duplicate-issue-check: false
# # open-state-id is the ID of the open state in Linear
# open-state-id: ""
+#mongodb:
+# # the connection string to the MongoDB database
+# # (e.g., mongodb://root:example@localhost:27017/nuclei?ssl=false&authSource=admin)
+# connection-string: ""
+# # the name of the collection to store the issues
+# collection-name: ""
+# # excludes the Request and Response from the results (helps with filesize)
+# omit-raw: false
+# # determines the number of results to be kept in memory before writing it to the database or 0 to
+# # persist all in memory and write all results at the end (default)
+# batch-size: 0
\ No newline at end of file
diff --git a/go.mod b/go.mod
index f2adacca21..86a8a00643 100644
--- a/go.mod
+++ b/go.mod
@@ -105,6 +105,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
github.com/zmap/zgrab2 v0.1.8-0.20230806160807-97ba87c0e706
+ go.mongodb.org/mongo-driver v1.17.0
golang.org/x/term v0.24.0
gopkg.in/yaml.v3 v3.0.1
moul.io/http2curl v1.0.0
@@ -195,6 +196,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -228,9 +230,13 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/got v0.34.1 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
diff --git a/go.sum b/go.sum
index da71e9b287..b69d6283a5 100644
--- a/go.sum
+++ b/go.sum
@@ -755,6 +755,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -1085,6 +1087,12 @@ github.com/xanzy/go-gitlab v0.107.0 h1:P2CT9Uy9yN9lJo3FLxpMZ4xj6uWcpnigXsjvqJ6nd
github.com/xanzy/go-gitlab v0.107.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -1098,6 +1106,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
@@ -1150,6 +1160,8 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r40k=
+go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
diff --git a/pkg/reporting/exporters/mongo/mongo.go b/pkg/reporting/exporters/mongo/mongo.go
new file mode 100644
index 0000000000..faf8bb579b
--- /dev/null
+++ b/pkg/reporting/exporters/mongo/mongo.go
@@ -0,0 +1,155 @@
+package mongo
+
+import (
+ "context"
+ "github.com/pkg/errors"
+ "github.com/projectdiscovery/gologger"
+ "github.com/projectdiscovery/nuclei/v3/pkg/output"
+ "go.mongodb.org/mongo-driver/mongo"
+ "net/url"
+ "os"
+ "strings"
+ "sync"
+
+ mongooptions "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type Exporter struct {
+ options *Options
+ mutex *sync.Mutex
+ rows []output.ResultEvent
+ collection *mongo.Collection
+ connection *mongo.Client
+}
+
+// Options contains the configuration options for MongoDB exporter client
+type Options struct {
+ // ConnectionString is the connection string to the MongoDB database
+ ConnectionString string `yaml:"connection-string"`
+ // CollectionName is the name of the MongoDB collection in which to store the results
+ CollectionName string `yaml:"collection-name"`
+ // OmitRaw excludes the Request and Response from the results (helps with filesize)
+ OmitRaw bool `yaml:"omit-raw"`
+ // BatchSize determines the number of results to be kept in memory before writing it to the database or 0 to
+ // persist all in memory and write all results at the end (default)
+ BatchSize int `yaml:"batch-size"`
+}
+
+// New creates a new MongoDB exporter integration client based on options.
+func New(options *Options) (*Exporter, error) {
+ exporter := &Exporter{
+ mutex: &sync.Mutex{},
+ options: options,
+ rows: []output.ResultEvent{},
+ }
+
+ // If the environment variable for the connection string is set, then use that instead. This allows for easier
+ // management of sensitive items such as credentials
+ envConnectionString := os.Getenv("MONGO_CONNECTION_STRING")
+ if envConnectionString != "" {
+ options.ConnectionString = envConnectionString
+ gologger.Info().Msgf("Using connection string from environment variable MONGO_CONNECTION_STRING")
+ }
+
+ // Create the connection to the database
+ clientOptions := mongooptions.Client().ApplyURI(options.ConnectionString)
+
+ // Create a new client and connect to the MongoDB server
+ client, err := mongo.Connect(context.TODO(), clientOptions)
+ if err != nil {
+ gologger.Error().Msgf("Error creating MongoDB client: %s", err)
+ return nil, err
+ }
+
+ // Ensure the connection is valid
+ err = client.Ping(context.Background(), nil)
+ if err != nil {
+ gologger.Error().Msgf("Error connecting to MongoDB: %s", err)
+ return nil, err
+ }
+
+ // Get the database from the connection string to set the database and collection
+ parsed, err := url.Parse(options.ConnectionString)
+ if err != nil {
+ gologger.Error().Msgf("Error parsing connection string: %s", options.ConnectionString)
+ return nil, err
+ }
+
+ databaseName := strings.TrimPrefix(parsed.Path, "/")
+
+ if databaseName == "" {
+ return nil, errors.New("error getting database name from connection string")
+ }
+
+ exporter.connection = client
+ exporter.collection = client.Database(databaseName).Collection(options.CollectionName)
+
+ return exporter, nil
+}
+
+// Export writes a result document to the configured MongoDB collection
+// in the database configured by the connection string
+func (exporter *Exporter) Export(event *output.ResultEvent) error {
+ exporter.mutex.Lock()
+ defer exporter.mutex.Unlock()
+
+ if exporter.options.OmitRaw {
+ event.Request = ""
+ event.Response = ""
+ }
+
+ // Add the row to the queue to be processed
+ exporter.rows = append(exporter.rows, *event)
+
+ // If the batch size is greater than 0 and the number of rows has reached the batch, flush it to the database
+ if exporter.options.BatchSize > 0 && len(exporter.rows) >= exporter.options.BatchSize {
+ err := exporter.WriteRows()
+ if err != nil {
+ // The error is already logged, return it to bubble up to the caller
+ return err
+ }
+ }
+
+ return nil
+}
+
+// WriteRows writes all rows from the rows list to the MongoDB collection and removes them from the list
+func (exporter *Exporter) WriteRows() error {
+ // Loop through the rows and write them, removing them as they're entered
+ for len(exporter.rows) > 0 {
+ data := exporter.rows[0]
+
+ // Write the data to the database
+ _, err := exporter.collection.InsertOne(context.TODO(), data)
+ if err != nil {
+ gologger.Fatal().Msgf("Error inserting record into MongoDB collection: %s", err)
+ return err
+ }
+
+ // Remove the item from the list
+ exporter.rows = exporter.rows[1:]
+ }
+
+ return nil
+}
+
+func (exporter *Exporter) Close() error {
+ exporter.mutex.Lock()
+ defer exporter.mutex.Unlock()
+
+ // Write all pending rows
+ err := exporter.WriteRows()
+ if err != nil {
+ // The error is already logged, return it to bubble up to the caller
+ return err
+ }
+
+ // Close the database connection
+ err = exporter.connection.Disconnect(context.TODO())
+ if err != nil {
+ gologger.Error().Msgf("Error disconnecting from MongoDB: %s", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/pkg/reporting/options.go b/pkg/reporting/options.go
index c5090de014..bda9b6c28d 100644
--- a/pkg/reporting/options.go
+++ b/pkg/reporting/options.go
@@ -5,6 +5,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
+ "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/sarif"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/splunk"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
@@ -44,6 +45,8 @@ type Options struct {
JSONExporter *jsonexporter.Options `yaml:"json"`
// JSONLExporter contains configuration options for JSONL Exporter Module
JSONLExporter *jsonl.Options `yaml:"jsonl"`
+ // MongoDBExporter containers the configuration options for the MongoDB Exporter Module
+ MongoDBExporter *mongo.Options `yaml:"mongodb"`
HttpClient *retryablehttp.Client `yaml:"-"`
OmitRaw bool `yaml:"-"`
diff --git a/pkg/reporting/reporting.go b/pkg/reporting/reporting.go
index c6a7d63e10..ddc5428636 100644
--- a/pkg/reporting/reporting.go
+++ b/pkg/reporting/reporting.go
@@ -2,6 +2,7 @@ package reporting
import (
"fmt"
+ "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
"os"
"strings"
"sync/atomic"
@@ -166,6 +167,13 @@ func New(options *Options, db string, doNotDedupe bool) (Client, error) {
}
client.exporters = append(client.exporters, exporter)
}
+ if options.MongoDBExporter != nil {
+ exporter, err := mongo.New(options.MongoDBExporter)
+ if err != nil {
+ return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
+ }
+ client.exporters = append(client.exporters, exporter)
+ }
if doNotDedupe {
return client, nil
@@ -212,6 +220,7 @@ func CreateConfigIfNotExists() error {
SplunkExporter: &splunk.Options{},
JSONExporter: &json_exporter.Options{},
JSONLExporter: &jsonl.Options{},
+ MongoDBExporter: &mongo.Options{},
}
reportingFile, err := os.Create(reportingConfig)
if err != nil {
From 888a732fbc9b16c7776f78f147d9277641fc61ca Mon Sep 17 00:00:00 2001
From: Danny Shemesh
Date: Mon, 14 Oct 2024 11:48:59 +0300
Subject: [PATCH 09/20] Unlock memguard global change mutex only when locked
(#5714)
---
pkg/protocols/common/protocolstate/memguardian.go | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/pkg/protocols/common/protocolstate/memguardian.go b/pkg/protocols/common/protocolstate/memguardian.go
index 1db1e0a775..b5c8cffc75 100644
--- a/pkg/protocols/common/protocolstate/memguardian.go
+++ b/pkg/protocols/common/protocolstate/memguardian.go
@@ -77,9 +77,8 @@ var muGlobalChange sync.Mutex
// Global setting
func GlobalGuardBytesBufferAlloc() error {
- if muGlobalChange.TryLock() {
+ if !muGlobalChange.TryLock() {
return nil
-
}
defer muGlobalChange.Unlock()
@@ -95,9 +94,8 @@ func GlobalGuardBytesBufferAlloc() error {
// Global setting
func GlobalRestoreBytesBufferAlloc() {
- if muGlobalChange.TryLock() {
+ if !muGlobalChange.TryLock() {
return
-
}
defer muGlobalChange.Unlock()
From d68af67e6ea08c9df13f44af8e9fbd9dc378faf2 Mon Sep 17 00:00:00 2001
From: Dwi Siswanto
Date: Mon, 14 Oct 2024 16:23:36 +0700
Subject: [PATCH 10/20] feat(nuclei): generate trace file when using
`profile-mem` (#5690)
* feat(nuclei): generate trace file when using `profile-mem`
Signed-off-by: Dwi Siswanto
* docs(DESIGN): dynamically grep mod path
Signed-off-by: Dwi Siswanto
---------
Signed-off-by: Dwi Siswanto
---
.gitignore | 7 ++++++-
DESIGN.md | 43 +++++++++++++++++++++++++++++++++----------
cmd/nuclei/main.go | 40 ++++++++++++++++++++++++++++++----------
3 files changed, 69 insertions(+), 21 deletions(-)
diff --git a/.gitignore b/.gitignore
index 146b0a892c..6145103342 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,9 @@ pkg/protocols/common/helpers/deserialization/testdata/ValueObject2.ser
vendor
# Headless `screenshot` action
-*.png
\ No newline at end of file
+*.png
+
+# Profiling & tracing
+*.prof
+*.pprof
+*.trace
\ No newline at end of file
diff --git a/DESIGN.md b/DESIGN.md
index 9d92e28f8d..af0e27baf9 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -457,26 +457,49 @@ func (template *Template) compileProtocolRequests(options protocols.ExecuterOpti
That's it, you've added a new protocol to Nuclei. The next good step would be to write integration tests which are described in `integration-tests` and `cmd/integration-tests` directories.
-## Profiling Instructions
+## Profiling and Tracing
-To enable dumping of Memory profiling data, `-profile-mem` flag can be used along with path to a file. This writes a pprof formatted file which can be used for investigate resource usage with `pprof` tool.
+To analyze Nuclei's performance and resource usage, you can generate memory profiles and trace files using the `-profile-mem` flag:
-```console
-$ nuclei -t nuclei-templates/ -u https://example.com -profile-mem mem.pprof
+```bash
+nuclei -t nuclei-templates/ -u https://example.com -profile-mem=nuclei-$(git describe --tags)
```
-To view profile data in pprof, first install pprof. Then run the below command -
+This command creates two files:
-```console
-$ go tool pprof mem.pprof
+* `nuclei.prof`: Memory (heap) profile
+* `nuclei.trace`: Execution trace
+
+### Analyzing the Memory Profile
+
+1. View the profile in the terminal:
+
+```bash
+go tool pprof nuclei.prof
+```
+
+2. Display top memory consumers:
+
+```bash
+go tool pprof -top nuclei.prof | grep "$(go list -m)" | head -10
```
-To open a web UI on a port to visualize debug data, the below command can be used.
+3. Visualize the profile in a web browser:
-```console
-$ go tool pprof -http=:8081 mem.pprof
+```bash
+go tool pprof -http=:$(shuf -i 1000-99999 -n 1) nuclei.prof
```
+### Analyzing the Trace File
+
+To examine the execution trace:
+
+```bash
+go tool trace nuclei.trace
+```
+
+These tools help identify performance bottlenecks and memory leaks, allowing for targeted optimizations of Nuclei's codebase.
+
## Project Structure
- [pkg/reporting](./pkg/reporting) - Reporting modules for nuclei.
diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go
index 8fe5b29573..caac7e5546 100644
--- a/cmd/nuclei/main.go
+++ b/cmd/nuclei/main.go
@@ -9,6 +9,7 @@ import (
"path/filepath"
"runtime"
"runtime/pprof"
+ "runtime/trace"
"strings"
"time"
@@ -103,21 +104,40 @@ func main() {
return
}
- // Profiling related code
+ // Profiling & tracing related code
if memProfile != "" {
- f, err := os.Create(memProfile)
+ memProfile = strings.TrimSuffix(memProfile, filepath.Ext(memProfile)) + ".prof"
+ memProfileFile, err := os.Create(memProfile)
if err != nil {
- gologger.Fatal().Msgf("profile: could not create memory profile %q: %v", memProfile, err)
+ gologger.Fatal().Msgf("profile: could not create memory profile %q file: %v", memProfile, err)
}
- old := runtime.MemProfileRate
+
+ traceFilepath := strings.TrimSuffix(memProfile, filepath.Ext(memProfile)) + ".trace"
+ traceFile, err := os.Create(traceFilepath)
+ if err != nil {
+ gologger.Fatal().Msgf("profile: could not create trace %q file: %v", traceFilepath, err)
+ }
+
+ oldMemProfileRate := runtime.MemProfileRate
runtime.MemProfileRate = 4096
- gologger.Print().Msgf("profile: memory profiling enabled (rate %d), %s", runtime.MemProfileRate, memProfile)
+
+ // Start tracing
+ if err := trace.Start(traceFile); err != nil {
+ gologger.Fatal().Msgf("profile: could not start trace: %v", err)
+ }
defer func() {
- _ = pprof.Lookup("heap").WriteTo(f, 0)
- f.Close()
- runtime.MemProfileRate = old
- gologger.Print().Msgf("profile: memory profiling disabled, %s", memProfile)
+ // Start CPU profiling
+ if err := pprof.WriteHeapProfile(memProfileFile); err != nil {
+ gologger.Fatal().Msgf("profile: could not start CPU profile: %v", err)
+ }
+ memProfileFile.Close()
+ traceFile.Close()
+ trace.Stop()
+ runtime.MemProfileRate = oldMemProfileRate
+
+ gologger.Info().Msgf("Memory profile saved at %q", memProfile)
+ gologger.Info().Msgf("Traced at %q", traceFilepath)
}()
}
@@ -402,7 +422,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.CallbackVar(printVersion, "version", "show nuclei version"),
flagSet.BoolVarP(&options.HangMonitor, "hang-monitor", "hm", false, "enable nuclei hang monitoring"),
flagSet.BoolVarP(&options.Verbose, "verbose", "v", false, "show verbose output"),
- flagSet.StringVar(&memProfile, "profile-mem", "", "optional nuclei memory profile dump file"),
+ flagSet.StringVar(&memProfile, "profile-mem", "", "generate memory (heap) profile & trace files"),
flagSet.BoolVar(&options.VerboseVerbose, "vv", false, "display templates loaded for scan"),
flagSet.BoolVarP(&options.ShowVarDump, "show-var-dump", "svd", false, "show variables dump for debugging"),
flagSet.BoolVarP(&options.EnablePprof, "enable-pprof", "ep", false, "enable pprof debugging server"),
From 4ef058758ce29807669b5b350b8dc10d43e7f9c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Do=C4=9Fan=20Can=20Bak=C4=B1r?=
Date: Mon, 14 Oct 2024 15:53:53 +0300
Subject: [PATCH 11/20] fix template loading logic
---
lib/sdk.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/sdk.go b/lib/sdk.go
index daeb68c14b..9bd46f4766 100644
--- a/lib/sdk.go
+++ b/lib/sdk.go
@@ -100,6 +100,7 @@ func (e *NucleiEngine) LoadAllTemplates() error {
return errorutil.New("Could not create loader client: %s\n", err)
}
e.store.Load()
+ e.templatesLoaded = true
return nil
}
From aab2cadb64e32d213586fdea25c8b6192007482d Mon Sep 17 00:00:00 2001
From: chuu
Date: Mon, 14 Oct 2024 06:52:52 -0700
Subject: [PATCH 12/20] fix: input helper in executor options (#5712)
---
lib/sdk_private.go | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/sdk_private.go b/lib/sdk_private.go
index cacd9a1ca1..f63c7b1c34 100644
--- a/lib/sdk_private.go
+++ b/lib/sdk_private.go
@@ -3,6 +3,7 @@ package nuclei
import (
"context"
"fmt"
+ "github.com/projectdiscovery/nuclei/v3/pkg/input"
"strings"
"sync"
"time"
@@ -171,6 +172,7 @@ func (e *NucleiEngine) init(ctx context.Context) error {
ResumeCfg: types.NewResumeCfg(),
Browser: e.browserInstance,
Parser: e.parser,
+ InputHelper: input.NewHelper(),
}
if len(e.opts.SecretsFile) > 0 {
authTmplStore, err := runner.GetAuthTmplStore(*e.opts, e.catalog, e.executerOpts)
From cc5c5509dc8d87f8b0779b0dfa1e1b9f6f31b5fb Mon Sep 17 00:00:00 2001
From: Dwi Siswanto
Date: Mon, 14 Oct 2024 20:55:46 +0700
Subject: [PATCH 13/20] feat: global matchers (#5701)
* feat: global matchers
Signed-off-by: Dwi Siswanto
Co-authored-by: Ice3man543
* feat(globalmatchers): make `Callback` as type
Signed-off-by: Dwi Siswanto
* feat: update `passive` term to `(matchers-)static`
Signed-off-by: Dwi Siswanto
* feat(globalmatchers): add `origin-template-*` event
also use `Set` method instead of `maps.Clone`
Signed-off-by: Dwi Siswanto
* feat: update `matchers-static` term to `global-matchers`
Signed-off-by: Dwi Siswanto
* feat(globalmatchers): clone event before `operator.Execute`
Signed-off-by: Dwi Siswanto
* fix(tmplexec): don't store `matched` on `global-matchers` templ
This will end up generating 2 events from the same
`scan.ScanContext` if one of the templates has
`global-matchers` enabled. This way, non-
`global-matchers` templates can enter the
`writeFailureCallback` func to log failure output.
Signed-off-by: Dwi Siswanto
* feat(globalmatchers): initializes `requests` on `New`
Signed-off-by: Dwi Siswanto
* feat(globalmatchers): add `hasStorage` method
Signed-off-by: Dwi Siswanto
* refactor(templates): rename global matchers checks method
Signed-off-by: Dwi Siswanto
* fix(loader): handle nil `templates.Template` pointer
Signed-off-by: Dwi Siswanto
---------
Signed-off-by: Dwi Siswanto
Co-authored-by: Ice3man543
---
internal/runner/runner.go | 2 +
pkg/catalog/loader/loader.go | 14 ++++
pkg/output/format_screen.go | 5 ++
pkg/output/output.go | 3 +
.../common/globalmatchers/globalmatchers.go | 84 +++++++++++++++++++
pkg/protocols/http/http.go | 3 +
pkg/protocols/http/operators.go | 5 ++
pkg/protocols/http/request.go | 11 ++-
pkg/protocols/protocols.go | 3 +
pkg/templates/compile.go | 32 +++++++
pkg/tmplexec/exec.go | 10 ++-
11 files changed, 170 insertions(+), 2 deletions(-)
create mode 100644 pkg/protocols/common/globalmatchers/globalmatchers.go
diff --git a/internal/runner/runner.go b/internal/runner/runner.go
index 0b6da592d3..516bd7ca50 100644
--- a/internal/runner/runner.go
+++ b/internal/runner/runner.go
@@ -48,6 +48,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/automaticscan"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
+ "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
@@ -475,6 +476,7 @@ func (r *Runner) RunEnumeration() error {
TemporaryDirectory: r.tmpDir,
Parser: r.parser,
FuzzParamsFrequency: fuzzFreqCache,
+ GlobalMatchers: globalmatchers.New(),
}
if config.DefaultConfig.IsDebugArgEnabled(config.DebugExportURLPattern) {
diff --git a/pkg/catalog/loader/loader.go b/pkg/catalog/loader/loader.go
index 48f14a4054..94818020b7 100644
--- a/pkg/catalog/loader/loader.go
+++ b/pkg/catalog/loader/loader.go
@@ -353,6 +353,20 @@ func (store *Store) areWorkflowOrTemplatesValid(filteredTemplatePaths map[string
if isParsingError("Error occurred parsing template %s: %s\n", templatePath, err) {
areTemplatesValid = false
}
+ } else if template == nil {
+ // NOTE(dwisiswant0): possibly global matchers template.
+ // This could definitely be handled better, for example by returning an
+ // `ErrGlobalMatchersTemplate` during `templates.Parse` and checking it
+ // with `errors.Is`.
+ //
+ // However, I’m not sure if every reference to it should be handled
+ // that way. Returning a `templates.Template` pointer would mean it’s
+ // an active template (sending requests), and adding a specific field
+ // like `isGlobalMatchers` in `templates.Template` (then checking it
+ // with a `*templates.Template.IsGlobalMatchersEnabled` method) would
+ // just introduce more unknown issues - like during template
+ // clustering, AFAIK.
+ continue
} else {
if existingTemplatePath, found := templateIDPathMap[template.ID]; !found {
templateIDPathMap[template.ID] = templatePath
diff --git a/pkg/output/format_screen.go b/pkg/output/format_screen.go
index 9d0f03efaf..3cdcec1e15 100644
--- a/pkg/output/format_screen.go
+++ b/pkg/output/format_screen.go
@@ -39,6 +39,11 @@ func (w *StandardWriter) formatScreen(output *ResultEvent) []byte {
}
}
+ if output.GlobalMatchers {
+ builder.WriteString("] [")
+ builder.WriteString(w.aurora.BrightMagenta("global").String())
+ }
+
builder.WriteString("] [")
builder.WriteString(w.aurora.BrightBlue(output.Type).String())
builder.WriteString("] ")
diff --git a/pkg/output/output.go b/pkg/output/output.go
index fbc9f71306..9d84fd20fc 100644
--- a/pkg/output/output.go
+++ b/pkg/output/output.go
@@ -184,6 +184,9 @@ type ResultEvent struct {
MatcherStatus bool `json:"matcher-status"`
// Lines is the line count for the specified match
Lines []int `json:"matched-line,omitempty"`
+ // GlobalMatchers identifies whether the matches was detected in the response
+ // of another template's result event
+ GlobalMatchers bool `json:"global-matchers,omitempty"`
// IssueTrackers is the metadata for issue trackers
IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"`
diff --git a/pkg/protocols/common/globalmatchers/globalmatchers.go b/pkg/protocols/common/globalmatchers/globalmatchers.go
new file mode 100644
index 0000000000..321759f927
--- /dev/null
+++ b/pkg/protocols/common/globalmatchers/globalmatchers.go
@@ -0,0 +1,84 @@
+package globalmatchers
+
+import (
+ "maps"
+
+ "github.com/projectdiscovery/nuclei/v3/pkg/model"
+ "github.com/projectdiscovery/nuclei/v3/pkg/operators"
+ "github.com/projectdiscovery/nuclei/v3/pkg/output"
+)
+
+// Storage is a struct that holds the global matchers
+type Storage struct {
+ requests []*Item
+}
+
+// Callback is called when a global matcher is matched.
+// It receives internal event & result of the operator execution.
+type Callback func(event output.InternalEvent, result *operators.Result)
+
+// Item is a struct that holds the global matchers
+// details for a template
+type Item struct {
+ TemplateID string
+ TemplatePath string
+ TemplateInfo model.Info
+ Operators []*operators.Operators
+}
+
+// New creates a new storage for global matchers
+func New() *Storage {
+ return &Storage{requests: make([]*Item, 0)}
+}
+
+// hasStorage checks if the Storage is initialized
+func (s *Storage) hasStorage() bool {
+ return s != nil
+}
+
+// AddOperator adds a new operator to the global matchers
+func (s *Storage) AddOperator(item *Item) {
+ if !s.hasStorage() {
+ return
+ }
+
+ s.requests = append(s.requests, item)
+}
+
+// HasMatchers returns true if we have global matchers
+func (s *Storage) HasMatchers() bool {
+ if !s.hasStorage() {
+ return false
+ }
+
+ return len(s.requests) > 0
+}
+
+// Match matches the global matchers against the response
+func (s *Storage) Match(
+ event output.InternalEvent,
+ matchFunc operators.MatchFunc,
+ extractFunc operators.ExtractFunc,
+ isDebug bool,
+ callback Callback,
+) {
+ for _, item := range s.requests {
+ for _, operator := range item.Operators {
+ newEvent := maps.Clone(event)
+ newEvent.Set("origin-template-id", event["template-id"])
+ newEvent.Set("origin-template-info", event["template-info"])
+ newEvent.Set("origin-template-path", event["template-path"])
+ newEvent.Set("template-id", item.TemplateID)
+ newEvent.Set("template-info", item.TemplateInfo)
+ newEvent.Set("template-path", item.TemplatePath)
+ newEvent.Set("global-matchers", true)
+
+ result, matched := operator.Execute(newEvent, matchFunc, extractFunc, isDebug)
+ if !matched {
+ continue
+ }
+
+ callback(newEvent, result)
+ }
+ }
+}
diff --git a/pkg/protocols/http/http.go b/pkg/protocols/http/http.go
index 844bf8c579..bd089edd47 100644
--- a/pkg/protocols/http/http.go
+++ b/pkg/protocols/http/http.go
@@ -223,6 +223,9 @@ type Request struct {
// FuzzPreConditionOperator is the operator between multiple PreConditions for fuzzing Default is OR
FuzzPreConditionOperator string `yaml:"pre-condition-operator,omitempty" json:"pre-condition-operator,omitempty" jsonschema:"title=condition between the filters,description=Operator to use between multiple per-conditions,enum=and,enum=or"`
fuzzPreConditionOperator matchers.ConditionType `yaml:"-" json:"-"`
+ // description: |
+ // GlobalMatchers marks matchers as static and applies globally to all result events from other templates
+ GlobalMatchers bool `yaml:"global-matchers,omitempty" json:"global-matchers,omitempty" jsonschema:"title=global matchers,description=marks matchers as static and applies globally to all result events from other templates"`
}
func (e Request) JSONSchemaExtend(schema *jsonschema.Schema) {
diff --git a/pkg/protocols/http/operators.go b/pkg/protocols/http/operators.go
index d630bfd8b0..9e7d58af0a 100644
--- a/pkg/protocols/http/operators.go
+++ b/pkg/protocols/http/operators.go
@@ -166,6 +166,10 @@ func (request *Request) MakeResultEventItem(wrapped *output.InternalWrappedEvent
if types.ToString(wrapped.InternalEvent["path"]) != "" {
fields.Path = types.ToString(wrapped.InternalEvent["path"])
}
+ var isGlobalMatchers bool
+ if value, ok := wrapped.InternalEvent["global-matchers"]; ok {
+ isGlobalMatchers = value.(bool)
+ }
data := &output.ResultEvent{
TemplateID: types.ToString(wrapped.InternalEvent["template-id"]),
TemplatePath: types.ToString(wrapped.InternalEvent["template-path"]),
@@ -183,6 +187,7 @@ func (request *Request) MakeResultEventItem(wrapped *output.InternalWrappedEvent
Timestamp: time.Now(),
MatcherStatus: true,
IP: fields.Ip,
+ GlobalMatchers: isGlobalMatchers,
Request: types.ToString(wrapped.InternalEvent["request"]),
Response: request.truncateResponse(wrapped.InternalEvent["response"]),
CURLCommand: types.ToString(wrapped.InternalEvent["curl-command"]),
diff --git a/pkg/protocols/http/request.go b/pkg/protocols/http/request.go
index 994e065582..f74020eefb 100644
--- a/pkg/protocols/http/request.go
+++ b/pkg/protocols/http/request.go
@@ -973,13 +973,22 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ
// prune signature internal values if any
request.pruneSignatureInternalValues(generatedRequest.meta)
- event := eventcreator.CreateEventWithAdditionalOptions(request, generators.MergeMaps(generatedRequest.dynamicValues, finalEvent), request.options.Options.Debug || request.options.Options.DebugResponse, func(internalWrappedEvent *output.InternalWrappedEvent) {
+ interimEvent := generators.MergeMaps(generatedRequest.dynamicValues, finalEvent)
+ isDebug := request.options.Options.Debug || request.options.Options.DebugResponse
+ event := eventcreator.CreateEventWithAdditionalOptions(request, interimEvent, isDebug, func(internalWrappedEvent *output.InternalWrappedEvent) {
internalWrappedEvent.OperatorsResult.PayloadValues = generatedRequest.meta
})
+
if hasInteractMatchers {
event.UsesInteractsh = true
}
+ if request.options.GlobalMatchers.HasMatchers() {
+ request.options.GlobalMatchers.Match(interimEvent, request.Match, request.Extract, isDebug, func(event output.InternalEvent, result *operators.Result) {
+ callback(eventcreator.CreateEventWithOperatorResults(request, event, result))
+ })
+ }
+
// if requrlpattern is enabled, only then it is reflected in result event else it is empty string
// consult @Ice3man543 before changing this logic (context: vuln_hash)
if request.options.ExportReqURLPattern {
diff --git a/pkg/protocols/protocols.go b/pkg/protocols/protocols.go
index a9d50c1481..9ead70321d 100644
--- a/pkg/protocols/protocols.go
+++ b/pkg/protocols/protocols.go
@@ -25,6 +25,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/projectfile"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
+ "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
@@ -126,6 +127,8 @@ type ExecutorOptions struct {
// ExportReqURLPattern exports the request URL pattern
// in ResultEvent it contains the exact url pattern (ex: {{BaseURL}}/{{randstr}}/xyz) used in the request
ExportReqURLPattern bool
+ // GlobalMatchers is the storage for global matchers with http passive templates
+ GlobalMatchers *globalmatchers.Storage
}
// todo: centralizing components is not feasible with current clogged architecture
diff --git a/pkg/templates/compile.go b/pkg/templates/compile.go
index 01af3a999b..b4005afe81 100644
--- a/pkg/templates/compile.go
+++ b/pkg/templates/compile.go
@@ -20,6 +20,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
+ "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec"
@@ -81,6 +82,18 @@ func Parse(filePath string, preprocessor Preprocessor, options protocols.Executo
if err != nil {
return nil, err
}
+ if template.isGlobalMatchersEnabled() {
+ item := &globalmatchers.Item{
+ TemplateID: template.ID,
+ TemplatePath: filePath,
+ TemplateInfo: template.Info,
+ }
+ for _, request := range template.RequestsHTTP {
+ item.Operators = append(item.Operators, request.CompiledOperators)
+ }
+ options.GlobalMatchers.AddOperator(item)
+ return nil, nil
+ }
// Compile the workflow request
if len(template.Workflows) > 0 {
compiled := &template.Workflow
@@ -96,6 +109,25 @@ func Parse(filePath string, preprocessor Preprocessor, options protocols.Executo
return template, nil
}
+// isGlobalMatchersEnabled checks if any of requests in the template
+// have global matchers enabled. It iterates through all requests and
+// returns true if at least one request has global matchers enabled;
+// otherwise, it returns false.
+//
+// Note: This method only checks the `RequestsHTTP`
+// field of the template, which is specific to http-protocol-based
+// templates.
+//
+// TODO: support all protocols.
+func (template *Template) isGlobalMatchersEnabled() bool {
+ for _, request := range template.RequestsHTTP {
+ if request.GlobalMatchers {
+ return true
+ }
+ }
+ return false
+}
+
// parseSelfContainedRequests parses the self contained template requests.
func (template *Template) parseSelfContainedRequests() {
if template.Signature.Value.String() != "" {
diff --git a/pkg/tmplexec/exec.go b/pkg/tmplexec/exec.go
index 279d03d849..0caefe6024 100644
--- a/pkg/tmplexec/exec.go
+++ b/pkg/tmplexec/exec.go
@@ -174,7 +174,15 @@ func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
if !event.HasOperatorResult() && event.InternalEvent != nil {
lastMatcherEvent = event
} else {
- if writer.WriteResult(event, e.options.Output, e.options.Progress, e.options.IssuesClient) {
+ var isGlobalMatchers bool
+ isGlobalMatchers, _ = event.InternalEvent["global-matchers"].(bool)
+ // NOTE(dwisiswant0): Don't store `matched` on a `global-matchers` template.
+ // This will end up generating 2 events from the same `scan.ScanContext` if
+ // one of the templates has `global-matchers` enabled. This way,
+ // non-`global-matchers` templates can enter the `writeFailureCallback`
+ // func to log failure output.
+ wr := writer.WriteResult(event, e.options.Output, e.options.Progress, e.options.IssuesClient)
+ if wr && !isGlobalMatchers {
matched.Store(true)
} else {
lastMatcherEvent = event
From 53f56e179d6a5f254a442dcc9690153a75b9df10 Mon Sep 17 00:00:00 2001
From: ghost
Date: Mon, 14 Oct 2024 13:56:50 +0000
Subject: [PATCH 14/20] Auto Generate Syntax Docs + JSONSchema [Mon Oct 14
13:56:50 UTC 2024] :robot:
---
SYNTAX-REFERENCE.md | 13 +++++++++++++
nuclei-jsonschema.json | 5 +++++
pkg/templates/templates_doc.go | 7 ++++++-
3 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/SYNTAX-REFERENCE.md b/SYNTAX-REFERENCE.md
index 394f0a0684..c448d251d3 100755
--- a/SYNTAX-REFERENCE.md
+++ b/SYNTAX-REFERENCE.md
@@ -1651,6 +1651,19 @@ FuzzPreConditionOperator is the operator between multiple PreConditions for fuzz
+
+
+global-matchers
bool
+
+
+
+
+GlobalMatchers marks matchers as static and applies globally to all result events from other templates
+
+
+
+
+
diff --git a/nuclei-jsonschema.json b/nuclei-jsonschema.json
index b4ed1d2982..8a8d5dd1ba 100644
--- a/nuclei-jsonschema.json
+++ b/nuclei-jsonschema.json
@@ -889,6 +889,11 @@
],
"title": "condition between the filters",
"description": "Operator to use between multiple per-conditions"
+ },
+ "global-matchers": {
+ "type": "boolean",
+ "title": "global matchers",
+ "description": "marks matchers as static and applies globally to all result events from other templates"
}
},
"additionalProperties": false,
diff --git a/pkg/templates/templates_doc.go b/pkg/templates/templates_doc.go
index b1422c8333..9a7fc97a37 100644
--- a/pkg/templates/templates_doc.go
+++ b/pkg/templates/templates_doc.go
@@ -459,7 +459,7 @@ func init() {
Value: "HTTP response headers in name:value format",
},
}
- HTTPRequestDoc.Fields = make([]encoder.Doc, 36)
+ HTTPRequestDoc.Fields = make([]encoder.Doc, 37)
HTTPRequestDoc.Fields[0].Name = "path"
HTTPRequestDoc.Fields[0].Type = "[]string"
HTTPRequestDoc.Fields[0].Note = ""
@@ -668,6 +668,11 @@ func init() {
HTTPRequestDoc.Fields[35].Note = ""
HTTPRequestDoc.Fields[35].Description = "FuzzPreConditionOperator is the operator between multiple PreConditions for fuzzing Default is OR"
HTTPRequestDoc.Fields[35].Comments[encoder.LineComment] = "FuzzPreConditionOperator is the operator between multiple PreConditions for fuzzing Default is OR"
+ HTTPRequestDoc.Fields[36].Name = "global-matchers"
+ HTTPRequestDoc.Fields[36].Type = "bool"
+ HTTPRequestDoc.Fields[36].Note = ""
+ HTTPRequestDoc.Fields[36].Description = "GlobalMatchers marks matchers as static and applies globally to all result events from other templates"
+ HTTPRequestDoc.Fields[36].Comments[encoder.LineComment] = "GlobalMatchers marks matchers as static and applies globally to all result events from other templates"
GENERATORSAttackTypeHolderDoc.Type = "generators.AttackTypeHolder"
GENERATORSAttackTypeHolderDoc.Comments[encoder.LineComment] = " AttackTypeHolder is used to hold internal type of the protocol"
From 2c832f559045423bbc9bd1e9b87d85928f429e5c Mon Sep 17 00:00:00 2001
From: Dwi Siswanto
Date: Mon, 14 Oct 2024 21:01:36 +0700
Subject: [PATCH 15/20] refactor(vardump): use `godump` lib (#5676)
* refactor(vardump): use `godump` lib
also increate limit char to `255`.
Signed-off-by: Dwi Siswanto
* feat(vardump): add global var `Limit`
Signed-off-by: Dwi Siswanto
* chore(protocols): rm newline
Signed-off-by: Dwi Siswanto
* feat(types): add `VarDumpLimit` option
Signed-off-by: Dwi Siswanto
* test(vardump): add test cases
Signed-off-by: Dwi Siswanto
* chore: tidy up mod
Signed-off-by: Dwi Siswanto
---------
Signed-off-by: Dwi Siswanto
---
cmd/nuclei/main.go | 1 +
go.mod | 3 +-
go.sum | 2 +
internal/runner/options.go | 1 +
pkg/protocols/code/code.go | 2 +-
.../helpers/eventcreator/eventcreator.go | 2 +-
pkg/protocols/common/utils/vardump/dump.go | 84 +++++++++++--------
.../common/utils/vardump/dump_test.go | 55 ++++++++++++
pkg/protocols/common/utils/vardump/vars.go | 8 ++
pkg/protocols/dns/request.go | 2 +-
pkg/protocols/headless/engine/page_actions.go | 2 +-
pkg/protocols/headless/request.go | 2 +-
pkg/protocols/http/build_request.go | 2 +-
pkg/protocols/javascript/js.go | 2 +-
pkg/protocols/network/request.go | 2 +-
pkg/protocols/ssl/ssl.go | 2 +-
pkg/protocols/websocket/websocket.go | 2 +-
pkg/protocols/whois/whois.go | 2 +-
pkg/types/types.go | 2 +
19 files changed, 131 insertions(+), 47 deletions(-)
create mode 100644 pkg/protocols/common/utils/vardump/dump_test.go
create mode 100644 pkg/protocols/common/utils/vardump/vars.go
diff --git a/cmd/nuclei/main.go b/cmd/nuclei/main.go
index caac7e5546..5e95a8b1b2 100644
--- a/cmd/nuclei/main.go
+++ b/cmd/nuclei/main.go
@@ -425,6 +425,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVar(&memProfile, "profile-mem", "", "generate memory (heap) profile & trace files"),
flagSet.BoolVar(&options.VerboseVerbose, "vv", false, "display templates loaded for scan"),
flagSet.BoolVarP(&options.ShowVarDump, "show-var-dump", "svd", false, "show variables dump for debugging"),
+ flagSet.IntVarP(&options.VarDumpLimit, "var-dump-limit", "vdl", 255, "limit the number of characters displayed in var dump"),
flagSet.BoolVarP(&options.EnablePprof, "enable-pprof", "ep", false, "enable pprof debugging server"),
flagSet.CallbackVarP(printTemplateVersion, "templates-version", "tv", "shows the version of the installed nuclei-templates"),
flagSet.BoolVarP(&options.HealthCheck, "health-check", "hc", false, "run diagnostic check up"),
diff --git a/go.mod b/go.mod
index 86a8a00643..db236f5908 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/projectdiscovery/nuclei/v3
-go 1.21
+go 1.21.0
require (
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible
@@ -104,6 +104,7 @@ require (
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/stretchr/testify v1.9.0
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
+ github.com/yassinebenaid/godump v0.10.0
github.com/zmap/zgrab2 v0.1.8-0.20230806160807-97ba87c0e706
go.mongodb.org/mongo-driver v1.17.0
golang.org/x/term v0.24.0
diff --git a/go.sum b/go.sum
index b69d6283a5..556ba04f24 100644
--- a/go.sum
+++ b/go.sum
@@ -1104,6 +1104,8 @@ github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yassinebenaid/godump v0.10.0 h1:FolBA+Ix5uwUiXYBBYOsf1VkT5+0f4gtFNTkYTiIR08=
+github.com/yassinebenaid/godump v0.10.0/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
diff --git a/internal/runner/options.go b/internal/runner/options.go
index 2872b96a7a..4ad62a855a 100644
--- a/internal/runner/options.go
+++ b/internal/runner/options.go
@@ -68,6 +68,7 @@ func ParseOptions(options *types.Options) {
if options.ShowVarDump {
vardump.EnableVarDump = true
+ vardump.Limit = options.VarDumpLimit
}
if options.ShowActions {
gologger.Info().Msgf("Showing available headless actions: ")
diff --git a/pkg/protocols/code/code.go b/pkg/protocols/code/code.go
index 87a0128645..4503b2117f 100644
--- a/pkg/protocols/code/code.go
+++ b/pkg/protocols/code/code.go
@@ -235,7 +235,7 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
gologger.Verbose().Msgf("[%s] Executed code on local machine %v", request.options.TemplateID, input.MetaInput.Input)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Code Protocol request variables: \n%s\n", vardump.DumpVariables(allvars))
+ gologger.Debug().Msgf("Code Protocol request variables: %s\n", vardump.DumpVariables(allvars))
}
if request.options.Options.Debug || request.options.Options.DebugRequests {
diff --git a/pkg/protocols/common/helpers/eventcreator/eventcreator.go b/pkg/protocols/common/helpers/eventcreator/eventcreator.go
index 078963ceed..22c0a7810d 100644
--- a/pkg/protocols/common/helpers/eventcreator/eventcreator.go
+++ b/pkg/protocols/common/helpers/eventcreator/eventcreator.go
@@ -24,7 +24,7 @@ func CreateEventWithAdditionalOptions(request protocols.Request, outputEvent out
// Dump response variables if ran in debug mode
if vardump.EnableVarDump {
protoName := cases.Title(language.English).String(request.Type().String())
- gologger.Debug().Msgf("%v Protocol response variables: \n%s\n", protoName, vardump.DumpVariables(outputEvent))
+ gologger.Debug().Msgf("%v Protocol response variables: %s\n", protoName, vardump.DumpVariables(outputEvent))
}
for _, compiledOperator := range request.GetCompiledOperators() {
if compiledOperator != nil {
diff --git a/pkg/protocols/common/utils/vardump/dump.go b/pkg/protocols/common/utils/vardump/dump.go
index ab4f56b113..82de85e67b 100644
--- a/pkg/protocols/common/utils/vardump/dump.go
+++ b/pkg/protocols/common/utils/vardump/dump.go
@@ -1,53 +1,67 @@
package vardump
import (
- "strconv"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
mapsutil "github.com/projectdiscovery/utils/maps"
+ "github.com/yassinebenaid/godump"
)
-// EnableVarDump enables var dump for debugging optionally
-var EnableVarDump bool
+// variables is a map of variables
+type variables = map[string]any
-// DumpVariables writes the truncated dump of variables to a string
-// in a formatted key-value manner.
-//
-// The values are truncated to return 50 characters from start and end.
-func DumpVariables(data map[string]interface{}) string {
- var counter int
+// DumpVariables dumps the variables in a pretty format
+func DumpVariables(data variables) string {
+ if !EnableVarDump {
+ return ""
+ }
+
+ d := godump.Dumper{
+ Indentation: " ",
+ HidePrivateFields: false,
+ ShowPrimitiveNamedTypes: true,
+ }
+
+ d.Theme = godump.Theme{
+ String: godump.RGB{R: 138, G: 201, B: 38},
+ Quotes: godump.RGB{R: 112, G: 214, B: 255},
+ Bool: godump.RGB{R: 249, G: 87, B: 56},
+ Number: godump.RGB{R: 10, G: 178, B: 242},
+ Types: godump.RGB{R: 0, G: 150, B: 199},
+ Address: godump.RGB{R: 205, G: 93, B: 0},
+ PointerTag: godump.RGB{R: 110, G: 110, B: 110},
+ Nil: godump.RGB{R: 219, G: 57, B: 26},
+ Func: godump.RGB{R: 160, G: 90, B: 220},
+ Fields: godump.RGB{R: 189, G: 176, B: 194},
+ Chan: godump.RGB{R: 195, G: 154, B: 76},
+ UnsafePointer: godump.RGB{R: 89, G: 193, B: 180},
+ Braces: godump.RGB{R: 185, G: 86, B: 86},
+ }
- buffer := &strings.Builder{}
- buffer.Grow(len(data) * 78) // grow buffer to an approximate size
+ return d.Sprint(process(data, Limit))
+}
- builder := &strings.Builder{}
- // sort keys for deterministic output
+// process is a helper function that processes the variables
+// and returns a new map of variables
+func process(data variables, limit int) variables {
keys := mapsutil.GetSortedKeys(data)
+ vars := make(variables)
+
+ if limit == 0 {
+ limit = 255
+ }
for _, k := range keys {
- v := data[k]
- valueString := types.ToString(v)
-
- counter++
- if len(valueString) > 50 {
- builder.Grow(56)
- builder.WriteString(valueString[0:25])
- builder.WriteString(" .... ")
- builder.WriteString(valueString[len(valueString)-25:])
- valueString = builder.String()
- builder.Reset()
+ v := types.ToString(data[k])
+ v = strings.ReplaceAll(strings.ReplaceAll(v, "\r", " "), "\n", " ")
+ if len(v) > limit {
+ v = v[:limit]
+ v += " [...]"
}
- valueString = strings.ReplaceAll(strings.ReplaceAll(valueString, "\r", " "), "\n", " ")
-
- buffer.WriteString("\t")
- buffer.WriteString(strconv.Itoa(counter))
- buffer.WriteString(". ")
- buffer.WriteString(k)
- buffer.WriteString(" => ")
- buffer.WriteString(valueString)
- buffer.WriteString("\n")
+
+ vars[k] = v
}
- final := buffer.String()
- return final
+
+ return vars
}
diff --git a/pkg/protocols/common/utils/vardump/dump_test.go b/pkg/protocols/common/utils/vardump/dump_test.go
new file mode 100644
index 0000000000..9929fa5318
--- /dev/null
+++ b/pkg/protocols/common/utils/vardump/dump_test.go
@@ -0,0 +1,55 @@
+package vardump
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDumpVariables(t *testing.T) {
+ // Enable var dump for testing
+ EnableVarDump = true
+
+ // Test case
+ testVars := variables{
+ "string": "test",
+ "int": 42,
+ "bool": true,
+ "slice": []string{"a", "b", "c"},
+ }
+
+ result := DumpVariables(testVars)
+
+ // Assertions
+ assert.NotEmpty(t, result)
+ assert.Contains(t, result, "string")
+ assert.Contains(t, result, "test")
+ assert.Contains(t, result, "int")
+ assert.Contains(t, result, "42")
+ assert.Contains(t, result, "bool")
+ assert.Contains(t, result, "true")
+ assert.Contains(t, result, "slice")
+ assert.Contains(t, result, "a")
+ assert.Contains(t, result, "b")
+ assert.Contains(t, result, "c")
+
+ // Test with EnableVarDump set to false
+ EnableVarDump = false
+ result = DumpVariables(testVars)
+ assert.Empty(t, result)
+}
+
+func TestProcess(t *testing.T) {
+ testVars := variables{
+ "short": "short string",
+ "long": strings.Repeat("a", 300),
+ "number": 42,
+ }
+
+ processed := process(testVars, 255)
+
+ assert.Equal(t, "short string", processed["short"])
+ assert.Equal(t, strings.Repeat("a", 255)+" [...]", processed["long"])
+ assert.Equal(t, "42", processed["number"])
+}
diff --git a/pkg/protocols/common/utils/vardump/vars.go b/pkg/protocols/common/utils/vardump/vars.go
new file mode 100644
index 0000000000..f5e18bce69
--- /dev/null
+++ b/pkg/protocols/common/utils/vardump/vars.go
@@ -0,0 +1,8 @@
+package vardump
+
+var (
+ // EnableVarDump enables var dump for debugging optionally
+ EnableVarDump bool
+ // Limit is the maximum characters to be dumped
+ Limit int = 255
+)
diff --git a/pkg/protocols/dns/request.go b/pkg/protocols/dns/request.go
index 9457845270..6e82c047bc 100644
--- a/pkg/protocols/dns/request.go
+++ b/pkg/protocols/dns/request.go
@@ -108,7 +108,7 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, metadata,
func (request *Request) execute(input *contextargs.Context, domain string, metadata, previous output.InternalEvent, vars map[string]interface{}, callback protocols.OutputEventCallback) error {
var err error
if vardump.EnableVarDump {
- gologger.Debug().Msgf("DNS Protocol request variables: \n%s\n", vardump.DumpVariables(vars))
+ gologger.Debug().Msgf("DNS Protocol request variables: %s\n", vardump.DumpVariables(vars))
}
// Compile each request for the template based on the URL
diff --git a/pkg/protocols/headless/engine/page_actions.go b/pkg/protocols/headless/engine/page_actions.go
index 18ed515662..640bb76214 100644
--- a/pkg/protocols/headless/engine/page_actions.go
+++ b/pkg/protocols/headless/engine/page_actions.go
@@ -334,7 +334,7 @@ func (p *Page) NavigateURL(action *Action, out ActionData, allvars map[string]in
allvars = generators.MergeMaps(allvars, defaultReqVars)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Headless Protocol request variables: \n%s\n", vardump.DumpVariables(allvars))
+ gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(allvars))
}
// Evaluate the target url with all variables
diff --git a/pkg/protocols/headless/request.go b/pkg/protocols/headless/request.go
index 5f9b53174e..aae70aa347 100644
--- a/pkg/protocols/headless/request.go
+++ b/pkg/protocols/headless/request.go
@@ -122,7 +122,7 @@ func (request *Request) executeRequestWithPayloads(input *contextargs.Context, p
defer instance.Close()
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Headless Protocol request variables: \n%s\n", vardump.DumpVariables(payloads))
+ gologger.Debug().Msgf("Headless Protocol request variables: %s\n", vardump.DumpVariables(payloads))
}
instance.SetInteractsh(request.options.Interactsh)
diff --git a/pkg/protocols/http/build_request.go b/pkg/protocols/http/build_request.go
index 1b046bffd8..bc3b41244e 100644
--- a/pkg/protocols/http/build_request.go
+++ b/pkg/protocols/http/build_request.go
@@ -204,7 +204,7 @@ func (r *requestGenerator) Make(ctx context.Context, input *contextargs.Context,
finalVars := generators.MergeMaps(allVars, payloads)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("HTTP Protocol request variables: \n%s\n", vardump.DumpVariables(finalVars))
+ gologger.Debug().Msgf("HTTP Protocol request variables: %s\n", vardump.DumpVariables(finalVars))
}
// Note: If possible any changes to current logic (i.e evaluate -> then parse URL)
diff --git a/pkg/protocols/javascript/js.go b/pkg/protocols/javascript/js.go
index 0bd26b88a5..5953c9c296 100644
--- a/pkg/protocols/javascript/js.go
+++ b/pkg/protocols/javascript/js.go
@@ -319,7 +319,7 @@ func (request *Request) ExecuteWithResults(target *contextargs.Context, dynamicV
templateCtx.Merge(payloadValues)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Javascript Protocol request variables: \n%s\n", vardump.DumpVariables(payloadValues))
+ gologger.Debug().Msgf("JavaScript Protocol request variables: %s\n", vardump.DumpVariables(payloadValues))
}
if request.PreCondition != "" {
diff --git a/pkg/protocols/network/request.go b/pkg/protocols/network/request.go
index 32d4ae3494..29cdca4ad2 100644
--- a/pkg/protocols/network/request.go
+++ b/pkg/protocols/network/request.go
@@ -283,7 +283,7 @@ func (request *Request) executeRequestWithPayloads(variables map[string]interfac
interimValues := generators.MergeMaps(variables, payloads)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Network Protocol request variables: \n%s\n", vardump.DumpVariables(interimValues))
+ gologger.Debug().Msgf("Network Protocol request variables: %s\n", vardump.DumpVariables(interimValues))
}
inputEvents := make(map[string]interface{})
diff --git a/pkg/protocols/ssl/ssl.go b/pkg/protocols/ssl/ssl.go
index 554a4f5c10..fd0dae83db 100644
--- a/pkg/protocols/ssl/ssl.go
+++ b/pkg/protocols/ssl/ssl.go
@@ -222,7 +222,7 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
payloadValues = generators.MergeMaps(variablesMap, payloadValues, request.options.Constants)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("SSL Protocol request variables: \n%s\n", vardump.DumpVariables(payloadValues))
+ gologger.Debug().Msgf("SSL Protocol request variables: %s\n", vardump.DumpVariables(payloadValues))
}
finalAddress, dataErr := expressions.EvaluateByte([]byte(request.Address), payloadValues)
diff --git a/pkg/protocols/websocket/websocket.go b/pkg/protocols/websocket/websocket.go
index aa099ef43a..8eeeedf217 100644
--- a/pkg/protocols/websocket/websocket.go
+++ b/pkg/protocols/websocket/websocket.go
@@ -207,7 +207,7 @@ func (request *Request) executeRequestWithPayloads(target *contextargs.Context,
}
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Websocket Protocol request variables: \n%s\n", vardump.DumpVariables(payloadValues))
+ gologger.Debug().Msgf("WebSocket Protocol request variables: %s\n", vardump.DumpVariables(payloadValues))
}
finalAddress, dataErr := expressions.EvaluateByte([]byte(request.Address), payloadValues)
diff --git a/pkg/protocols/whois/whois.go b/pkg/protocols/whois/whois.go
index 9963ec19f8..91d0edcf8a 100644
--- a/pkg/protocols/whois/whois.go
+++ b/pkg/protocols/whois/whois.go
@@ -99,7 +99,7 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
variables := generators.MergeMaps(vars, defaultVars, optionVars, dynamicValues, request.options.Constants)
if vardump.EnableVarDump {
- gologger.Debug().Msgf("Whois Protocol request variables: \n%s\n", vardump.DumpVariables(variables))
+ gologger.Debug().Msgf("Whois Protocol request variables: %s\n", vardump.DumpVariables(variables))
}
// and replace placeholders
diff --git a/pkg/types/types.go b/pkg/types/types.go
index 9cc88f49ff..f6e7ab4470 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -206,6 +206,8 @@ type Options struct {
VerboseVerbose bool
// ShowVarDump displays variable dump
ShowVarDump bool
+ // VarDumpLimit limits the number of characters displayed in var dump
+ VarDumpLimit int
// No-Color disables the colored output.
NoColor bool
// UpdateTemplates updates the templates installed at startup (also used by cloud to update datasources)
From 98948d0266354fbbd7bbfbaf4041e7e8a59dcda7 Mon Sep 17 00:00:00 2001
From: Ramana Reddy <90540245+RamanaReddy0M@users.noreply.github.com>
Date: Mon, 14 Oct 2024 20:54:58 +0530
Subject: [PATCH 16/20] support stop-at-first-match for network templates
(#5554)
---
pkg/protocols/network/network.go | 4 ++++
pkg/protocols/network/request.go | 33 ++++++++++++++++++++++++++++----
2 files changed, 33 insertions(+), 4 deletions(-)
diff --git a/pkg/protocols/network/network.go b/pkg/protocols/network/network.go
index 70d618dcb5..c90f5e7019 100644
--- a/pkg/protocols/network/network.go
+++ b/pkg/protocols/network/network.go
@@ -85,6 +85,10 @@ type Request struct {
// SelfContained specifies if the request is self-contained.
SelfContained bool `yaml:"-" json:"-"`
+ // description: |
+ // StopAtFirstMatch stops the execution of the requests and template as soon as a match is found.
+ StopAtFirstMatch bool `yaml:"stop-at-first-match,omitempty" json:"stop-at-first-match,omitempty" jsonschema:"title=stop at first match,description=Stop the execution after a match is found"`
+
// description: |
// ports is post processed list of ports to scan (obtained from Port)
ports []string `yaml:"-" json:"-"`
diff --git a/pkg/protocols/network/request.go b/pkg/protocols/network/request.go
index 29cdca4ad2..10c71b04db 100644
--- a/pkg/protocols/network/request.go
+++ b/pkg/protocols/network/request.go
@@ -8,6 +8,7 @@ import (
"os"
"strings"
"sync"
+ "sync/atomic"
"time"
"github.com/pkg/errors"
@@ -99,6 +100,16 @@ func (request *Request) ExecuteWithResults(target *contextargs.Context, metadata
gologger.Verbose().Msgf("[%v] got errors while checking open ports: %s\n", request.options.TemplateID, err)
}
+ // stop at first match if requested
+ atomicBool := &atomic.Bool{}
+ shouldStopAtFirstMatch := request.StopAtFirstMatch || request.options.StopAtFirstMatch || request.options.Options.StopAtFirstMatch
+ wrappedCallback := func(event *output.InternalWrappedEvent) {
+ if event != nil && event.HasOperatorResult() {
+ atomicBool.Store(true)
+ }
+ callback(event)
+ }
+
for _, port := range ports {
input := target.Clone()
// use network port updates input with new port requested in template file
@@ -107,9 +118,12 @@ func (request *Request) ExecuteWithResults(target *contextargs.Context, metadata
if err := input.UseNetworkPort(port, request.ExcludePorts); err != nil {
gologger.Debug().Msgf("Could not network port from constants: %s\n", err)
}
- if err := request.executeOnTarget(input, visitedAddresses, metadata, previous, callback); err != nil {
+ if err := request.executeOnTarget(input, visitedAddresses, metadata, previous, wrappedCallback); err != nil {
return err
}
+ if shouldStopAtFirstMatch && atomicBool.Load() {
+ break
+ }
}
return nil
@@ -141,6 +155,16 @@ func (request *Request) executeOnTarget(input *contextargs.Context, visited maps
variablesMap := request.options.Variables.Evaluate(variables)
variables = generators.MergeMaps(variablesMap, variables, request.options.Constants)
+ // stop at first match if requested
+ atomicBool := &atomic.Bool{}
+ shouldStopAtFirstMatch := request.StopAtFirstMatch || request.options.StopAtFirstMatch || request.options.Options.StopAtFirstMatch
+ wrappedCallback := func(event *output.InternalWrappedEvent) {
+ if event != nil && event.HasOperatorResult() {
+ atomicBool.Store(true)
+ }
+ callback(event)
+ }
+
for _, kv := range request.addresses {
select {
case <-input.Context().Done():
@@ -154,12 +178,13 @@ func (request *Request) executeOnTarget(input *contextargs.Context, visited maps
continue
}
visited.Set(actualAddress, struct{}{})
-
- if err = request.executeAddress(variables, actualAddress, address, input, kv.tls, previous, callback); err != nil {
+ if err = request.executeAddress(variables, actualAddress, address, input, kv.tls, previous, wrappedCallback); err != nil {
outputEvent := request.responseToDSLMap("", "", "", address, "")
callback(&output.InternalWrappedEvent{InternalEvent: outputEvent})
gologger.Warning().Msgf("[%v] Could not make network request for (%s) : %s\n", request.options.TemplateID, actualAddress, err)
- continue
+ }
+ if shouldStopAtFirstMatch && atomicBool.Load() {
+ break
}
}
return err
From 159a8a53cfd258e4c2556e6d24bc63957e78e1ed Mon Sep 17 00:00:00 2001
From: ghost
Date: Mon, 14 Oct 2024 15:26:08 +0000
Subject: [PATCH 17/20] Auto Generate Syntax Docs + JSONSchema [Mon Oct 14
15:26:08 UTC 2024] :robot:
---
SYNTAX-REFERENCE.md | 13 +++++++++++++
nuclei-jsonschema.json | 5 +++++
pkg/templates/templates_doc.go | 7 ++++++-
3 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/SYNTAX-REFERENCE.md b/SYNTAX-REFERENCE.md
index c448d251d3..6d1d9effc4 100755
--- a/SYNTAX-REFERENCE.md
+++ b/SYNTAX-REFERENCE.md
@@ -3223,6 +3223,19 @@ read-all: false
+
+
+stop-at-first-match
bool
+
+
+
+
+StopAtFirstMatch stops the execution of the requests and template as soon as a match is found.
+
+
+
+
+
diff --git a/nuclei-jsonschema.json b/nuclei-jsonschema.json
index 8a8d5dd1ba..97477e1f7e 100644
--- a/nuclei-jsonschema.json
+++ b/nuclei-jsonschema.json
@@ -1370,6 +1370,11 @@
"title": "read all response stream",
"description": "Read all response stream till the server stops sending"
},
+ "stop-at-first-match": {
+ "type": "boolean",
+ "title": "stop at first match",
+ "description": "Stop the execution after a match is found"
+ },
"matchers": {
"items": {
"$ref": "#/$defs/matchers.Matcher"
diff --git a/pkg/templates/templates_doc.go b/pkg/templates/templates_doc.go
index 9a7fc97a37..9bf754e24c 100644
--- a/pkg/templates/templates_doc.go
+++ b/pkg/templates/templates_doc.go
@@ -1334,7 +1334,7 @@ func init() {
Value: "Full Network protocol data",
},
}
- NETWORKRequestDoc.Fields = make([]encoder.Doc, 10)
+ NETWORKRequestDoc.Fields = make([]encoder.Doc, 11)
NETWORKRequestDoc.Fields[0].Name = "id"
NETWORKRequestDoc.Fields[0].Type = "string"
NETWORKRequestDoc.Fields[0].Note = ""
@@ -1393,6 +1393,11 @@ func init() {
NETWORKRequestDoc.Fields[9].Comments[encoder.LineComment] = "ReadAll determines if the data stream should be read till the end regardless of the size"
NETWORKRequestDoc.Fields[9].AddExample("", false)
+ NETWORKRequestDoc.Fields[10].Name = "stop-at-first-match"
+ NETWORKRequestDoc.Fields[10].Type = "bool"
+ NETWORKRequestDoc.Fields[10].Note = ""
+ NETWORKRequestDoc.Fields[10].Description = "StopAtFirstMatch stops the execution of the requests and template as soon as a match is found."
+ NETWORKRequestDoc.Fields[10].Comments[encoder.LineComment] = "StopAtFirstMatch stops the execution of the requests and template as soon as a match is found."
NETWORKInputDoc.Type = "network.Input"
NETWORKInputDoc.Comments[encoder.LineComment] = ""
From 7e4b4a8c55f2ef1c3495f8c41cfea9d77dd7b7df Mon Sep 17 00:00:00 2001
From: Ramana Reddy <90540245+RamanaReddy0M@users.noreply.github.com>
Date: Fri, 18 Oct 2024 20:44:40 +0530
Subject: [PATCH 18/20] fix: interactsh-url placeholder replacement in
variables for network template (#5677)
---
pkg/protocols/network/request.go | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/pkg/protocols/network/request.go b/pkg/protocols/network/request.go
index 10c71b04db..3579acd3b8 100644
--- a/pkg/protocols/network/request.go
+++ b/pkg/protocols/network/request.go
@@ -314,30 +314,31 @@ func (request *Request) executeRequestWithPayloads(variables map[string]interfac
inputEvents := make(map[string]interface{})
for _, input := range request.Inputs {
- data := []byte(input.Data)
+ dataInBytes := []byte(input.Data)
+ var err error
- if request.options.Interactsh != nil {
- var transformedData string
- transformedData, interactshURLs = request.options.Interactsh.Replace(string(data), []string{})
- data = []byte(transformedData)
- }
-
- finalData, err := expressions.EvaluateByte(data, interimValues)
+ dataInBytes, err = expressions.EvaluateByte(dataInBytes, interimValues)
if err != nil {
request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)
request.options.Progress.IncrementFailedRequestsBy(1)
return errors.Wrap(err, "could not evaluate template expressions")
}
- reqBuilder.Write(finalData)
+ data := string(dataInBytes)
+ if request.options.Interactsh != nil {
+ data, interactshURLs = request.options.Interactsh.Replace(data, []string{})
+ dataInBytes = []byte(data)
+ }
+
+ reqBuilder.Write(dataInBytes)
- if err := expressions.ContainsUnresolvedVariables(string(finalData)); err != nil {
+ if err := expressions.ContainsUnresolvedVariables(data); err != nil {
gologger.Warning().Msgf("[%s] Could not make network request for %s: %v\n", request.options.TemplateID, actualAddress, err)
return nil
}
if input.Type.GetType() == hexType {
- finalData, err = hex.DecodeString(string(finalData))
+ dataInBytes, err = hex.DecodeString(data)
if err != nil {
request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)
request.options.Progress.IncrementFailedRequestsBy(1)
@@ -345,7 +346,7 @@ func (request *Request) executeRequestWithPayloads(variables map[string]interfac
}
}
- if _, err := conn.Write(finalData); err != nil {
+ if _, err := conn.Write(dataInBytes); err != nil {
request.options.Output.Request(request.options.TemplatePath, address, request.Type().String(), err)
request.options.Progress.IncrementFailedRequestsBy(1)
return errors.Wrap(err, "could not write request to server")
From d7c8c8af8007a5e6175cc51322bd7a9d50ec7706 Mon Sep 17 00:00:00 2001
From: sandeep <8293321+ehsandeep@users.noreply.github.com>
Date: Fri, 18 Oct 2024 22:28:02 +0530
Subject: [PATCH 19/20] version update
---
pkg/catalog/config/constants.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/catalog/config/constants.go b/pkg/catalog/config/constants.go
index e29af00cdd..7d11b6f71e 100644
--- a/pkg/catalog/config/constants.go
+++ b/pkg/catalog/config/constants.go
@@ -31,7 +31,7 @@ const (
CLIConfigFileName = "config.yaml"
ReportingConfigFilename = "reporting-config.yaml"
// Version is the current version of nuclei
- Version = `v3.3.4`
+ Version = `v3.3.5`
// Directory Names of custom templates
CustomS3TemplatesDirName = "s3"
CustomGitHubTemplatesDirName = "github"
From 44f398c08da62acbf555d3b21f7260e5daff2ce1 Mon Sep 17 00:00:00 2001
From: sandeep <8293321+ehsandeep@users.noreply.github.com>
Date: Sat, 19 Oct 2024 17:21:13 +0530
Subject: [PATCH 20/20] readme updates
---
README.md | 372 +++++++++++++------
static/nuclei-cover-image.png | Bin 0 -> 818033 bytes
static/nuclei-cover.png | Bin 0 -> 818033 bytes
static/nuclei-getting-started.png | Bin 0 -> 45304 bytes
static/nuclei-template-example.png | Bin 0 -> 2155430 bytes
static/nuclei-templates-teamcity-example.png | Bin 0 -> 1162315 bytes
static/nuclei-templates-teamcity.png | Bin 0 -> 1538352 bytes
static/nuclei-write-your-first-template.png | Bin 0 -> 47472 bytes
static/projectdiscovery-browse-results.gif | Bin 0 -> 2862660 bytes
static/teamcity-example.png | Bin 0 -> 1098783 bytes
10 files changed, 252 insertions(+), 120 deletions(-)
create mode 100644 static/nuclei-cover-image.png
create mode 100644 static/nuclei-cover.png
create mode 100644 static/nuclei-getting-started.png
create mode 100644 static/nuclei-template-example.png
create mode 100644 static/nuclei-templates-teamcity-example.png
create mode 100644 static/nuclei-templates-teamcity.png
create mode 100644 static/nuclei-write-your-first-template.png
create mode 100644 static/projectdiscovery-browse-results.gif
create mode 100644 static/teamcity-example.png
diff --git a/README.md b/README.md
index 412d348645..02cacb9ccf 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,4 @@
-
-
-
-
-
-Fast and customisable vulnerability scanner based on simple YAML based DSL.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- How •
- Install •
- Documentation •
- Credits •
- FAQs •
- Join Discord
-
+![nuclei](/static/nuclei-cover-image.png)
English •
@@ -34,78 +8,103 @@
Spanish •
日本語
-
----
-
-Nuclei is used to send requests across targets based on a template, leading to zero false positives and providing fast scanning on a large number of hosts. Nuclei offers scanning for a variety of protocols, including TCP, DNS, HTTP, SSL, File, Whois, Websocket, Headless, Code etc. With powerful and flexible templating, Nuclei can be used to model all kinds of security checks.
+
-We have a [dedicated repository](https://github.com/projectdiscovery/nuclei-templates) that houses various type of vulnerability templates contributed by **more than 300** security researchers and engineers.
+
-## How it works
+
+
+
+
+
+
+
+---
-
-
-
+
+
+
+
+Nuclei is a modern, high-performance vulnerability scanner that leverages simple YAML-based templates. It empowers you to design custom vulnerability detection scenarios that mimic real-world conditions, leading to zero false positives.
+
+- Simple YAML format for creating and customizing vulnerability templates.
+- Contributed by thousands of security professionals to tackle trending vulnerabilities.
+- Reduce false positives by simulating real-world steps to verify a vulnerability.
+- Ultra-fast parallel scan processing and request clustering.
+- Integrate into CI/CD pipelines for vulnerability detection and regression testing.
+- Supports multiple protocols like TCP, DNS, HTTP, SSL, WHOIS JavaScript, Code and more.
+- Integrate with Jira, Splunk, GitHub, Elastic, GitLab.
+
+## Table of Contents
+
+- [Get Started](#get-started)
+ - [1. Nuclei CLI](#1-nuclei-cli)
+ - [2. Pro and Enterprise Editions](#2-pro-and-enterprise-editions)
+- [Documentation](#documentation)
+ - [Command Line Flags](#command-line-flags)
+ - [Single target scan](#single-target-scan)
+ - [Scanning multiple targets](#scanning-multiple-targets)
+ - [Network scan](#network-scan)
+ - [Scanning with your custom template](#scanning-with-your-custom-template)
+ - [Connect Nuclei to ProjectDiscovery](#connect-nuclei-to-projectdiscovery)
+- [Nuclei Templates, Community and Rewards 💎](#nuclei-templates-community-and-rewards-)
+- [Our Mission](#our-mission)
+- [Contributors ❤️](#contributors-️)
+- [License](#license)
+---
| :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. |
+| This project is 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. |
-# Install Nuclei
+## Get Started
-Nuclei requires **go1.21** to install successfully. Run the following command to install the latest version -
+### **1. Nuclei CLI**
-```sh
-go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
-```
+Install Nuclei on your machine. Get started by following the installation guide [here](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme). Additionally, We provide [a free cloud tier](https://cloud.projectdiscovery.io/sign-up) and comes with a generous monthly free limits:
-
- Brew
-
- ```sh
- brew install nuclei
- ```
-
-
-
- Docker
-
- ```sh
- docker pull projectdiscovery/nuclei:latest
- ```
-
-
+- Store and visualize your vulnerability findings
+- Write and manage your nuclei templates
+- Access latest nuclei templates
+- Discover and store your targets
+
+### **2. Pro and Enterprise Editions**
-**More installation [methods can be found here](https://docs.projectdiscovery.io/tools/nuclei/install).**
+For security teams and enterprises, we provide a cloud-hosted service built on top of Nuclei OSS, fine-tuned to help you continuously run vulnerability scans at scale with your team and existing workflows:
-
-
-
+- 50x faster scans
+- Large scale scanning with high accuracy
+- Integrations with cloud services (AWS, GCP, Azure, CloudFlare, Fastly, Terraform, Kubernetes)
+- Jira, Slack, Linear, APIs and Webhooks
+- Executive and compliance reporting
+- Plus: Real-time scanning, SAML SSO, SOC 2 compliant platform (with EU and US hosting options), shared team workspaces, and more
+- We're constantly [adding new features](https://feedback.projectdiscovery.io/changelog)!
+- **Ideal for:** Pentesters, security teams, and enterprises
-### Nuclei Templates
+## Documentation
-Nuclei has built-in support for automatic template download/update as default since version [v2.5.2](https://github.com/projectdiscovery/nuclei/releases/tag/v2.5.2). [**Nuclei-Templates**](https://github.com/projectdiscovery/nuclei-templates) project provides a community-contributed list of ready-to-use templates that is constantly updated.
+Browse the full Nuclei [documentation here](https://docs.projectdiscovery.io/tools/nuclei/running). If you’re new to Nuclei, check out our [foundational Youtube series.](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl)
-You may still use the `update-templates` flag to update the nuclei templates at any time; You can write your own checks for your individual workflow and needs following Nuclei's [templating guide](https://docs.projectdiscovery.io/templates/).
+
-The YAML DSL reference syntax is available [here](SYNTAX-REFERENCE.md).
+
- |
-
-
+
-### Usage
+### Command Line Flags
+
+To display all the flags for the tool:
```sh
nuclei -h
```
-This will display help for the tool. Here are all the switches it supports.
-
+
+ Expand full help flags
```console
Nuclei is a fast, template based vulnerability scanner focusing
@@ -279,23 +278,24 @@ HEADLESS:
-lha, -list-headless-action list available headless actions
DEBUG:
- -debug show all requests and responses
- -dreq, -debug-req show all sent requests
- -dresp, -debug-resp show all received responses
- -p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
- -pi, -proxy-internal proxy all internal requests
- -ldf, -list-dsl-function list all supported DSL function signatures
- -tlog, -trace-log string file to write sent requests trace log
- -elog, -error-log string file to write sent requests error log
- -version show nuclei version
- -hm, -hang-monitor enable nuclei hang monitoring
- -v, -verbose show verbose output
- -profile-mem string optional nuclei memory profile dump file
- -vv display templates loaded for scan
- -svd, -show-var-dump show variables dump for debugging
- -ep, -enable-pprof enable pprof debugging server
- -tv, -templates-version shows the version of the installed nuclei-templates
- -hc, -health-check run diagnostic check up
+ -debug show all requests and responses
+ -dreq, -debug-req show all sent requests
+ -dresp, -debug-resp show all received responses
+ -p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
+ -pi, -proxy-internal proxy all internal requests
+ -ldf, -list-dsl-function list all supported DSL function signatures
+ -tlog, -trace-log string file to write sent requests trace log
+ -elog, -error-log string file to write sent requests error log
+ -version show nuclei version
+ -hm, -hang-monitor enable nuclei hang monitoring
+ -v, -verbose show verbose output
+ -profile-mem string generate memory (heap) profile & trace files
+ -vv display templates loaded for scan
+ -svd, -show-var-dump show variables dump for debugging
+ -vdl, -var-dump-limit int limit the number of characters displayed in var dump (default 255)
+ -ep, -enable-pprof enable pprof debugging server
+ -tv, -templates-version shows the version of the installed nuclei-templates
+ -hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
@@ -310,11 +310,13 @@ STATISTICS:
-mp, -metrics-port int port to expose nuclei metrics on (default 9092)
CLOUD:
- -auth configure projectdiscovery cloud (pdcp) api key (default true)
- -tid, -team-id string upload scan results to given team id (optional) (default "none")
- -cup, -cloud-upload upload scan results to pdcp dashboard
- -sid, -scan-id string upload scan results to existing scan id (optional)
- -sname, -scan-name string scan name to set (optional)
+ -auth configure projectdiscovery cloud (pdcp) api key (default true)
+ -tid, -team-id string upload scan results to given team id (optional) (default "none")
+ -cup, -cloud-upload upload scan results to pdcp dashboard [DEPRECATED use -dashboard]
+ -sid, -scan-id string upload scan results to existing scan id (optional)
+ -sname, -scan-name string scan name to set (optional)
+ -pd, -dashboard upload / view nuclei results in projectdiscovery cloud (pdcp) UI dashboard
+ -pdu, -dashboard-upload string upload / view nuclei results file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard
AUTHENTICATION:
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
@@ -323,59 +325,189 @@ AUTHENTICATION:
EXAMPLES:
Run nuclei on single host:
- $ nuclei -target example.com
+ $ nuclei -target example.com
Run nuclei with specific template directories:
- $ nuclei -target example.com -t http/cves/ -t ssl
+ $ nuclei -target example.com -t http/cves/ -t ssl
Run nuclei against a list of hosts:
- $ nuclei -list hosts.txt
+ $ nuclei -list hosts.txt
Run nuclei with a JSON output:
- $ nuclei -target example.com -json-export output.json
+ $ nuclei -target example.com -json-export output.json
Run nuclei with sorted Markdown outputs (with environment variables):
- $ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
+ $ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
Additional documentation is available at: https://docs.nuclei.sh/getting-started/running
```
-### Running Nuclei
+Additional documentation is available at: [https://docs.nuclei.sh/getting-started/running](https://docs.nuclei.sh/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
-See https://docs.projectdiscovery.io/tools/nuclei/running for details on running Nuclei
+
-### Using Nuclei From Go Code
+### Single target scan
-Complete guide of using Nuclei as Library/SDK is available at [godoc](https://pkg.go.dev/github.com/projectdiscovery/nuclei/v3/lib#section-readme)
+To perform a quick scan on web-application:
+```sh
+nuclei -target https://example.com
+```
-### Resources
+### Scanning multiple targets
-You can access the main documentation for Nuclei at https://docs.projectdiscovery.io/tools/nuclei/, and learn more about Nuclei in the cloud with [ProjectDiscovery Cloud Platform](https://cloud.projectdiscovery.io)
+Nuclei can handle bulk scanning by providing a list of targets. You can use a file containing multiple URLs.
-See https://docs.projectdiscovery.io/tools/nuclei/resources for more resources and videos about Nuclei!
+```sh
+nuclei -targets urls.txt
+```
-### Credits
+### Network scan
-Thanks to all the amazing [community contributors for sending PRs](https://github.com/projectdiscovery/nuclei/graphs/contributors) and keeping this project updated. :heart:
+This will scan the entire subnet for network-related issues, such as open ports or misconfigured services.
+
+```sh
+nuclei -target 192.168.1.0/24
+```
-If you have an idea or some kind of improvement, you are welcome to contribute and participate in the Project, feel free to send your PR.
+### Scanning with your custom template
-
-
-
-
+To write and use your own template, create a `.yaml` file with specific rules, then use it as follows.
+
+```sh
+nuclei -u https://example.com -t /path/to/your-template.yaml
+```
+
+### Connect Nuclei to ProjectDiscovery
+
+You can run the scans on your machine and upload the results to the cloud platform for further analysis and remediation.
+
+```sh
+nuclei -target https://example.com -dashboard
+```
+
+> [!NOTE]
+> This feature is absolutely free and does not require any subscription. For a detailed guide, refer to the [documentation](https://docs.projectdiscovery.io/cloud/scanning/nuclei-scan?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
+
+## Nuclei Templates, Community and Rewards 💎
+[Nuclei templates](https://github.com/projectdiscovery/nuclei-templates) are based on the concepts of YAML based template files that define how the requests will be sent and processed. This allows easy extensibility capabilities to nuclei. The templates are written in YAML which specifies a simple human-readable format to quickly define the execution process.
+
+Try it online with our free AI powered Nuclei Templates Editor by [clicking here.](https://cloud.projectdiscovery.io/templates)
+
+Nuclei Templates offer a streamlined way to identify and communicate vulnerabilities, combining essential details like severity ratings and detection methods. This open-source, community-developed tool accelerates threat response and is widely recognized in the cybersecurity world. Nuclei templates are actively contributed by thousands of security researchers globally. We run two programs for our contributors: [Pioneers](https://projectdiscovery.io/pioneers) and [💎 bounties](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
+
+
+
+
+#### Examples
+
+Visit [our documentation](https://docs.projectdiscovery.io/templates/introduction) for use cases and ideas.
+
+| Use case | Nuclei template |
+| :----------------------------------- | :------------------------------------------------- |
+| Detect known CVEs | **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** |
+| Identify Out-of-Band vulnerabilities | **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** |
+| SQL Injection detection | **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** |
+| Cross-Site Scripting (XSS) | **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** |
+| Default or weak passwords | **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** |
+| Secret files or data exposure | **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** |
+| Identify open redirects | **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** |
+| Detect subdomain takeovers | **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** |
+| Security misconfigurations | **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** |
+| Weak SSL/TLS configurations | **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** |
+| Misconfigured cloud services | **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** |
+| Remote code execution vulnerabilities| **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** |
+| Directory traversal attacks | **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** |
+| File inclusion vulnerabilities | **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** |
+
-Do also check out the below similar open-source projects that may fit in your workflow:
+## Our Mission
-[FFuF](https://github.com/ffuf/ffuf), [Qsfuzz](https://github.com/ameenmaali/qsfuzz), [Inception](https://github.com/proabiral/inception), [Snallygaster](https://github.com/hannob/snallygaster), [Gofingerprint](https://github.com/Static-Flow/gofingerprint), [Sn1per](https://github.com/1N3/Sn1per/tree/master/templates), [Google tsunami](https://github.com/google/tsunami-security-scanner), [Jaeles](https://github.com/jaeles-project/jaeles), [ChopChop](https://github.com/michelin/ChopChop)
+Traditional vulnerability scanners were built decades ago. They are closed-source, incredibly slow, and vendor-driven. Today's attackers are mass exploiting newly released CVEs across the internet within days, unlike the years it used to take. This shift requires a completely different approach to tackling trending exploits on the internet.
+
+We built Nuclei to solve this challenge. We made the entire scanning engine framework open and customizable—allowing the global security community to collaborate and tackle the trending attack vectors and vulnerabilities on the internet. Nuclei is now used and contributed by Fortune 500 enterprises, government agencies, universities.
+
+You can participate by contributing to our code, [templates library](https://github.com/projectdiscovery/nuclei-templates), or [joining our team.](https://projectdiscovery.io/)
+
+## Contributors :heart:
+
+Thanks to all the amazing [community contributors for sending PRs](https://github.com/projectdiscovery/nuclei/graphs/contributors) and keeping this project updated. :heart:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-### License
+## License
-Nuclei is distributed under [MIT License](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md)
+Nuclei is distributed under [MIT License](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md).
-
-
-
+
diff --git a/static/nuclei-cover-image.png b/static/nuclei-cover-image.png
new file mode 100644
index 0000000000000000000000000000000000000000..caab6fb2d4ec9d5f02b30220f62d5bb3e3bfd9ce
GIT binary patch
literal 818033
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%0D#AE>=vtKon^k@zkDU%X*9QAxpYOEnOGi!
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