mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 12:21:25 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e80959022 | ||
|
|
17e6a46cba | ||
|
|
a03e867be0 | ||
|
|
38133c1d7b | ||
|
|
c53042cfb3 | ||
|
|
b7fde879cf | ||
|
|
1e5dada8d5 | ||
|
|
8b3dbfd1c9 | ||
|
|
15b7ee0d6c | ||
|
|
f28691ad59 | ||
|
|
54e6279130 |
No files matched your search
@@ -17,4 +17,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/[email protected]2.0
|
||||
- uses: crate-ci/[email protected]1.1
|
||||
@@ -36,7 +36,7 @@ It also specifies a `WorkflowLoader` interface that is used during workflow load
|
||||
|
||||
```go
|
||||
type WorkflowLoader interface {
|
||||
GetTemplatePathsByTags(tags []string, noValidate bool) []string
|
||||
GetTemplatePathsByTags(tags []string) []string
|
||||
GetTemplatePaths(templatesList []string, noValidate bool) []string
|
||||
}
|
||||
```
|
||||
|
||||
@@ -145,12 +145,12 @@ TARGET:
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6); default uses dialer fallback, -sa scans all addresses
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -166,7 +166,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -178,7 +177,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,10 +219,8 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-ce, -csv-export string file to export results in CSV format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -258,7 +255,6 @@ CONFIGURATIONS:
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-mrbi, -max-request-body-inflight int max request body bytes held by in-flight http requests; requests with bodies of 1MB or more queue past it (0 disables)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -330,7 +326,6 @@ OPTIMIZATIONS:
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
-ac, -auto-calibrate drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)
|
||||
|
||||
HEADLESS:
|
||||
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -165,7 +165,7 @@ TARGET:
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -181,7 +181,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -193,7 +192,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -237,7 +236,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+2
-4
@@ -148,7 +148,7 @@ TARGET:
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
@@ -164,7 +164,6 @@ TEMPLATES:
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-plan string write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
@@ -176,7 +175,7 @@ TEMPLATES:
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
@@ -220,7 +219,6 @@ OUTPUT:
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
|
||||
+231
-104
@@ -1,128 +1,255 @@
|
||||
<!-- Generated by securitycontext.dev. Source HEAD: 40d08ee493f6f84c9a6b3c7f15f22da83853130c. -->
|
||||
<!-- Generated by securitycontext.dev. Source HEAD: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1. -->
|
||||
# Security Context: projectdiscovery/nuclei @ dev
|
||||
|
||||
> Reference context for editing projectdiscovery/nuclei. Before changing any component below, **preserve the listed guards and do not reintroduce these fixed issues**. Each fingerprint is a regression check, and each shared attack surface is a guard that must keep holding on every path.
|
||||
|
||||
_Analyzed [40d08ee493][c-40d08ee493] on `dev` · 5242 commits, 586 security-relevant examined · generated 2026-11-09_
|
||||
_Generated 2026-09-01 16:22 UTC by secc_
|
||||
|
||||
_Analyzed commit: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1_
|
||||
|
||||
**Remediation coverage: 56.7%**
|
||||
_17 of 30 historical fixes have no remaining unguarded variant; 368 open remediation leads to verify. Measures fix consistency, not a vulnerability verdict._
|
||||
|
||||
- Commits analyzed: **4814**
|
||||
- Commits flagged for review: **235**
|
||||
- Confirmed historical security fixes: **32**
|
||||
- Known published CVEs: **0**
|
||||
|
||||
## Summary
|
||||
|
||||
Nuclei's historical security posture is dominated by sandbox escapes and code execution via unsigned or unverified templates, with recurring SSRF, path traversal, and auth bypass issues across JS libraries, loaders, and workflow paths. The most critical theme is inconsistent enforcement of signature verification and local-file-access (-lfa) guards across direct, workflow, DAST, and JS-sandbox entry points, enabling variant attacks. A second theme is network policy (host allowlist) gaps in LDAP dialing and proxy handling. The security context also includes dependency upgrades (Alpine, go-git) and output file permission hardening, but the highest-value focus is on template trust boundaries and sandbox enforcement parity.
|
||||
|
||||
## Recurring Security Patterns
|
||||
|
||||
**12.5% of classified fixes repeat a previously fixed pattern** (2 recurring, 14 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
|
||||
**15.6% of classified fixes repeat a previously fixed pattern** (5 recurring, 27 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
|
||||
|
||||
- **Code Execution:** [1c440e755a](https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645) → [9de96e4dda](https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76) — matched on `class:code execution`, `component:pkg/catalog/loader/loader.go`
|
||||
- **Code Execution via Unsigned Template:** [fa56800fcc](https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de) → [fac0b7196d](https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37) — matched on `class:code execution via unsigned template`, `CWE-347`, `sink:loadtemplates`
|
||||
- **SSRF:** [a167e6c57b](https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770) → [9def9af7e3](https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572) — matched on `class:ssrf`, `CWE-918`, `component:pkg/js/libs/ldap/ldap.go`
|
||||
- **Dependency Vulnerability:** [d93b4a01df](https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe) → [fcd5c6b111](https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529) — matched on `class:dependency vulnerability`, `CWE-1104`, `component:dockerfile`
|
||||
- **Sandbox Escape:** [96049a32fa](https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a) → [cf2a98b03d](https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91) — matched on `class:sandbox escape`, `CWE-284`, `component:v2/internal/runner/processor.go`, `sink:stdlib allmodulenames`, `sink:stdlib getmodulemap`
|
||||
|
||||
## Top Risks
|
||||
|
||||
Components with the strongest concentration of past fixes for one class.
|
||||
### 1. Sandbox Escape: `pkg/js/compiler/pool.go, v2/pkg/protocols/javascript/js.go, v2/pkg/protocols/headless/engine/engine.go, v2/internal/runner/processor.go` (high)
|
||||
|
||||
| # | Class | Component | Severity | Fixes | Evidence |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Code Execution | `pkg/fuzz/execute.go` | high | 2 | [4fed40f75a][c-4fed40f75a], [ccbfb12bd0][c-ccbfb12bd0] |
|
||||
| 2 | SSRF | `pkg/protocols/common/protocolstate/headless.go` | high | 2 | [d43c1f547f][c-d43c1f547f], [d9dabbb4a1][c-d9dabbb4a1] |
|
||||
| 3 | Arbitrary File Access | `v2/pkg/protocols/common/generators/load.go` | high | 0 (1 component gone, not counted) | [e5154d362a][c-e5154d362a] |
|
||||
| 4 | Arbitrary File Access | `pkg/js/libs/mysql/mysql.go` | high | 1 | [634cecbb28][c-634cecbb28] |
|
||||
| 5 | Certificate Validation Bypass | `pkg/reporting/exporters/es/elasticsearch.go` | high | 1 | [82d5a8acc2][c-82d5a8acc2] |
|
||||
| 6 | Code Execution | `pkg/protocols/common/expressions/expressions.go` | high | 1 | [d221732016][c-d221732016] |
|
||||
Multiple sandbox escapes across JS require(), eval/ReadFile, headless browser flags, and tengo imports, indicating weak default sandbox boundaries that require consistent -lfa and safe-module enforcement.
|
||||
|
||||
## Fixed-Issue Fingerprints
|
||||
Evidence: 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, df55f7a2eb591a7949b5c66a9c54db54aa28f450, cf2a98b03d76327c20fb0183b7f1c849eb692c91, 96049a32fa70aef0483d4180d386a57b3c2d973a
|
||||
|
||||
Real security fixes mined from git history; use them as regression checks. `verified` fixes are linked to a published advisory, `inferred` fixes were confirmed from the diff evidence alone. Each is identified by its short commit SHA, which the variant-leads report uses to reference it back to this history.
|
||||
### 2. Code Execution via Unsigned Template: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go, pkg/templates/templates.go` (critical)
|
||||
|
||||
**By component** (directories with the most fixes):
|
||||
Unsigned templates can execute code through direct loading, workflows, and DAST branches; signature verification was bypassed via cache poisoning and missing guards on specific paths.
|
||||
|
||||
- `pkg/fuzz` — 2 fixes: Code Execution (2)
|
||||
- `pkg/protocols/common/protocolstate` — 2 fixes: SSRF (2)
|
||||
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de
|
||||
|
||||
- [4fed40f75a][c-4fed40f75a] **fix(fuzz): stop re-rendering evaluated template variables (#8057)** — Code Execution, high [CWE-94] · inferred · 2026-11-05
|
||||
`pkg/fuzz/execute.go` · sink `rule.evaluateVarsWithInteractsh` · fix: safe parser configuration
|
||||
- [82d5a8acc2][c-82d5a8acc2] **exporters: verify TLS certificates (#8042)** — Certificate Validation Bypass, high [CWE-295] · inferred · 2026-11-04
|
||||
`pkg/reporting/exporters/es/elasticsearch.go` · sink `DialTLS` · fix: cryptographic verification
|
||||
- [1c0f77d28e][c-1c0f77d28e] **fix(js/net): enforce network policy on proxied target in JS net dial** — SSRF, high [CWE-918] · inferred · 2026-10-18
|
||||
`pkg/js/libs/net/net.go` · sink `dialHTTPProxy` · fix: input validation
|
||||
- [d43c1f547f][c-d43c1f547f] **fix(headless): canonicalize IPv4-mapped IPv6 literals and re-validate dial targets** — SSRF, high [CWE-918] · inferred · 2026-10-13
|
||||
`pkg/protocols/common/protocolstate/headless.go` · sink `net.DialTimeout` · fix: canonicalization before validation
|
||||
- [d9dabbb4a1][c-d9dabbb4a1] **fix(headless): enforce network policy on ws and wss schemes (#7867)** — SSRF, high [CWE-918] · inferred · 2026-10-12
|
||||
`pkg/protocols/common/protocolstate/headless.go` · sink `isValidHost` · fix: canonicalization before validation
|
||||
- [a20aa26024][c-a20aa26024] **Fix pprof/auth exposure on http-api-endpoint and harden file permissions** — Information Disclosure, high [CWE-200] · inferred · 2026-08-03
|
||||
`internal/httpapi/apiendpoint.go` · sink `http.ListenAndServe` · fix: authentication verification
|
||||
- [fac0b7196d][c-fac0b7196d] **feat(templates): require signatures for javascript templates (#7514)** — Signature Verification Bypass, high [CWE-347] · verified (CVE-2026-76819, GHSA-vxg7-f2jj-jmqm) · 2026-07-03
|
||||
`pkg/catalog/loader/loader.go` · sink `t.HasJavascriptRequest` · fix: cryptographic verification
|
||||
- [ccbfb12bd0][c-ccbfb12bd0] **refactor(protocols): template rendering boundary (#7499)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-06-30
|
||||
`pkg/fuzz/execute.go` · sink `expressions.Evaluate` · fix: sandbox isolation
|
||||
- [3a8b65cb45][c-3a8b65cb45] **fix(js): reject unsandboxed ccache writes in krbforge (#7482)** — Sandbox Escape, high [CWE-693] · inferred · 2026-06-24
|
||||
`pkg/js/libs/krbforge/krbforge.go` · sink `OutputFile` · fix: sandbox isolation
|
||||
- [68daef44a7][c-68daef44a7] **fix(js): escape dbname in lib/pq URL (#7479)** — SQL Injection, high [CWE-89] · inferred · 2026-06-23
|
||||
`pkg/js/libs/postgres/postgres.go` · sink `fmt.Sprintf` (library call — the operation around it is the sink) · fix: context-aware output escaping
|
||||
- [634cecbb28][c-634cecbb28] **gate mysql allowAllFiles behind -lfa** — Arbitrary File Access, high [CWE-22] · verified (CVE-2026-76803, GHSA-xhmx-w2j4-rw3q) · 2026-06-19
|
||||
`pkg/js/libs/mysql/mysql.go` · sink `sql.Open` · fix: sandbox isolation
|
||||
- [20cbf0bd73][c-20cbf0bd73] **refactor: use path-aware filesystem containment checks (#7420)** — Path Traversal, high [CWE-22] · inferred · 2026-06-04
|
||||
`pkg/installer/template.go` · sink `filepath.Join` · fix: canonicalization before validation
|
||||
- [6f2ade6a9b][c-6f2ade6a9b] **fix(js): respect \`allow-local-file-access\` in \`require\` (#7332)** — Sandbox Escape, high [CWE-693] · inferred · 2026-04-10
|
||||
`pkg/js/compiler/pool.go` · sink `require.DefaultSourceLoader` · fix: sandbox isolation
|
||||
- [d221732016][c-d221732016] **fix(expressions): only eval template-authored expressions (#7221)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-03-16
|
||||
`pkg/protocols/common/expressions/expressions.go` · sink `FindExpressions` · fix: allowlist enforcement
|
||||
- [0da993afe6][c-0da993afe6] **Merge commit from fork** — Signature Verification Bypass, medium [CWE-347] · verified (CVE-2024-43405, GHSA-7h5p-mmpp-hgmm) · 2024-08-19
|
||||
`pkg/templates/signer/tmpl_signer.go` · sink `os.ReadFile` · fix: cryptographic verification
|
||||
- [e5154d362a][c-e5154d362a] **fixing payload load (#3927)** — Arbitrary File Access, high [CWE-22] · verified (CVE-2023-37896, GHSA-2xx4-jj5v-6mff) · 2023-07-14
|
||||
`v2/pkg/protocols/common/generators/load.go` · sink `loadPayloads` · fix: sandbox isolation
|
||||
### 3. Auth Bypass: `pkg/templates/compile.go, pkg/templates/signer/tmpl_signer.go, pkg/catalog/loader/loader.go, pkg/protocols/http/request.go` (high)
|
||||
|
||||
Signature verification can be bypassed via cached metadata, improper signature extraction, or missing checks on specific load paths, and cookie jar bypass via disabled cookie handling.
|
||||
|
||||
Evidence: 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b, a976ca8d3f6a18c9ca746c8ce73f28fdcd518642
|
||||
|
||||
### 4. SSRF: `pkg/js/libs/ldap/ldap.go, v2/internal/runner/proxy.go` (high)
|
||||
|
||||
SSRF via LDAP dialing to internal hosts and via proxy configuration; host allowlist checks were missing or applied to the wrong target (domain vs controller).
|
||||
|
||||
Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f
|
||||
|
||||
### 5. Path Traversal: `pkg/catalog/config/nucleiconfig.go, v2/internal/runner/update.go, v2/pkg/protocols/common/protocolstate/headless.go` (high)
|
||||
|
||||
Path traversal via lexical prefix checks, zip-slip in template extraction, and file:// scheme in headless mode; canonicalization and containment checks are required.
|
||||
|
||||
Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, d3928e080d819f25306bfbaecc399f03516db5aa
|
||||
|
||||
### 6. Code Execution: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go` (high)
|
||||
|
||||
Code execution via cache poisoning and unverified code templates in workflows; requires -code flag and verified templates.
|
||||
|
||||
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, e86f38299765b82ad724fdb701557e0eaff3884d
|
||||
|
||||
## Shared Attack Surfaces
|
||||
|
||||
Capabilities guarded by more than one past fix on different code paths. A guard enforced on one entry point is often missing on a sibling; verify each alternate path before assuming it's covered.
|
||||
|
||||
### 1. Template signature verification and unsigned-template opt-in enforcement across all load paths
|
||||
|
||||
- Guard that should hold everywhere: signature verification (template.Verified, Verify), DisableUnsignedTemplates flag, -code flag for code templates, cache invalidation on verification status/fingerprint/digest
|
||||
- Entry points: `pkg/catalog/loader/loader.go (LoadTemplatesWithTags, DAST branch)`, `pkg/templates/workflows.go (loadTemplates, workflowExecutor)`, `pkg/templates/templates.go (loadTemplates)`, `pkg/templates/compile.go (template.Verified)`, `pkg/templates/signer/tmpl_signer.go (Verify)`
|
||||
- Check: Inspect the DAST and workflow sub-loaders: they must enforce the same DisableUnsignedTemplates and -code checks as the direct loader, and must not rely on cached verification results without re-checking signatures.
|
||||
- Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de, 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b
|
||||
|
||||
### 2. Local file access (read/write) outside the sandbox, gated by -lfa flag
|
||||
|
||||
- Guard that should hold everywhere: -lfa flag enforcement, path canonicalization and containment (sandbox directory), file:// scheme blocking, allowAllFiles stripping, require() registry rebuild
|
||||
- Entry points: `pkg/js/libs/krbforge/krbforge.go (CreateTicket)`, `pkg/js/libs/mysql/mysql_private.go (sql.Open)`, `pkg/js/compiler/pool.go (require)`, `v2/pkg/protocols/javascript/js.go (ReadFile, eval)`, `v2/pkg/protocols/common/protocolstate/headless.go (file:)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
|
||||
- Check: Verify that every JS library and protocol helper (krbforge, mysql, require, headless file://, payload loaders) enforces the -lfa flag and path containment; a sibling path that skips the -lfa check is a sandbox escape.
|
||||
- Evidence: 3a8b65cb45f121fcdf418f60e5177ed9f250c122, 9611926a9a1ca99e8ed554d1b26fbeff60e06368, 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, d3928e080d819f25306bfbaecc399f03516db5aa, e7fb40a41308f6c41267e858d1781eb67f7e0572
|
||||
|
||||
### 3. Network policy (host allowlist) enforcement before outbound connections
|
||||
|
||||
- Guard that should hold everywhere: IsHostAllowed check on the actual target host (controller, not domain), proxy validation and connectivity check, ProxyInternal gate for HTTP_PROXY env
|
||||
- Entry points: `pkg/js/libs/ldap/ldap.go (Dial, newLdapSession)`, `v2/internal/runner/proxy.go (loadProxyServers, os.Setenv HTTP_PROXY)`
|
||||
- Check: Inspect other dialing paths (e.g., HTTP, headless) to ensure they apply the same host allowlist check on the resolved IP/host, not just a user-supplied domain, and that proxy settings cannot redirect traffic to internal hosts.
|
||||
- Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f, 3f5c055aca1499642caf4f40e8cf55ad32bd4da9
|
||||
|
||||
### 4. Path traversal in file extraction and template path trust boundaries
|
||||
|
||||
- Guard that should hold everywhere: canonical path containment check (filepath.Rel, prefix), zip-slip validation against template directory, sandbox path prefix validation
|
||||
- Entry points: `pkg/catalog/config/nucleiconfig.go (IsCustomTemplate)`, `v2/internal/runner/update.go (os.OpenFile)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
|
||||
- Check: Audit all file-write and file-read paths that accept template-controlled paths (e.g., template extraction, payload loading) to ensure they use canonical containment, not lexical prefix checks, and apply the same sandbox directory restriction.
|
||||
- Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, e7fb40a41308f6c41267e858d1781eb67f7e0572
|
||||
|
||||
## Historical Vulnerability Classes
|
||||
|
||||
| Class | Fixes | Verified | Most recent fix |
|
||||
|---|---|---|---|
|
||||
| Code Execution | 3 | 2 | [2026-11-05][c-4fed40f75a] 4fed40f75a |
|
||||
| SSRF | 3 | 0 | [2026-10-18][c-1c0f77d28e] 1c0f77d28e |
|
||||
| Arbitrary File Access | 1 (1 component gone, not counted) | 2 | [2026-06-19][c-634cecbb28] 634cecbb28 |
|
||||
| Sandbox Escape | 2 | 0 | [2026-06-24][c-3a8b65cb45] 3a8b65cb45 |
|
||||
| Signature Verification Bypass | 2 | 2 | [2026-07-03][c-fac0b7196d] fac0b7196d |
|
||||
| Certificate Validation Bypass | 1 | 0 | [2026-11-04][c-82d5a8acc2] 82d5a8acc2 |
|
||||
| Information Disclosure | 1 | 0 | [2026-08-03][c-a20aa26024] a20aa26024 |
|
||||
| Path Traversal | 1 | 0 | [2026-06-04][c-20cbf0bd73] 20cbf0bd73 |
|
||||
| SQL Injection | 1 | 0 | [2026-06-23][c-68daef44a7] 68daef44a7 |
|
||||
| Class | Count |
|
||||
|---|---|
|
||||
| Sandbox Escape | 7 |
|
||||
| Auth Bypass | 4 |
|
||||
| Code Execution | 3 |
|
||||
| Dependency Vulnerability | 3 |
|
||||
| Path Traversal | 3 |
|
||||
| SSRF | 3 |
|
||||
| Code Execution via Unsigned Template | 2 |
|
||||
| Arbitrary File Read | 1 |
|
||||
| Cryptographic Weakness | 1 |
|
||||
| Deserialization of Untrusted Data | 1 |
|
||||
| Information Disclosure | 1 |
|
||||
| Insecure File Permissions | 1 |
|
||||
| Race Condition | 1 |
|
||||
| Security Misconfiguration | 1 |
|
||||
|
||||
## Known Advisories
|
||||
## Fixed-Issue Fingerprints
|
||||
|
||||
### Linked to a fix commit in this history (5)
|
||||
These are real security fixes mined from git history. Use them as regression checks.
|
||||
|
||||
- **CVE-2026-76805** / GHSA-jpvm-9frm-hjcq (medium 5.3) CWE-200 — fixed in [ccbfb12bd0][c-ccbfb12bd0], [d221732016][c-d221732016]
|
||||
- Environment Variable Disclosure via Response-Derived Data in DAST/Fuzz Mode. A vulnerability in Nuclei's DAST/fuzz expression evaluation path allows a malicious target server to trigger disclosure of ...
|
||||
- **CVE-2026-76803** / GHSA-xhmx-w2j4-rw3q (medium 5.3) CWE-284 — fixed in [634cecbb28][c-634cecbb28]
|
||||
- Local File Read via MySQL Client Sandbox Bypass. A vulnerability in Nuclei's JavaScript MySQL client library allows arbitrary local file reads that bypass the `-allow-local-file-access` (`-lfa`) sandb...
|
||||
- **CVE-2026-76819** / GHSA-vxg7-f2jj-jmqm (high 8.6) CWE-94,CWE-787 — fixed in [fac0b7196d][c-fac0b7196d]
|
||||
- Arbitrary Code Execution via Goja JavaScript Engine Vulnerability. A vulnerability in the Goja JavaScript engine used by Nuclei's `javascript:` protocol allows arbitrary native code execution on the s...
|
||||
- **CVE-2024-43405** / GHSA-7h5p-mmpp-hgmm (high 7.4) CWE-78 — fixed in [0da993afe6][c-0da993afe6]
|
||||
- Nuclei Template Signature Verification Bypass. ## Summary A vulnerability has been identified in Nuclei's template signature verification system that could allow an attacker to bypass the signature ch...
|
||||
- **CVE-2023-37896** / GHSA-2xx4-jj5v-6mff (medium 5.9) CWE-22 — fixed in [e5154d362a][c-e5154d362a]
|
||||
- Nuclei < 2.9.9 - Sandbox restriction bypass. ## Overview Affected: < 2.9.9 (patched 2.9.9).
|
||||
|
||||
### Without a linked fix commit (6)
|
||||
|
||||
Published elsewhere, fixed before the history window, or not yet linked; full descriptions are in the JSON context.
|
||||
|
||||
| Advisory | Severity | Published | CWE | Summary |
|
||||
|---|---|---|---|---|
|
||||
| CVE-2026-76804 / GHSA-qgw5-7j4f-fg97 | medium 5.5 | 2026-07-16 | CWE-284 | Local File Read via Workflow File-Protocol Gate Bypass. A vulnerability in Nuclei's workfl... |
|
||||
| CVE-2026-76802 / GHSA-jpf4-98qj-qr67 | medium 4.7 | 2026-07-16 | CWE-78 | Arbitrary Command Execution via DAST Code Signature Bypass. A vulnerability in Nuclei's DA... |
|
||||
| CVE-2026-41645 / GHSA-jm34-66cf-qpvr | medium 5.3 | 2026-04-18 | CWE-94 | Environment variable disclosure via Response-Derived DSL Expressions. A vulnerability in N... |
|
||||
| CVE-2026-41646 / GHSA-29rg-wmcw-hpf4 | medium 5.5 | 2026-04-18 | CWE-284 | Local File Read via require() Module Loader Bypass. A vulnerability in Nuclei's JavaScript... |
|
||||
| CVE-2024-40641 / GHSA-c3q9-c27p-cw9h | high 7.4 | 2024-07-17 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Summary... |
|
||||
| CVE-2024-27920 / GHSA-w5wx-6g2r-r78q | high 7.4 | 2024-03-15 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Overvie... |
|
||||
|
||||
[c-40d08ee493]: https://github.com/projectdiscovery/nuclei/commit/40d08ee493f6f84c9a6b3c7f15f22da83853130c
|
||||
[c-4fed40f75a]: https://github.com/projectdiscovery/nuclei/commit/4fed40f75a57c7c414aac62d213d772cc3f430d3
|
||||
[c-ccbfb12bd0]: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6
|
||||
[c-d43c1f547f]: https://github.com/projectdiscovery/nuclei/commit/d43c1f547f98ac56654f4335ad20af0cca4c229c
|
||||
[c-d9dabbb4a1]: https://github.com/projectdiscovery/nuclei/commit/d9dabbb4a19e897a6bc12334a030a43035b27b73
|
||||
[c-e5154d362a]: https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648
|
||||
[c-634cecbb28]: https://github.com/projectdiscovery/nuclei/commit/634cecbb289db494247c04a9c4ec6a113aef3cc0
|
||||
[c-82d5a8acc2]: https://github.com/projectdiscovery/nuclei/commit/82d5a8acc2c756394c7fa92eae08b41330e89d69
|
||||
[c-d221732016]: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3
|
||||
[c-1c0f77d28e]: https://github.com/projectdiscovery/nuclei/commit/1c0f77d28ed639a34c2036fa23c7503ffac0bf27
|
||||
[c-a20aa26024]: https://github.com/projectdiscovery/nuclei/commit/a20aa260245e5db889c5a421d4a65aeb2a64bf46
|
||||
[c-fac0b7196d]: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37
|
||||
[c-3a8b65cb45]: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122
|
||||
[c-68daef44a7]: https://github.com/projectdiscovery/nuclei/commit/68daef44a7eafd41c39690924620e418762909ef
|
||||
[c-20cbf0bd73]: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206
|
||||
[c-6f2ade6a9b]: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d
|
||||
[c-0da993afe6]: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a
|
||||
- **Auth Bypass** (high) [CWE-287]: Honors disable-cookie by preventing the shared cookie jar from being applied when cookie handling is disabled, closing a path where cookies could be reused across requests.
|
||||
- sink: `SetCookieJar(input.CookieJar)`
|
||||
- fix: Adds !request.DisableCookie check before applying shared cookie jar
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/a976ca8d3f6a18c9ca746c8ce73f28fdcd518642 (2026-08-25)
|
||||
- **Code Execution** (high) [CWE-347]: This commit fixes a cache poisoning vulnerability where unsigned templates could bypass signature verification and execute malicious code during compilation or request execution.
|
||||
- sink: `template compilation and execution of JavaScript`
|
||||
- fix: cache invalidation based on verification status, verifier fingerprint, and content digest
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76 (2026-08-04)
|
||||
- **Code Execution via Unsigned Template** (high) [CWE-347]: Enforces signature verification on JavaScript templates, preventing execution of unsigned templates that could lead to arbitrary code execution.
|
||||
- sink: `loading unsigned javascript templates into execution paths`
|
||||
- fix: Require signature verification for javascript templates in direct loading and workflows when signing is enforced
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37 (2026-07-03)
|
||||
- **Sandbox Escape** (high) [CWE-22]: Enforces nuclei's local file access policy on krbforge output paths, preventing arbitrary file writes outside the sandbox.
|
||||
- sink: `gpkrb.CreateTicket(cfg) with cfg.OutputFile`
|
||||
- fix: Path normalization into sandbox and denial of outside paths unless -lfa
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122 (2026-06-24)
|
||||
- **SSRF** (high) [CWE-918]: Adds network policy host allowlist checks before LDAP dialing, preventing SSRF to restricted internal hosts.
|
||||
- sink: `dialers.Fastdialer.Dial`
|
||||
- fix: host allowlist check via IsHostAllowed before dialing
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572 (2026-06-24)
|
||||
- **Arbitrary File Read** (high) [CWE-22]: Gates MySQL's allowAllFiles option behind the -lfa flag, preventing arbitrary local file reads via LOAD DATA LOCAL INFILE.
|
||||
- sink: `sql.Open("mysql", dsn) with AllowAllFiles=true`
|
||||
- fix: DSN sandbox that strips allowAllFiles unless -lfa is set
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/9611926a9a1ca99e8ed554d1b26fbeff60e06368 (2026-06-19)
|
||||
- **Code Execution** (critical) [CWE-94]: Moves the unsigned code-template guard ahead of the DAST branch, preventing unsigned code templates from being loaded and executed under DAST, which previously bypassed the signature check.
|
||||
- sink: `Template loading and execution`
|
||||
- fix: Hoisting unsigned code-template guard before DAST branch to ensure it applies on all load paths
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645 (2026-06-19)
|
||||
- **Path Traversal** (high) [CWE-22]: This commit replaces lexical prefix checks with canonical path containment checks across multiple filesystem trust boundaries, preventing path traversal where sibling paths were incorrectly treated as trusted.
|
||||
- sink: `IsCustomTemplate`
|
||||
- fix: path canonicalization and containment check
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206 (2026-06-04)
|
||||
- **Information Disclosure** (medium) [CWE-200]: This commit fixes a data leakage issue in workflows where sibling state could leak across concurrent executions, and race requests ignored dynamic values, by syncing context and cloning inputs.
|
||||
- sink: `syncWorkflowInputToTemplateCtx`
|
||||
- fix: context synchronization and input cloning
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/805bcfe76f3629dbe71021795d4ce865406c1555 (2026-05-08)
|
||||
- **Sandbox Escape** (high) [CWE-284]: Fixes sandbox escape where require() bypassed local file access restrictions and reused privileged module cache across executions.
|
||||
- sink: `require() using default host filesystem loader`
|
||||
- fix: Rebuild require registry per execution and enforce allow-local-file-access sandbox rules
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d (2026-04-10)
|
||||
- **Cryptographic Weakness** (medium) [CWE-338]: Replaces predictable math/rand PRNG with cryptographically secure crypto/rand in JS helper functions.
|
||||
- sink: `rand.Intn(255) / rand.Int63()`
|
||||
- fix: Replace predictable math/rand with crypto/rand
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/44ef9ce34ffcfdfca3a96626abc73d99dd0e83ab (2026-03-15)
|
||||
- **Auth Bypass** (high) [CWE-347]: Caching signature verification results without cryptographic re-check allows a metadata index compromise to bypass template signature verification.
|
||||
- sink: `template.Verified, template.TemplateVerifier`
|
||||
- fix: cached verification data reuse
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4 (2026-01-21)
|
||||
- **Race Condition** (medium) [CWE-362]: Fixes a race condition in workflow execution caused by shared context callbacks, which could lead to data corruption or crashes; the primary impact is reliability, not a direct security breach.
|
||||
- sink: `shared context callbacks in workflow execution`
|
||||
- fix: fix race condition by making workflow execution sequential
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/24d1f58682027e23cb21a299a170abd2bdf20dc2 (2025-12-05)
|
||||
- **Auth Bypass** (high) [CWE-347]: Fixes template signature verification by centralizing signature extraction and requiring the signature to be at the end of the template, preventing bypass.
|
||||
- sink: `Verify`
|
||||
- fix: signature extraction and verification
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a (2024-08-19)
|
||||
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.5 to 3.18.6 to fix known security vulnerabilities in bundled packages.
|
||||
- sink: `FROM alpine:3.18.5`
|
||||
- fix: Upgrade base image to patched version
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529 (2024-03-15)
|
||||
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.2 to 3.18.5 to fix known security vulnerabilities in bundled packages.
|
||||
- sink: `FROM alpine:3.18.2`
|
||||
- fix: Upgrade base image to patched version
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe (2024-03-15)
|
||||
- **Dependency Vulnerability** (high) [CWE-1035]: Updates go-git to fix known vulnerabilities (CVE-2023-49569, CVE-2023-49568), which could be exploited when processing untrusted git data.
|
||||
- sink: `go-git library usage`
|
||||
- fix: dependency upgrade to patched version
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/1700cdf4e23128a358066364062246b58c1c013f (2024-03-14)
|
||||
- **Code Execution via Unsigned Template** (high) [CWE-347]: Introduces a flag to disable unsigned templates, preventing execution of unverified templates that could lead to arbitrary code execution.
|
||||
- sink: `loading unsigned templates when -dut flag is set`
|
||||
- fix: Adds -disable-unsigned-templates flag to skip unsigned templates and report them separately
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de (2024-03-14)
|
||||
- **Auth Bypass** (medium) [CWE-347]: This commit introduces a flag to skip unsigned templates, closing a path where an attacker could execute untrusted templates by bypassing signature verification.
|
||||
- sink: `parsed.Verified`
|
||||
- fix: skip unverified templates when DisableUnsignedTemplates is set
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/9bd4db3f74aedd067af568df4f9198a8a090f68b (2024-03-10)
|
||||
- **Code Execution** (high) [CWE-94]: Prevents execution of unverified code templates in workflows, mitigating code execution risk.
|
||||
- sink: `template.RequestsCode`
|
||||
- fix: require -code flag and verified templates for code templates in workflows
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/e86f38299765b82ad724fdb701557e0eaff3884d (2024-03-01)
|
||||
- **SSRF** (high) [CWE-918]: Fixes SSRF by validating the actual LDAP controller host against the network policy instead of the domain name.
|
||||
- sink: `newLdapSession`
|
||||
- fix: host allowlist check on controller instead of domain
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770 (2024-01-20)
|
||||
- **Sandbox Escape** (high) [CWE-94]: Disables eval and tightens file read validation in the JavaScript sandbox, preventing template-driven code execution and file access outside the sandbox.
|
||||
- sink: `eval function and file read in sandbox mode`
|
||||
- fix: Disable eval function and rewrite file validation in sandbox mode
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/c35162c8ef061a96dd74b54c8f022c1a41cf5833 (2023-10-13)
|
||||
- **Path Traversal** (high) [CWE-22]: Adds network policy and blocks file:// protocol in headless mode unless -lfa is set, preventing local file disclosure.
|
||||
- sink: `file:// protocol access in headless browser`
|
||||
- fix: Network policy validation and file:// scheme blocking when local file access is disabled
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/d3928e080d819f25306bfbaecc399f03516db5aa (2023-08-25)
|
||||
- **Deserialization of Untrusted Data** (medium) [CWE-502]: Fixes JSON deserialization by adding explicit UnmarshalJSON methods and correcting tags, preventing type confusion and potential logic bypasses.
|
||||
- sink: `UnmarshalJSON on user-controlled fields like Type and Name`
|
||||
- fix: Adds proper UnmarshalJSON implementations and corrects json tags to prevent type confusion
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/85090b75317977bb9301444c9dfd8e4c2dba667a (2023-02-07)
|
||||
- **Sandbox Escape** (medium) [CWE-22]: The commit adds sandbox mode that restricts payload file paths to the template directory, preventing arbitrary file reads when sandbox is enabled.
|
||||
- sink: `loadPayloadsFromFile`
|
||||
- fix: path prefix validation against template directory and template path directory
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/e7fb40a41308f6c41267e858d1781eb67f7e0572 (2022-11-16)
|
||||
- **Insecure File Permissions** (low) [CWE-732]: Fixes overly permissive file permissions on debug output files by changing from os.ModePerm (0777) to 0644.
|
||||
- sink: `os.OpenFile`
|
||||
- fix: restrict file mode to 0644
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/af4854f90de728bb08689d5cae3dec4acfdb6b9a (2022-06-20)
|
||||
- **Security Misconfiguration** (medium) [CWE-16]: Prevents setting HTTP_PROXY environment variable unless explicitly enabled, avoiding potential proxy hijacking.
|
||||
- sink: `os.Setenv(types.HTTP_PROXY_ENV, proxyURL.String())`
|
||||
- fix: Added check for options.ProxyInternal before setting HTTP_PROXY environment variable
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/3f5c055aca1499642caf4f40e8cf55ad32bd4da9 (2022-03-09)
|
||||
- **Sandbox Escape** (high) [CWE-250]: The commit restricts the use of the --no-sandbox flag to Linux root users, preventing sandbox escape vulnerabilities in other environments.
|
||||
- sink: `NoSandbox(true)`
|
||||
- fix: Conditionally disable sandbox only on Linux as root
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/df55f7a2eb591a7949b5c66a9c54db54aa28f450 (2021-12-05)
|
||||
- **Path Traversal** (high) [CWE-22]: This commit fixes a zip-slip path traversal vulnerability by validating the extracted file path against the configured template directory.
|
||||
- sink: `os.OpenFile`
|
||||
- fix: path canonicalization and prefix check
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/f9c214a66f37a00fc29fe64232b690e6285ec59c (2021-11-25)
|
||||
- **SSRF** (high) [CWE-918]: Adds validation and connectivity checks for SOCKS5 proxies before launching a scan, preventing SSRF through proxy configuration.
|
||||
- sink: `Proxy connection validation and loading`
|
||||
- fix: Proxy server validation and connectivity check before scan launch
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/0e46d3e0410f89e4af41099f5cd38fa7f54ae54f (2021-11-10)
|
||||
- **Sandbox Escape** (high) [CWE-284]: This commit adds a sandbox flag that, when enabled, restricts the tengo scripting engine to safe modules, preventing sandbox escape and arbitrary command execution.
|
||||
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
|
||||
- fix: Added sandbox flag to restrict imports to safe modules when sandbox is enabled
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91 (2020-12-13)
|
||||
- **Sandbox Escape** (high) [CWE-284]: This commit restricts the tengo scripting engine to a safe module list, preventing sandbox escape and arbitrary command execution via workflow templates.
|
||||
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
|
||||
- fix: Restricted tengo script imports to a safe module list
|
||||
- commit: https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a (2020-12-13)
|
||||
|
||||
---
|
||||
_Fixed-issue fingerprints and advisory matches are derived from git history and published advisories; the risk tables and pattern descriptions above are derived from them and should be verified against the cited commits._
|
||||
_Fixed-issue fingerprints and CVE matches are derived from real data; LLM-written prose is advisory and should be verified against the cited commits._
|
||||
@@ -530,16 +530,6 @@ Description of the template.
|
||||
|
||||
You can go in-depth here on what the template actually does.
|
||||
|
||||
Description, impact and remediation may hold {{placeholders}} filled for
|
||||
each finding: named extractors, payloads, variables and DSL helpers
|
||||
(network helpers excluded), and the result fields host, port, ip, url,
|
||||
scheme, path, matched-at, matcher-name and extractor-name, which win over
|
||||
values of the same name. Use join(", ", name) for an extractor with
|
||||
several values, and brackets for a hyphenated name inside a helper:
|
||||
to_upper([matcher-name]). Unknown placeholders stay as written, and so
|
||||
does a field whose expression fails. Global matcher templates match on
|
||||
other templates' responses, so only the result fields fill their text.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -553,10 +543,6 @@ description: Bower is a package manager which stores package information in the
|
||||
description: Subversion ALM for the enterprise before 8.8.2 allows reflected XSS at multiple locations
|
||||
```
|
||||
|
||||
```yaml
|
||||
description: Redis {{version}} on {{host}}:{{port}} accepts the default password
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -573,8 +559,6 @@ Impact of the template.
|
||||
|
||||
You can go in-depth here on impact of the template.
|
||||
|
||||
Supports the same placeholders as description.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -681,8 +665,6 @@ Remediation steps for the template.
|
||||
|
||||
You can go in-depth here on how to mitigate the problem found by this template.
|
||||
|
||||
Supports the same placeholders as description.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -3775,8 +3757,6 @@ Part Definitions:
|
||||
- <code>req</code> - Headless request made from the client
|
||||
- <code>duration</code> - Latest measured operation duration in seconds
|
||||
- <code>resp,body,data</code> - Headless response received from client (default)
|
||||
- <code>header,all_headers</code> - Headless response headers
|
||||
- <code>status_code</code> - Status code received from the server
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ extend-exclude = [
|
||||
"vendor/",
|
||||
# Go checksum file
|
||||
"go.sum",
|
||||
# Generated history. Commit SHAs are not words.
|
||||
"SECURITY_CONTEXT.md",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const headerChildEnv = "NUCLEI_HEADER_CHILD_ARGS"
|
||||
|
||||
// -H values can contain "#", on the command line, in a header file and in the
|
||||
// config file, while lines of a header file that start with "#" are skipped as
|
||||
// comments.
|
||||
func TestHeaderValuesKeepHash(t *testing.T) {
|
||||
if args := os.Getenv(headerChildEnv); args != "" {
|
||||
options = &types.Options{Logger: gologger.DefaultLogger}
|
||||
os.Args = append([]string{os.Args[0]}, strings.Split(args, "\n")...)
|
||||
readConfig()
|
||||
fmt.Printf("headers=%s\n", strings.Join(options.CustomHeaders, "|"))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
headerFile := filepath.Join(root, "headers.txt")
|
||||
require.NoError(t, os.WriteFile(headerFile, []byte("# auth headers\nX-Token: abc#def\nReferer: https://app.example.com/#/dashboard\n"), 0o600))
|
||||
configFile := filepath.Join(root, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("header:\n - \"X-Config: cfg#value\"\n"), 0o600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{"cli and header file", []string{"-H", "Cookie: sid=1; color=#ff0000", "-H", headerFile}, "Cookie: sid=1; color=#ff0000|X-Token: abc#def|Referer: https://app.example.com/#/dashboard"},
|
||||
{"config file", []string{"-config", configFile}, "X-Config: cfg#value"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempRoot := filepath.Join(t.TempDir(), "tmp")
|
||||
require.NoError(t, os.Mkdir(tempRoot, 0o700))
|
||||
|
||||
args := append(tt.args, "-disable-update-check")
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestHeaderValuesKeepHash$", "-test.count=1")
|
||||
cmd.Env = isolatedNucleiStorageEnv(root, tempRoot, headerChildEnv+"="+strings.Join(args, "\n"))
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(output))
|
||||
require.Contains(t, string(output), "headers="+tt.want+"\n")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,7 @@
|
||||
# severity: low
|
||||
# # duplicate-issue-check (optional) flag to enable duplicate tracking issue check
|
||||
# duplicate-issue-check: false
|
||||
# # duplicate-issue-page-size (optional) sets the number of issues to request per page when searching for duplicates
|
||||
# # (GitLab returns at most 100 issues per page)
|
||||
# # duplicate-issue-page-size (optional) controls how many issues to fetch per page when searching for duplicates
|
||||
# duplicate-issue-page-size: 100
|
||||
# # duplicate-issue-max-pages (optional) limits how many pages to fetch when searching for duplicates (0 = no limit)
|
||||
# duplicate-issue-max-pages: 0
|
||||
|
||||
+4
-25
@@ -55,17 +55,6 @@ var (
|
||||
inlineSecretsTempMu sync.Mutex
|
||||
)
|
||||
|
||||
// normalizeHeaderValue trims a -H value and drops it when the whole line is a
|
||||
// "#" comment. The goflags default strips inline comments, which cut values
|
||||
// such as "Cookie: color=#fff" at the "#".
|
||||
func normalizeHeaderValue(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "#") {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
options.Logger = gologger.DefaultLogger
|
||||
|
||||
@@ -266,11 +255,6 @@ func readConfig() *goflags.FlagSet {
|
||||
var pdcpAuth string
|
||||
var fuzzFlag bool
|
||||
|
||||
// FileStringSliceOptions as set up by runner.ConfigureOptions, but keeping
|
||||
// "#" inside -H values
|
||||
headerSliceOptions := goflags.FileStringSliceOptions
|
||||
headerSliceOptions.Normalize = normalizeHeaderValue
|
||||
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.CaseSensitive = true
|
||||
flagSet.SetDescription(`Nuclei is a fast, template based vulnerability scanner focusing
|
||||
@@ -287,10 +271,10 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringSliceVarP(&options.Targets, "target", "u", nil, "target URLs/hosts to scan", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVarP(&options.TargetsFilePath, "list", "l", "", "path to file containing a list of target URLs/hosts to scan (one per line)"),
|
||||
flagSet.StringVarP(&options.InlineTargetsList, "targets-inline", "", "", "inline multiline target list (for use in template profiles)"),
|
||||
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileNormalizedOriginalStringSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
|
||||
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6); default uses dialer fallback, -sa scans all addresses", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
|
||||
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
|
||||
)
|
||||
@@ -313,7 +297,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringSliceVarP(&options.Workflows, "workflows", "w", nil, "list of workflow or workflow directory to run (comma-separated, file)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.WorkflowURLs, "workflow-url", "wurl", nil, "workflow url or list containing workflow urls to run (comma-separated, file)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVar(&options.Validate, "validate", false, "validate the passed templates to nuclei"),
|
||||
flagSet.StringVar(&options.PlanFile, "plan", "", "write the work a scan would do with its estimated cost to a jsonl file, then exit without sending requests to any target"),
|
||||
flagSet.BoolVarP(&options.NoStrictSyntax, "no-strict-syntax", "nss", false, "disable strict syntax check on templates"),
|
||||
flagSet.BoolVarP(&options.TemplateDisplay, "template-display", "td", false, "displays the templates content"),
|
||||
flagSet.BoolVar(&options.TemplateList, "tl", false, "list all templates matching current filters"),
|
||||
@@ -326,7 +309,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
@@ -370,10 +353,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVarP(&options.SarifExport, "sarif-export", "se", "", "file to export results in SARIF format"),
|
||||
flagSet.StringVarP(&options.JSONExport, "json-export", "je", "", "file to export results in JSON format"),
|
||||
flagSet.StringVarP(&options.JSONLExport, "jsonl-export", "jle", "", "file to export results in JSONL(ine) format"),
|
||||
flagSet.StringVarP(&options.CSVExport, "csv-export", "ce", "", "file to export results in CSV format"),
|
||||
flagSet.StringVarP(&options.PDFExport, "pdf-export", "pe", "", "file to export results in PDF format"),
|
||||
flagSet.StringSliceVarP(&options.Redact, "redact", "rd", nil, "redact given list of keys from query parameter, request header and body", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.NoRedact, "no-redact", "nrd", false, "disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("configs", "Configurations",
|
||||
@@ -385,7 +366,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.IntVarP(&options.MaxRedirects, "max-redirects", "mr", 10, "max number of redirects to follow for http templates"),
|
||||
flagSet.BoolVarP(&options.DisableRedirects, "disable-redirects", "dr", false, "disable redirects for http templates"),
|
||||
flagSet.StringVarP(&options.ReportingConfig, "report-config", "rc", "", "nuclei reporting module configuration file"), // TODO merge into the config file or rename to issue-tracking
|
||||
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", headerSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", goflags.FileStringSliceOptions),
|
||||
flagSet.RuntimeMapVarP(&options.Vars, "var", "V", nil, "custom vars in key=value format"),
|
||||
flagSet.StringVarP(&options.ResolversFile, "resolvers", "r", "", "file containing resolver list for nuclei"),
|
||||
flagSet.BoolVarP(&options.SystemResolvers, "system-resolvers", "sr", false, "use system DNS resolving as error fallback"),
|
||||
@@ -409,7 +390,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.IntVarP(&options.ResponseReadSize, "response-size-read", "rsr", 0, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.ResponseSaveSize, "response-size-save", "rss", unitutils.Mega, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.RequestSaveSize, "request-size-save", "rqss", 32*unitutils.Kilo, "max request body size to save in bytes (0 to save the whole body)"),
|
||||
flagSet.IntVarP(&options.MaxRequestBodyInFlight, "max-request-body-inflight", "mrbi", 0, "max request body bytes held by in-flight http requests; requests with bodies of 1MB or more queue past it (0 disables)"),
|
||||
flagSet.CallbackVar(func() { resetRequested = true }, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
|
||||
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
|
||||
@@ -490,7 +470,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVar(&options.PreflightPortScan, "preflight-portscan", false, "run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)"),
|
||||
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "disable stdin processing"),
|
||||
flagSet.BoolVar(&options.EnableHTTPCache, "http-cache", false, "enable HTTP cache (RFC 9111) for HTTP requests"),
|
||||
flagSet.BoolVarP(&options.AutoCalibrate, "auto-calibrate", "ac", false, "drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("headless", "Headless",
|
||||
|
||||
@@ -15,14 +15,13 @@ require (
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
|
||||
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
github.com/alitto/pond v1.9.2
|
||||
github.com/andybalholm/brotli v1.2.7
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.7
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/antchfx/xpath v1.3.9
|
||||
github.com/antchfx/xpath v1.3.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20
|
||||
@@ -34,11 +33,10 @@ require (
|
||||
github.com/clbanning/mxj/v2 v2.7.0
|
||||
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/elazarl/goproxy v1.7.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/getkin/kin-openapi v0.149.0
|
||||
github.com/getkin/kin-openapi v0.144.0
|
||||
github.com/getsops/sops/v3 v3.9.2
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
@@ -63,7 +61,6 @@ require (
|
||||
github.com/jhump/protoreflect v1.17.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kitabisa/go-ci v1.0.3
|
||||
github.com/klauspost/compress v1.20.2
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/logrusorgru/aurora/v4 v4.0.0
|
||||
@@ -74,12 +71,11 @@ require (
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/oiweiwei/go-msrpc v1.2.12
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/openrdap/rdap v0.9.1
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.9.0
|
||||
github.com/projectdiscovery/dsl v0.8.24
|
||||
github.com/projectdiscovery/fastdialer v0.6.0
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
@@ -89,7 +85,7 @@ require (
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
|
||||
github.com/projectdiscovery/gologger v1.1.73
|
||||
github.com/projectdiscovery/gostruct v0.0.2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/httpx v1.12.0
|
||||
@@ -106,8 +102,8 @@ require (
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.6
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
@@ -117,7 +113,6 @@ require (
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/spaolacci/murmur3 v1.1.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
@@ -129,14 +124,14 @@ require (
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/net v0.62.0
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/term v0.47.0
|
||||
golang.org/x/text v0.43.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.51.0
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -176,9 +171,11 @@ require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/akrylysov/pogreb v0.10.2 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
@@ -237,10 +234,12 @@ require (
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
@@ -263,7 +262,7 @@ require (
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
@@ -324,6 +323,7 @@ require (
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
|
||||
github.com/kataras/jwt v0.1.10 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
@@ -366,6 +366,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.3.6 // indirect
|
||||
github.com/openrdap/rdap v0.9.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.23 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
@@ -384,13 +385,14 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||
@@ -455,7 +457,7 @@ require (
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.58.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
google.golang.org/api v0.264.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
|
||||
@@ -147,13 +147,15 @@ github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi
|
||||
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -170,21 +172,22 @@ github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
|
||||
github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI=
|
||||
github.com/andybalholm/brotli v1.2.7 h1:UgafRPGMam5ySHfRkhthMqOFA6IFoeF3D7mWIDLjG5A=
|
||||
github.com/andybalholm/brotli v1.2.7/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/andygrunwald/go-jira v1.16.1 h1:WoQEar5XoDRAibOgKzTFELlPNlKAtnfWr296R9zdFLA=
|
||||
github.com/andygrunwald/go-jira v1.16.1/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antchfx/htmlquery v1.3.7 h1:KCUklE1EO/xm6iS3CWx6iatFlcnENBk4I4UYa2pBmLU=
|
||||
github.com/antchfx/htmlquery v1.3.7/go.mod h1:/uBTMnFEl6LaBlIgw3KZ+c061tAQdlfbCXObZoxwQf4=
|
||||
github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=
|
||||
github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.9 h1:muXJSod6jRzhsiUjvduNSxsRSptFZ2GOJ1oMD1QkPSA=
|
||||
github.com/antchfx/xpath v1.3.9/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
|
||||
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
@@ -356,6 +359,7 @@ github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9
|
||||
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||
@@ -423,8 +427,8 @@ github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc=
|
||||
github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
|
||||
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
|
||||
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
|
||||
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe h1:QKe/kmAYbndxwu91TcjHERsnMh5SgOB1x/qicvOdUJ8=
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
|
||||
github.com/getsops/sops/v3 v3.9.2 h1:Cuuahc/UGu7W3+NXd+mrl+2oXGxxj/jIPsw6Ah2CbxA=
|
||||
@@ -452,8 +456,8 @@ github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuw
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -749,8 +753,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/kitabisa/go-ci v1.0.3 h1:JmIUIvcercRQc/9x/v02ydCCqU4MadSHaNaOF8T2pGA=
|
||||
github.com/kitabisa/go-ci v1.0.3/go.mod h1:e3wBSzaJbcifXrr/Gw2ZBLn44MmeqP5WySwXyHlCK/U=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.20.2 h1:v5tiZvm1KGLyg5d5oXdXe3lsSPrbEUiKoXGia//I7Qo=
|
||||
github.com/klauspost/compress v1.20.2/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -931,8 +935,8 @@ github.com/projectdiscovery/cdncheck v1.3.2 h1:I453ZWNBeG0WWIy3wi9412l4PTzorLgeb
|
||||
github.com/projectdiscovery/cdncheck v1.3.2/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
|
||||
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
|
||||
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.9.0 h1:i1KyCbKJyKfHkgdbfh0JkWdd14APUjpEYf+GaYANlCg=
|
||||
github.com/projectdiscovery/dsl v0.9.0/go.mod h1:ntNFkkiYCTEosGFTbApA61RMrvJTCjnakPxnWj56MyY=
|
||||
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
|
||||
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
@@ -953,8 +957,8 @@ github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwap
|
||||
github.com/projectdiscovery/gologger v1.1.73/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
|
||||
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
|
||||
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2 h1:aSLbDvrJQPlp4HvojU+bTU5XV8xdAVCjoc5gX1Wfbw4=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2/go.mod h1:Z3VjpUJV1pnXDwvAddA8aZ4e839SoeftB7CSdZntAVI=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
|
||||
github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP1Erl8kaU=
|
||||
@@ -995,10 +999,10 @@ github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLv
|
||||
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
|
||||
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
|
||||
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
|
||||
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350 h1:uXqpYa6XKV9dCvoti9PbSgIWIsBKPu5YKMywB9G4q0Q=
|
||||
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.6 h1:1FXCMeCa1xVu2zaW+eeBcbVxtlMStqGOlKcMLyh2pzI=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.6/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5 h1:AOspf7ScbwZr8A2YnjCFd4ZJAMSFqzfEaIJIbJPuu10=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -1051,8 +1055,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQKO9E0=
|
||||
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
@@ -1335,8 +1339,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1424,8 +1428,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1452,8 +1456,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1518,8 +1522,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1532,8 +1536,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1550,8 +1554,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1603,8 +1607,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
+5
-29
@@ -4,8 +4,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -89,7 +87,11 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
|
||||
tmp.Update()
|
||||
u.uploadURL = tmp.URL
|
||||
|
||||
u.client = newUploadClient()
|
||||
// create http client
|
||||
opts := retryablehttp.DefaultOptionsSingle
|
||||
opts.NoAdjustTimeout = true
|
||||
opts.Timeout = time.Duration(3) * time.Minute
|
||||
u.client = retryablehttp.NewClient(opts)
|
||||
|
||||
// create context
|
||||
ctx, u.cancel = context.WithCancel(ctx)
|
||||
@@ -99,32 +101,6 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// newUploadClient returns the client for cloud uploads. The upload sends the
|
||||
// API key and the scan results, so the client verifies the server certificate.
|
||||
// The retryablehttp default clients skip verification.
|
||||
func newUploadClient() *retryablehttp.Client {
|
||||
transport := retryablehttp.DefaultReusePooledTransport()
|
||||
// fastdialer DialTLS does not verify certificates, so net/http does the
|
||||
// TLS handshake over the fastdialer connection.
|
||||
transport.DialTLSContext = nil
|
||||
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
|
||||
opts := retryablehttp.DefaultOptionsSingle
|
||||
opts.NoAdjustTimeout = true
|
||||
opts.Timeout = time.Duration(3) * time.Minute
|
||||
opts.HttpClient = &http.Client{Transport: transport}
|
||||
// a certificate error does not go away on retry
|
||||
defaultRetry := retryablehttp.DefaultRetryPolicy()
|
||||
opts.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
var certErr *tls.CertificateVerificationError
|
||||
if errors.As(err, &certErr) {
|
||||
return false, nil
|
||||
}
|
||||
return defaultRetry(ctx, resp, err)
|
||||
}
|
||||
return retryablehttp.NewClient(opts)
|
||||
}
|
||||
|
||||
// SetScanID sets the scan id for the upload writer
|
||||
func (u *UploadWriter) SetScanID(id string) error {
|
||||
if !xidRegex.MatchString(id) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package pdcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestUploadWriter(t *testing.T, gotKey *atomic.Value) (*UploadWriter, *httptest.Server) {
|
||||
t.Helper()
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotKey.Store(r.Header.Get(pdcpauth.ApiKeyHeaderName))
|
||||
_, _ = w.Write([]byte(`{"id":"abcdefghij0123456789"}`))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
creds := &pdcpauth.PDCPCredentials{APIKey: "test-key", Server: srv.URL}
|
||||
u, err := NewUploadWriter(context.Background(), gologger.DefaultLogger, creds)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(u.Close)
|
||||
return u, srv
|
||||
}
|
||||
|
||||
func TestUploadRejectsUntrustedCertificate(t *testing.T) {
|
||||
var gotKey atomic.Value
|
||||
u, _ := newTestUploadWriter(t, &gotKey)
|
||||
|
||||
err := u.upload([]byte("{}\n"))
|
||||
require.Error(t, err)
|
||||
var unknownAuthority x509.UnknownAuthorityError
|
||||
require.ErrorAs(t, err, &unknownAuthority)
|
||||
require.Nil(t, gotKey.Load(), "API key must not reach a server with an untrusted certificate")
|
||||
}
|
||||
|
||||
func TestUploadTrustedCertificate(t *testing.T) {
|
||||
var gotKey atomic.Value
|
||||
u, srv := newTestUploadWriter(t, &gotKey)
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(srv.Certificate())
|
||||
transport, ok := u.client.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}
|
||||
|
||||
require.NoError(t, u.upload([]byte("{}\n")))
|
||||
require.Equal(t, "test-key", gotKey.Load())
|
||||
require.Equal(t, "abcdefghij0123456789", u.scanID)
|
||||
}
|
||||
@@ -39,9 +39,6 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
|
||||
}
|
||||
httpxOptions.RetryMax = r.options.Retries
|
||||
httpxOptions.Timeout = time.Duration(r.options.Timeout) * time.Second
|
||||
if len(r.options.InternalResolversList) > 0 {
|
||||
httpxOptions.Resolvers = r.options.InternalResolversList
|
||||
}
|
||||
|
||||
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
|
||||
if dialers == nil {
|
||||
|
||||
+1
-35
@@ -2,12 +2,8 @@ package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
utilsllm "github.com/projectdiscovery/utils/llm"
|
||||
@@ -32,32 +28,6 @@ func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON b
|
||||
})
|
||||
}
|
||||
|
||||
// llmJudgeClient is an llmMatcherClient whose provider classifies natively, so
|
||||
// llm matchers ask it through Judge instead of Complete.
|
||||
type llmJudgeClient struct {
|
||||
*llmMatcherClient
|
||||
warnComplete sync.Once
|
||||
}
|
||||
|
||||
func (a *llmJudgeClient) Judge(ctx context.Context, question string, labels, inputs []string) (llmclient.Verdict, error) {
|
||||
verdict, err := a.client.Judge(ctx, utilsllm.JudgeRequest{Question: question, Labels: labels, Inputs: inputs})
|
||||
if err != nil {
|
||||
return llmclient.Verdict{}, err
|
||||
}
|
||||
return llmclient.Verdict{Label: verdict.Label, Confidence: verdict.Confidence, Model: verdict.Model}, nil
|
||||
}
|
||||
|
||||
// Complete is only reached by llm extractors, which need free text a
|
||||
// classifier cannot produce. Returning here spends no call budget and
|
||||
// makes no provider request. Extractors turn the error into an empty
|
||||
// result, and the warning says why.
|
||||
func (a *llmJudgeClient) Complete(context.Context, string, bool) (string, error) {
|
||||
a.warnComplete.Do(func() {
|
||||
gologger.Warning().Msgf("llm provider %q cannot run llm extractors, they return nothing", utilsllm.ProviderJev)
|
||||
})
|
||||
return "", errors.New("llm provider cannot run extractors")
|
||||
}
|
||||
|
||||
// configureLLM builds the scan's llm client from options. It returns nil unless
|
||||
// -llm is set, so nothing reaches a model until the user opts in. The API key is
|
||||
// read from the environment by the provider layer, never from options.
|
||||
@@ -82,9 +52,5 @@ func configureLLM(options *types.Options) (llmclient.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matcherClient := &llmMatcherClient{client: client, model: options.LLMModel}
|
||||
if client.CanJudge() {
|
||||
return &llmJudgeClient{llmMatcherClient: matcherClient}, nil
|
||||
}
|
||||
return matcherClient, nil
|
||||
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigureLLMExposesJudgeOnlyForClassifiers(t *testing.T) {
|
||||
t.Setenv("LLM_API_KEY", "key")
|
||||
|
||||
client, err := configureLLM(&types.Options{EnableLLM: true, LLMProvider: "jev", LLMModel: "jev-latest"})
|
||||
require.NoError(t, err)
|
||||
require.Implements(t, (*llmclient.Judge)(nil), client)
|
||||
|
||||
client, err = configureLLM(&types.Options{EnableLLM: true, LLMProvider: "ollama", LLMModel: "llama3.2"})
|
||||
require.NoError(t, err)
|
||||
_, isJudge := client.(llmclient.Judge)
|
||||
require.False(t, isJudge, "chat providers keep the Complete path")
|
||||
}
|
||||
|
||||
func TestJevExtractorDoesNotSpendCallBudget(t *testing.T) {
|
||||
t.Setenv("LLM_API_KEY", "key")
|
||||
// ProxyFromEnvironment caches the first request in the process. Match the
|
||||
// proxy the interactsh client test expects, and keep the local server direct.
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
var calls atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
_, _ = w.Write([]byte(`{"model":"jev-1.14.0","answers":{"q":{"type":"noul","noul":0.9}}}`))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := configureLLM(&types.Options{
|
||||
EnableLLM: true,
|
||||
LLMProvider: "jev",
|
||||
LLMBaseURL: server.URL + "/v1",
|
||||
LLMModel: "jev-latest",
|
||||
LLMMaxCalls: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.Complete(context.Background(), "extract the title", true)
|
||||
require.ErrorContains(t, err, "cannot run extractors")
|
||||
_, err = client.Complete(context.Background(), "extract another", true)
|
||||
require.Error(t, err)
|
||||
require.Zero(t, calls.Load())
|
||||
|
||||
judge := client.(llmclient.Judge)
|
||||
verdict, err := judge.Judge(context.Background(), "login page?", nil, []string{"<form>"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "yes", verdict.Label)
|
||||
require.EqualValues(t, 1, calls.Load())
|
||||
}
|
||||
+22
-47
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/csv"
|
||||
"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"
|
||||
@@ -120,14 +119,6 @@ func ParseOptions(options *types.Options) {
|
||||
options.Logger.Debug().Msgf("Store response directory specified, enabling \"store-resp\" flag automatically\n")
|
||||
options.StoreResponse = true
|
||||
}
|
||||
// Derived before validation: -uncover-query turns uncover on, and -plan
|
||||
// has to reject that before any target lookup starts.
|
||||
if options.UncoverQuery != nil {
|
||||
options.Uncover = true
|
||||
if len(options.UncoverEngine) == 0 {
|
||||
options.UncoverEngine = append(options.UncoverEngine, "shodan")
|
||||
}
|
||||
}
|
||||
// Validate the options passed by the user and if any
|
||||
// invalid options have been used, exit.
|
||||
if err := ValidateOptions(options); err != nil {
|
||||
@@ -147,6 +138,13 @@ func ParseOptions(options *types.Options) {
|
||||
_ = os.Setenv("GITHUB_TOKEN", options.GitHubToken)
|
||||
}
|
||||
|
||||
if options.UncoverQuery != nil {
|
||||
options.Uncover = true
|
||||
if len(options.UncoverEngine) == 0 {
|
||||
options.UncoverEngine = append(options.UncoverEngine, "shodan")
|
||||
}
|
||||
}
|
||||
|
||||
if options.OfflineHTTP {
|
||||
options.DisableHTTPProbe = true
|
||||
}
|
||||
@@ -164,9 +162,6 @@ func ValidateOptions(options *types.Options) error {
|
||||
}
|
||||
return errors.Wrap(errors.New(strings.Join(errs, ", ")), "validation failed for these fields")
|
||||
}
|
||||
if err := validatePlanOptions(options); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Verbose && options.Silent {
|
||||
return errors.New("both verbose and silent mode specified")
|
||||
}
|
||||
@@ -233,14 +228,25 @@ func ValidateOptions(options *types.Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
// An empty -iv leaves one unpinned target so the dialer can fall back
|
||||
// between address families. -sa resolves and scans every selected address.
|
||||
// Explicit -iv 4, -iv 6 and -iv 4,6 retain their usual selection semantics.
|
||||
// verify that a valid ip version type was selected (4, 6)
|
||||
if len(options.IPVersion) == 0 {
|
||||
// add ipv4 as default
|
||||
options.IPVersion = append(options.IPVersion, "4")
|
||||
}
|
||||
var useIPV4, useIPV6 bool
|
||||
for _, ipv := range options.IPVersion {
|
||||
if ipv != "4" && ipv != "6" {
|
||||
switch ipv {
|
||||
case "4":
|
||||
useIPV4 = true
|
||||
case "6":
|
||||
useIPV6 = true
|
||||
default:
|
||||
return fmt.Errorf("unsupported ip version: %s", ipv)
|
||||
}
|
||||
}
|
||||
if !useIPV4 && !useIPV6 {
|
||||
return errors.New("ipv4 and/or ipv6 must be selected")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -372,18 +378,6 @@ func createReportingOptions(options *types.Options) (*reporting.Options, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.CSVExport != "" {
|
||||
// Combine the CLI options with the config file options with the CLI options taking precedence
|
||||
if reportingOptions.CSVExporter != nil {
|
||||
reportingOptions.CSVExporter.File = options.CSVExport
|
||||
reportingOptions.CSVExporter.OmitRaw = options.OmitRawRequests
|
||||
} else {
|
||||
reportingOptions.CSVExporter = &csv.Options{
|
||||
File: options.CSVExport,
|
||||
OmitRaw: options.OmitRawRequests,
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.PDFExport != "" {
|
||||
if reportingOptions.PDFExporter != nil {
|
||||
reportingOptions.PDFExporter.File = options.PDFExport
|
||||
@@ -553,22 +547,3 @@ func getBoolEnvValue(key string) bool {
|
||||
value := os.Getenv(key)
|
||||
return strings.EqualFold(value, "true")
|
||||
}
|
||||
|
||||
// validatePlanOptions rejects options that change the templates or targets
|
||||
// only once scanning starts, which a plan written beforehand cannot show.
|
||||
func validatePlanOptions(options *types.Options) error {
|
||||
if options.PlanFile == "" {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case options.AutomaticScan:
|
||||
return errors.New("-plan cannot be used with -automatic-scan, which picks templates while scanning")
|
||||
case options.Uncover || len(options.UncoverQuery) > 0:
|
||||
return errors.New("-plan cannot be used with -uncover, which finds targets over the network")
|
||||
case options.PreflightPortScan:
|
||||
return errors.New("-plan cannot be used with -preflight-portscan, which drops targets after scanning their ports")
|
||||
case options.DASTServer || options.DASTProxy:
|
||||
return errors.New("-plan cannot be used with -dast-server or -dast-proxy, which start a server instead of writing a plan")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -90,15 +90,3 @@ func TestReadsTargetsFromStdin(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The default must remain empty so the provider can distinguish automatic
|
||||
// IPv6 preference from explicit user-supplied -iv selections.
|
||||
func TestValidateOptionsIPVersionPreference(t *testing.T) {
|
||||
automatic := &types.Options{}
|
||||
require.NoError(t, ValidateOptions(automatic))
|
||||
require.Empty(t, automatic.IPVersion)
|
||||
for _, versions := range []goflags.StringSlice{{"4"}, {"6"}, {"4", "6"}} {
|
||||
require.NoError(t, ValidateOptions(&types.Options{IPVersion: versions}))
|
||||
}
|
||||
require.ErrorContains(t, ValidateOptions(&types.Options{IPVersion: goflags.StringSlice{"7"}}), "unsupported ip version")
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/core"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
// httpRequestSeconds is the time of one http request the plan costs are
|
||||
// weighted against (see core.PlanEntry.Cost).
|
||||
const httpRequestSeconds = 0.015
|
||||
|
||||
// planSummary is the last line of a plan file, written once every entry is
|
||||
// known.
|
||||
type planSummary struct {
|
||||
Entries int `json:"entries"`
|
||||
// Alternatives are entries that run only when another of their origin
|
||||
// fails, so they are left out of every other total.
|
||||
Alternatives int `json:"alternatives,omitempty"`
|
||||
Requests int `json:"requests"`
|
||||
Cost int `json:"cost"`
|
||||
// EstimatedSeconds is the largest of the time the rate limit allows, the
|
||||
// cost spread over every concurrent slot, and the longest entry, whose
|
||||
// requests run one after another. Hosts slower than the measured request
|
||||
// time make the scan take longer.
|
||||
EstimatedSeconds int `json:"estimated-seconds"`
|
||||
}
|
||||
|
||||
// writePlan writes one JSON line per unit of work the scan would run,
|
||||
// followed by the summary. Targets are listed as given: inputs that the scan
|
||||
// would probe to an http url first are not probed here, since probing sends
|
||||
// requests.
|
||||
func (r *Runner) writePlan(store *loader.Store, engine *core.Engine) error {
|
||||
if !r.options.DisableHTTPProbe && loader.IsHTTPBasedProtocolUsed(store) && r.isInputNonHTTP() {
|
||||
r.Logger.Info().Msgf("Targets without a scheme are planned as given; the scan probes them for http(s) first")
|
||||
}
|
||||
file, err := os.Create(r.options.PlanFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
writer := bufio.NewWriter(file)
|
||||
encoder := json.NewEncoder(writer)
|
||||
|
||||
var summary planSummary
|
||||
var headlessCost, longest int
|
||||
var encodeErr error
|
||||
engine.Plan(slices.Concat(store.Templates(), store.Workflows()), r.inputProvider, r.options.DisableClustering, func(entry core.PlanEntry) {
|
||||
if encodeErr != nil {
|
||||
return
|
||||
}
|
||||
encodeErr = encoder.Encode(entry)
|
||||
if entry.AlternativeTo != "" {
|
||||
summary.Alternatives++
|
||||
return
|
||||
}
|
||||
summary.Entries++
|
||||
summary.Requests += entry.Requests
|
||||
summary.Cost += entry.Cost
|
||||
longest = max(longest, entry.Cost)
|
||||
if entry.Protocol == types.HeadlessProtocol.String() {
|
||||
headlessCost += entry.Cost
|
||||
}
|
||||
})
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
summary.EstimatedSeconds = r.estimateSeconds(summary.Requests, summary.Cost-headlessCost, headlessCost, longest)
|
||||
if err := encoder.Encode(map[string]planSummary{"summary": summary}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Logger.Info().Msgf("Plan written to %s: %d template runs, up to %d requests, about %ds", r.options.PlanFile, summary.Entries, summary.Requests, summary.EstimatedSeconds)
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimateSeconds estimates the scan duration. Headless templates run in
|
||||
// their own, smaller pool, so their cost is spread over it separately.
|
||||
func (r *Runner) estimateSeconds(requests, cost, headlessCost, longest int) int {
|
||||
seconds := float64(longest) * httpRequestSeconds
|
||||
if r.options.RateLimit > 0 && r.options.RateLimitDuration > 0 {
|
||||
seconds = max(seconds, float64(requests)/(float64(r.options.RateLimit)/r.options.RateLimitDuration.Seconds()))
|
||||
}
|
||||
if slots := r.options.TemplateThreads * r.options.BulkSize; slots > 0 {
|
||||
seconds = max(seconds, float64(cost)*httpRequestSeconds/float64(slots))
|
||||
}
|
||||
if slots := r.options.HeadlessTemplateThreads * r.options.HeadlessBulkSize; slots > 0 {
|
||||
seconds = max(seconds, float64(headlessCost)*httpRequestSeconds/float64(slots))
|
||||
}
|
||||
return int(seconds + 0.5)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlanRejectsOptionsThatChangeTargetsLater(t *testing.T) {
|
||||
for name, options := range map[string]*types.Options{
|
||||
"automatic scan": {PlanFile: "p", AutomaticScan: true},
|
||||
"uncover": {PlanFile: "p", Uncover: true},
|
||||
"uncover query": {PlanFile: "p", UncoverQuery: goflags.StringSlice{"example"}},
|
||||
"preflight": {PlanFile: "p", PreflightPortScan: true},
|
||||
"dast server": {PlanFile: "p", DASTServer: true},
|
||||
"dast proxy": {PlanFile: "p", DASTProxy: true},
|
||||
} {
|
||||
require.Error(t, validatePlanOptions(options), name)
|
||||
}
|
||||
require.NoError(t, validatePlanOptions(&types.Options{PlanFile: "p"}))
|
||||
require.NoError(t, validatePlanOptions(&types.Options{Uncover: true}), "only -plan restricts them")
|
||||
require.NoError(t, validatePlanOptions(&types.Options{UncoverQuery: goflags.StringSlice{"example"}}))
|
||||
require.NoError(t, validatePlanOptions(&types.Options{DASTServer: true}))
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEstimateSecondsTakesTheSlowestBound(t *testing.T) {
|
||||
r := &Runner{options: &types.Options{
|
||||
RateLimit: 150, RateLimitDuration: time.Second,
|
||||
TemplateThreads: 25, BulkSize: 25,
|
||||
HeadlessTemplateThreads: 10, HeadlessBulkSize: 10,
|
||||
}}
|
||||
|
||||
require.Equal(t, 20, r.estimateSeconds(3000, 3000, 0, 12), "rate limit bound")
|
||||
require.Equal(t, 10, r.estimateSeconds(52, 41, 660, 660), "one long entry runs its requests in sequence")
|
||||
require.Equal(t, 9, r.estimateSeconds(100, 0, 60000, 60), "headless cost spreads over the headless pool")
|
||||
|
||||
r.options.RateLimit = 0
|
||||
require.Equal(t, 0, r.estimateSeconds(3000, 3000, 0, 1), "no rate limit, cost spread over 625 slots")
|
||||
}
|
||||
@@ -67,13 +67,11 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
@@ -147,18 +145,10 @@ func New(options *types.Options) (*Runner, error) {
|
||||
|
||||
// Version check by default
|
||||
if config.DefaultConfig.CanCheckForUpdates() {
|
||||
// -plan sends no request it can avoid, so it skips the version and
|
||||
// update checks; it still installs templates on a fresh setup, which
|
||||
// would otherwise plan nothing
|
||||
checkUpdates := options.PlanFile == ""
|
||||
var latestIgnoreHash string
|
||||
if checkUpdates {
|
||||
var versionCheckErr error
|
||||
latestIgnoreHash, versionCheckErr = installer.NucleiVersionCheck()
|
||||
if versionCheckErr != nil {
|
||||
if options.Verbose || options.Debug {
|
||||
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", versionCheckErr)
|
||||
}
|
||||
latestIgnoreHash, versionCheckErr := installer.NucleiVersionCheck()
|
||||
if versionCheckErr != nil {
|
||||
if options.Verbose || options.Debug {
|
||||
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", versionCheckErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,19 +169,17 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if err := tm.FreshInstallIfNotExists(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
|
||||
}
|
||||
if checkUpdates {
|
||||
if err := tm.UpdateIfOutdated(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to update nuclei templates: %s\n", err)
|
||||
}
|
||||
if err := tm.UpdateIfOutdated(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to update nuclei templates: %s\n", err)
|
||||
}
|
||||
|
||||
if checkUpdates && config.DefaultConfig.IgnoreFileNeedsUpdate(latestIgnoreHash) {
|
||||
if config.DefaultConfig.IgnoreFileNeedsUpdate(latestIgnoreHash) {
|
||||
if err := installer.UpdateIgnoreFile(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to update .nuclei-ignore file: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if checkUpdates && options.UpdateTemplates {
|
||||
if options.UpdateTemplates {
|
||||
// we automatically check for updates unless explicitly disabled
|
||||
// this print statement is only to inform the user that there are no updates
|
||||
noUpdates := !config.DefaultConfig.NeedsTemplateUpdate()
|
||||
@@ -602,13 +590,8 @@ func (r *Runner) RunEnumeration() error {
|
||||
httpclientpool.ResetConnectionStats()
|
||||
|
||||
// If the user has asked for DAST server mode, run the live
|
||||
// DAST fuzzing server. -plan never reaches this with those flags set:
|
||||
// validation rejects the combination, and this check covers a caller
|
||||
// that skipped it.
|
||||
// DAST fuzzing server.
|
||||
if r.options.DASTServer || r.options.DASTProxy {
|
||||
if err := validatePlanOptions(r.options); err != nil {
|
||||
return err
|
||||
}
|
||||
execurOpts := &server.NucleiExecutorOptions{
|
||||
Options: r.options,
|
||||
Output: r.output,
|
||||
@@ -692,7 +675,6 @@ func (r *Runner) RunEnumeration() error {
|
||||
Catalog: r.catalog,
|
||||
IssuesClient: r.issuesClient,
|
||||
RateLimiter: r.rateLimiter,
|
||||
RequestBodyBudget: httpbody.NewBudget(int64(r.options.MaxRequestBodyInFlight)),
|
||||
Interactsh: r.interactsh,
|
||||
ProjectFile: r.projectFile,
|
||||
Browser: r.browser,
|
||||
@@ -752,9 +734,6 @@ func (r *Runner) RunEnumeration() error {
|
||||
if r.options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
|
||||
}
|
||||
if r.options.AutoCalibrate {
|
||||
executorOpts.CatchAll = catchall.New(0)
|
||||
}
|
||||
|
||||
executorEngine := core.New(r.options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
@@ -832,11 +811,6 @@ func (r *Runner) RunEnumeration() error {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
}
|
||||
// before uncover, preflight, auth prefetch and probing below, all of which
|
||||
// reach the network
|
||||
if r.options.PlanFile != "" {
|
||||
return r.writePlan(store, executorEngine)
|
||||
}
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
@@ -1098,7 +1072,6 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
|
||||
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
|
||||
stats.ForceDisplayWarning(templates.SkippedUnverifiedTemplateStats)
|
||||
stats.ForceDisplayWarning(templates.SkippedRequestSignatureTemplateStats)
|
||||
stats.ForceDisplayWarning(templates.SkippedWorkflowStepStats)
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
|
||||
|
||||
@@ -6,17 +6,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
@@ -38,35 +32,6 @@ func TestCreateReportingOptions(t *testing.T) {
|
||||
require.Equal(t, resultOptions2.DenyList.Severities, resultOptions.DenyList.Severities)
|
||||
}
|
||||
|
||||
func TestCreateReportingOptionsCSVExport(t *testing.T) {
|
||||
t.Run("flag only", func(t *testing.T) {
|
||||
resultOptions, err := createReportingOptions(&types.Options{CSVExport: "out.csv", OmitRawRequests: true})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resultOptions.CSVExporter)
|
||||
require.Equal(t, "out.csv", resultOptions.CSVExporter.File)
|
||||
require.True(t, resultOptions.CSVExporter.OmitRaw)
|
||||
})
|
||||
|
||||
t.Run("flag is not set", func(t *testing.T) {
|
||||
resultOptions, err := createReportingOptions(&types.Options{})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resultOptions.CSVExporter)
|
||||
})
|
||||
|
||||
t.Run("flag takes precedence over config file", func(t *testing.T) {
|
||||
config := filepath.Join(t.TempDir(), "report.yaml")
|
||||
require.NoError(t, os.WriteFile(config, []byte("csv:\n file: from-config.csv\n"), 0o600))
|
||||
|
||||
resultOptions, err := createReportingOptions(&types.Options{ReportingConfig: config})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "from-config.csv", resultOptions.CSVExporter.File)
|
||||
|
||||
resultOptions, err = createReportingOptions(&types.Options{ReportingConfig: config, CSVExport: "from-flag.csv"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "from-flag.csv", resultOptions.CSVExporter.File)
|
||||
})
|
||||
}
|
||||
|
||||
type TestStruct1 struct {
|
||||
A string `yaml:"a"`
|
||||
Struct *TestStruct2 `yaml:"b"`
|
||||
@@ -232,63 +197,3 @@ func TestNewInteractshHTTPClient(t *testing.T) {
|
||||
require.NotNil(t, proxyURL)
|
||||
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
|
||||
}
|
||||
|
||||
func TestInitializeTemplatesHTTPInputUsesResolvers(t *testing.T) {
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
|
||||
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
queries := new(atomic.Int32)
|
||||
server := &dns.Server{PacketConn: listener, Handler: dns.HandlerFunc(func(writer dns.ResponseWriter, request *dns.Msg) {
|
||||
queries.Add(1)
|
||||
response := new(dns.Msg).SetReply(request)
|
||||
for _, question := range request.Question {
|
||||
if question.Qtype == dns.TypeA {
|
||||
response.Answer = append(response.Answer, &dns.A{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET}, A: net.ParseIP("127.0.0.1")})
|
||||
}
|
||||
}
|
||||
_ = writer.WriteMsg(response)
|
||||
})}
|
||||
started := make(chan struct{})
|
||||
server.NotifyStartedFunc = func() { close(started) }
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- server.ActivateAndServe() }()
|
||||
<-started
|
||||
t.Cleanup(func() { require.NoError(t, server.Shutdown()); require.NoError(t, <-done) })
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
resolverFile := filepath.Join(t.TempDir(), "resolv.conf")
|
||||
require.NoError(t, os.WriteFile(resolverFile, nil, 0o600))
|
||||
t.Setenv("RESOLVERS_PATH", resolverFile)
|
||||
|
||||
options := &types.Options{
|
||||
ExecutionId: t.Name(),
|
||||
Timeout: 5,
|
||||
BulkSize: 1,
|
||||
InternalResolversList: []string{listener.LocalAddr().String()},
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(options))
|
||||
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
|
||||
|
||||
target := "probe.nuclei.test:" + strconv.Itoa(srv.Listener.Addr().(*net.TCPAddr).Port)
|
||||
r := &Runner{
|
||||
options: options,
|
||||
inputProvider: provider.NewSimpleInputProviderWithUrls(options.ExecutionId, target),
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
|
||||
hm, err := r.initializeTemplatesHTTPInput()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = hm.Close() })
|
||||
|
||||
probed, ok := hm.Get(input.ProbeKey(target, ""))
|
||||
require.True(t, ok, "the probe should resolve the target through the -r resolver")
|
||||
require.Equal(t, "http://"+target, string(probed))
|
||||
require.Positive(t, queries.Load())
|
||||
}
|
||||
@@ -6,15 +6,13 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
"github.com/logrusorgru/aurora/v4"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
@@ -40,17 +38,12 @@ func (r *Runner) verboseTemplate(tpl *templates.Template) {
|
||||
tpl.Info.SeverityHolder.Severity))
|
||||
}
|
||||
|
||||
// listedTemplate names its fields like a result event, so a -jsonl listing
|
||||
// joins with scan results on the same keys.
|
||||
type listedTemplate struct {
|
||||
Template string `json:"template,omitempty"`
|
||||
TemplateID string `json:"template-id"`
|
||||
TemplatePath string `json:"template-path"`
|
||||
Info model.Info `json:"info"`
|
||||
}
|
||||
|
||||
func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
r.listingHeader("templates")
|
||||
r.Logger.Print().Msgf(
|
||||
"\nListing available %v nuclei templates for %v",
|
||||
config.DefaultConfig.TemplateVersion,
|
||||
config.DefaultConfig.TemplatesDirectory,
|
||||
)
|
||||
// order templates alphabetically by path
|
||||
templates := store.Templates()
|
||||
sort.Slice(templates, func(i, j int) bool {
|
||||
@@ -58,10 +51,6 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
})
|
||||
|
||||
for _, tpl := range templates {
|
||||
if r.options.JSONL && !r.options.TemplateDisplay {
|
||||
r.listTemplateJSON(tpl)
|
||||
continue
|
||||
}
|
||||
if hasExtraFlags(r.options) {
|
||||
if r.options.TemplateDisplay {
|
||||
colorize := !r.options.NoColor
|
||||
@@ -89,36 +78,12 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) listTemplateJSON(tpl *templates.Template) {
|
||||
path, _ := utils.TemplatePathURL(tpl.Path, tpl.ID, "")
|
||||
marshalled, err := json.Marshal(listedTemplate{
|
||||
Template: path,
|
||||
TemplateID: tpl.ID,
|
||||
TemplatePath: tpl.Path,
|
||||
Info: tpl.Info,
|
||||
})
|
||||
if err != nil {
|
||||
r.Logger.Error().Msgf("Could not marshal the template %s: %s", tpl.Path, err)
|
||||
return
|
||||
}
|
||||
r.Logger.Print().Msgf("%s", marshalled)
|
||||
}
|
||||
|
||||
// listingHeader is skipped for -jsonl, where every line must parse as JSON.
|
||||
func (r *Runner) listingHeader(what string) {
|
||||
if r.options.JSONL {
|
||||
return
|
||||
}
|
||||
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
|
||||
r.Logger.Print().Msgf(
|
||||
"\nListing available %v nuclei %s for %v",
|
||||
"\nListing available %v nuclei tags for %v",
|
||||
config.DefaultConfig.TemplateVersion,
|
||||
what,
|
||||
config.DefaultConfig.TemplatesDirectory,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
|
||||
r.listingHeader("tags")
|
||||
|
||||
type kv struct {
|
||||
Key string `json:"tag"`
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const listedTemplateYAML = `id: listed-template
|
||||
info:
|
||||
name: Listed Template
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: listing
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`
|
||||
|
||||
func TestTemplateListWritesJSONLines(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
path := filepath.Join(root, "http", "listed-template.yaml")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(listedTemplateYAML), 0o600))
|
||||
|
||||
writer := &ignoreFileLogWriter{}
|
||||
logger := &gologger.Logger{}
|
||||
logger.SetFormatter(formatter.NewCLI(true))
|
||||
logger.SetWriter(writer)
|
||||
logger.SetMaxLevel(levels.LevelInfo)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.Logger = logger
|
||||
options.Templates = []string{path}
|
||||
options.TemplateList = true
|
||||
options.JSONL = true
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
store, err := loader.New(loader.NewConfig(options, disk.NewCatalog(root), executerOpts))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, store.LoadTemplatesOnlyMetadata())
|
||||
|
||||
require.Len(t, store.Templates(), 1)
|
||||
|
||||
r := &Runner{options: options, Logger: logger}
|
||||
r.listAvailableStoreTemplates(store)
|
||||
|
||||
require.Len(t, writer.records, 1, "JSONL output carries no banner line")
|
||||
var listed map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(writer.records[0].message)), &listed))
|
||||
require.Equal(t, "listed-template", listed["template-id"])
|
||||
require.Equal(t, filepath.Join("http", "listed-template.yaml"), listed["template"])
|
||||
require.Equal(t, "Listed Template", listed["info"].(map[string]any)["name"])
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
browserEngine "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
@@ -111,9 +110,6 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
if opts.Options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
|
||||
}
|
||||
if opts.Options.AutoCalibrate {
|
||||
executorOpts.CatchAll = catchall.New(0)
|
||||
}
|
||||
|
||||
executorEngine := core.New(opts.Options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -102,7 +102,6 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/raw-unsafe-path-single-slash.yaml", TestCase: &httpRawUnsafePathSingleSlash{}},
|
||||
{Path: "protocols/http/disable-http-cache.yaml", TestCase: &httpDisableCache{}},
|
||||
{Path: "protocols/http/http-cache.yaml", TestCase: &httpCache{}},
|
||||
{Path: "protocols/http/get.yaml", TestCase: &httpExcludeHostsFileComments{}},
|
||||
}
|
||||
|
||||
type httpMultiVarSharing struct{}
|
||||
@@ -1984,59 +1983,3 @@ func (h *httpDisableCache) Execute(filePath string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure that comments in an -exclude-hosts file are ignored
|
||||
type httpExcludeHostsFileComments struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpExcludeHostsFileComments) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
_, _ = fmt.Fprintf(w, "This is test matcher text")
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
excludeHosts := func(content string) ([]string, error) {
|
||||
file, err := os.CreateTemp("", "exclude-hosts-*.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = os.Remove(file.Name()) }()
|
||||
_, err = file.WriteString(content)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", file.Name())
|
||||
}
|
||||
|
||||
// a comment line is not a host, even when it reads as an invalid regex
|
||||
results, err := excludeHosts("# hosts out of scope (see ticket\n203.0.113.10\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("comment line: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err != nil {
|
||||
return fmt.Errorf("comment line: %w", err)
|
||||
}
|
||||
|
||||
// a trailing comment does not stop the host from being excluded
|
||||
results, err = excludeHosts("127.0.0.1 # test server\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("trailing comment: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 0); err != nil {
|
||||
return fmt.Errorf("trailing comment: %w", err)
|
||||
}
|
||||
|
||||
// comma separated hosts are still split when passed on the command line
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", "203.0.113.10, 127.0.0.1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("comma separated: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 0); err != nil {
|
||||
return fmt.Errorf("comma separated: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
// ipmiSimSetup runs OpenIPMI's BMC simulator with an anonymous user,
|
||||
// ADMIN/ADMIN and root/calvin, and cipher suite 0 enabled.
|
||||
const ipmiSimSetup = `apt-get update -qq && apt-get install -y -qq --no-install-recommends openipmi >/dev/null && mkdir -p /etc/ipmi && cat > /etc/ipmi/lan.conf <<'EOF'
|
||||
name "bmc"
|
||||
set_working_mc 0x20
|
||||
startlan 1
|
||||
addr 0.0.0.0 623
|
||||
priv_limit admin
|
||||
allowed_auths_callback none md2 md5 straight
|
||||
allowed_auths_user none md2 md5 straight
|
||||
allowed_auths_operator none md2 md5 straight
|
||||
allowed_auths_admin none md2 md5 straight
|
||||
guid a123456789abcdefa123456789abcdef
|
||||
endlan
|
||||
user 1 true "" "" user 10 none md2 md5 straight
|
||||
user 2 true "ADMIN" "ADMIN" admin 10 none md2 md5 straight
|
||||
user 3 true "root" "calvin" admin 10 none md2 md5 straight
|
||||
EOF
|
||||
printf 'mc_setbmc 0x20\nmc_add 0x20 0 no-device-sdrs 0x23 9 8 0x9f 0x1291 0xf02 persist_sdr\nmc_enable 0x20\n' > /etc/ipmi/emu.cmd
|
||||
exec ipmi_sim -c /etc/ipmi/lan.conf -f /etc/ipmi/emu.cmd -n`
|
||||
|
||||
type javascriptIPMI struct{}
|
||||
|
||||
func (j *javascriptIPMI) Execute(filePath string) error {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create docker pool: %w", err)
|
||||
}
|
||||
if err := pool.Client.Ping(); err != nil {
|
||||
return fmt.Errorf("could not connect to Docker: %w", err)
|
||||
}
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "debian",
|
||||
Tag: "bookworm-slim",
|
||||
Platform: "linux/amd64",
|
||||
Cmd: []string{"sh", "-c", ipmiSimSetup},
|
||||
ExposedPorts: []string{"623/udp"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start ipmi simulator: %w", err)
|
||||
}
|
||||
defer purge(pool, resource)
|
||||
|
||||
address := net.JoinHostPort("127.0.0.1", resource.GetPort("623/udp"))
|
||||
if err := waitForServiceCheck(address, 3*time.Minute, rmcpPingCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// rmcpPingCheck waits for an answer to an RMCP presence ping, since the
|
||||
// port is udp and cannot be probed with a connect.
|
||||
func rmcpPingCheck(address string) error {
|
||||
conn, err := net.Dial("udp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if _, err := conn.Write([]byte{0x06, 0x00, 0xff, 0x06, 0x00, 0x00, 0x11, 0xbe, 0x80, 0x00, 0x00, 0x00}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
reply := make([]byte, 64)
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 9 || reply[8] != 0x40 {
|
||||
return fmt.Errorf("unexpected rmcp reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -46,7 +46,6 @@ var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
|
||||
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
|
||||
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
|
||||
{Path: "protocols/javascript/ipmi.yaml", TestCase: &javascriptIPMI{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
id: ipmi-checks
|
||||
|
||||
info:
|
||||
name: IPMI Checks
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
const ipmi = require('nuclei/ipmi');
|
||||
const caps = ipmi.GetAuthCapabilities(Host, Port);
|
||||
const root = ipmi.GetRAKPHash(Host, Port, 'root');
|
||||
|
||||
// cipher suite 0 lets a wrong password in
|
||||
const anyPassword = new ipmi.Client();
|
||||
const cipherZero = anyPassword.Login(Host, Port, 'ADMIN', 'wrong', 0);
|
||||
anyPassword.Close();
|
||||
|
||||
// an encrypted, signed session, then Get Device ID and admin privilege
|
||||
const client = new ipmi.Client();
|
||||
client.Login(Host, Port, 'ADMIN', 'ADMIN', 3);
|
||||
const device = client.Command(0x06, 0x01, []);
|
||||
const admin = client.Command(0x06, 0x3b, [0x04]);
|
||||
client.Close();
|
||||
const manufacturer = device.Data[6] | device.Data[7] << 8 | device.Data[8] << 16;
|
||||
|
||||
let wrongRejected = false;
|
||||
try { new ipmi.Client().Login(Host, Port, 'ADMIN', 'wrong', 3); } catch (e) { wrongRejected = true; }
|
||||
|
||||
ipmi.Ping(Host, Port) && caps.IPMI20 && caps.AnonymousLogin &&
|
||||
ipmi.GetCipherSuites(Host, Port).includes(0) &&
|
||||
ipmi.GetSystemGUID(Host, Port).length == 32 &&
|
||||
ipmi.FindRAKPPassword(root, ['admin', 'calvin']) == 'calvin' &&
|
||||
!ipmi.CheckRAKPPassword(root, 'admin') &&
|
||||
cipherZero && wrongRejected &&
|
||||
device.CompletionCode == 0 && manufacturer == 4753 &&
|
||||
admin.CompletionCode == 0;
|
||||
|
||||
args:
|
||||
Host: "{{Host}}"
|
||||
Port: "623"
|
||||
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "success == true"
|
||||
+6
-8
@@ -39,14 +39,12 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
Catalog: base.catalog,
|
||||
IssuesClient: base.rc,
|
||||
RateLimiter: base.rateLimiter,
|
||||
|
||||
RequestBodyBudget: base.executerOpts.RequestBodyBudget,
|
||||
Interactsh: base.interactshClient,
|
||||
Colorizer: aurora.New(aurora.WithColors(true)),
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Parser: base.parser,
|
||||
Browser: base.browserInstance,
|
||||
DoNotCache: opts.DoNotCacheTemplates,
|
||||
Interactsh: base.interactshClient,
|
||||
Colorizer: aurora.New(aurora.WithColors(true)),
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Parser: base.parser,
|
||||
Browser: base.browserInstance,
|
||||
DoNotCache: opts.DoNotCacheTemplates,
|
||||
}
|
||||
if base.interactshClient != nil {
|
||||
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
nucleiUtils "github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
)
|
||||
@@ -280,7 +279,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
Catalog: e.catalog,
|
||||
IssuesClient: e.rc,
|
||||
RateLimiter: e.rateLimiter,
|
||||
RequestBodyBudget: httpbody.NewBudget(int64(e.opts.MaxRequestBodyInFlight)),
|
||||
Interactsh: e.interactshClient,
|
||||
Colorizer: aurora.New(aurora.WithColors(true)),
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
@@ -360,9 +358,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
|
||||
httpxOptions := httpx.DefaultOptions
|
||||
httpxOptions.Timeout = 5 * time.Second
|
||||
if len(e.opts.InternalResolversList) > 0 {
|
||||
httpxOptions.Resolvers = e.opts.InternalResolversList
|
||||
}
|
||||
if client, err := httpx.New(&httpxOptions); err != nil {
|
||||
return err
|
||||
} else {
|
||||
|
||||
@@ -7,22 +7,18 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
aiTemplateGeneratorAPIEndpoint = "https://api.projectdiscovery.io/v1/template/ai"
|
||||
// aiTemplateHTTPClient verifies TLS, since the request carries the PDCP API key.
|
||||
aiTemplateHTTPClient = &http.Client{}
|
||||
// aiTemplateIDRegex keeps the template file name inside the pdcp directory.
|
||||
aiTemplateIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
)
|
||||
|
||||
type AITemplateResponse struct {
|
||||
@@ -116,7 +112,7 @@ func generateAITemplate(prompt string) (string, string, error) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(pdcpauth.ApiKeyHeaderName, creds.APIKey)
|
||||
|
||||
resp, err := aiTemplateHTTPClient.Do(req)
|
||||
resp, err := retryablehttp.DefaultClient().Do(req)
|
||||
if err != nil {
|
||||
return "", "", errkit.Newf("Failed to send HTTP request: %v", err)
|
||||
}
|
||||
@@ -142,9 +138,5 @@ func generateAITemplate(prompt string) (string, string, error) {
|
||||
return "", "", errkit.Newf("Failed to generate template")
|
||||
}
|
||||
|
||||
if !aiTemplateIDRegex.MatchString(result.TemplateID) {
|
||||
return "", "", errkit.Newf("Invalid template ID in API response: %q", result.TemplateID)
|
||||
}
|
||||
|
||||
return result.Completion, result.TemplateID, nil
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newAITemplateServer(t *testing.T, templateID string, gotKey *atomic.Value) {
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotKey.Store(r.Header.Get("X-Api-Key"))
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"completion": "id: test\n",
|
||||
"template_id": templateID,
|
||||
})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
oldEndpoint, oldClient := aiTemplateGeneratorAPIEndpoint, aiTemplateHTTPClient
|
||||
t.Cleanup(func() { aiTemplateGeneratorAPIEndpoint, aiTemplateHTTPClient = oldEndpoint, oldClient })
|
||||
aiTemplateGeneratorAPIEndpoint = srv.URL
|
||||
t.Setenv("PDCP_API_KEY", "test-key")
|
||||
if templateID != "" {
|
||||
// Trust the test certificate, to check the template_id only.
|
||||
aiTemplateHTTPClient = srv.Client()
|
||||
}
|
||||
}
|
||||
|
||||
func TestAITemplateVerifiesTLS(t *testing.T) {
|
||||
var gotKey atomic.Value
|
||||
newAITemplateServer(t, "", &gotKey)
|
||||
|
||||
_, _, err := generateAITemplate("check for exposed admin panels")
|
||||
require.ErrorContains(t, err, "certificate")
|
||||
require.Nil(t, gotKey.Load(), "the API key went to an untrusted server")
|
||||
}
|
||||
|
||||
func TestAITemplateRejectsUnsafeID(t *testing.T) {
|
||||
for _, id := range []string{"../../.config/nuclei/config", "a/b", `a\b`, "a.b", "a b"} {
|
||||
var gotKey atomic.Value
|
||||
newAITemplateServer(t, id, &gotKey)
|
||||
_, _, err := generateAITemplate("check for exposed admin panels")
|
||||
require.Error(t, err, id)
|
||||
}
|
||||
|
||||
var gotKey atomic.Value
|
||||
newAITemplateServer(t, "Abc-123_x", &gotKey)
|
||||
_, templateID, err := generateAITemplate("check for exposed admin panels")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Abc-123_x", templateID)
|
||||
}
|
||||
@@ -631,10 +631,10 @@ func (store *Store) noteMissingCapabilities(templatePath string, missingCaps []t
|
||||
// erroring out on discovering any faulty templates.
|
||||
func (store *Store) ValidateTemplates() error {
|
||||
templatePaths, errs := store.config.Catalog.GetTemplatesPath(store.finalTemplates)
|
||||
notFound := store.logErroredTemplates(errs)
|
||||
store.logErroredTemplates(errs)
|
||||
|
||||
workflowPaths, errs := store.config.Catalog.GetTemplatesPath(store.finalWorkflows)
|
||||
notFound += store.logErroredTemplates(errs)
|
||||
store.logErroredTemplates(errs)
|
||||
|
||||
templatePathsMap := make(map[string]struct{}, len(templatePaths))
|
||||
for _, path := range templatePaths {
|
||||
@@ -646,8 +646,7 @@ func (store *Store) ValidateTemplates() error {
|
||||
workflowPathsMap[path] = struct{}{}
|
||||
}
|
||||
|
||||
// a path that can't be resolved is not validated, so it can't count as valid
|
||||
if store.areTemplatesValid(templatePathsMap) && store.areWorkflowsValid(workflowPathsMap) && notFound == 0 {
|
||||
if store.areTemplatesValid(templatePathsMap) && store.areWorkflowsValid(workflowPathsMap) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1057,15 +1056,10 @@ func workflowContainsProtocol(workflow []*workflows.WorkflowTemplate) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// logErroredTemplates logs the templates that could not be found and returns
|
||||
// how many of them were not handled by the NotFoundCallback.
|
||||
func (s *Store) logErroredTemplates(erred map[string]error) int {
|
||||
notFound := 0
|
||||
func (s *Store) logErroredTemplates(erred map[string]error) {
|
||||
for template, err := range erred {
|
||||
if s.NotFoundCallback == nil || !s.NotFoundCallback(template) {
|
||||
s.logger.Error().Msgf("Could not find template '%s': %s", template, err)
|
||||
notFound++
|
||||
}
|
||||
}
|
||||
return notFound
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package loader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
templatesigner "github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -178,8 +176,25 @@ func TestLoadTemplatesOnlyMetadataLogsCachedTemplateParseErrors(t *testing.T) {
|
||||
require.Contains(t, output.String(), "Could not load template")
|
||||
}
|
||||
|
||||
func newValidateStore(t *testing.T, executionID, templatePath string) (*Store, *bytes.Buffer) {
|
||||
t.Helper()
|
||||
func TestValidateTemplatesRejectsRuntimeCompilationError(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "invalid-regex-group.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: invalid-regex-group
|
||||
|
||||
info:
|
||||
name: Invalid Regex Group
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: -1
|
||||
regex:
|
||||
- "(a)(b)"
|
||||
`), 0o600))
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
var output bytes.Buffer
|
||||
@@ -188,7 +203,7 @@ func newValidateStore(t *testing.T, executionID, templatePath string) (*Store, *
|
||||
logger.SetWriter(&utils.CaptureWriter{Buffer: &output})
|
||||
logger.SetMaxLevel(levels.LevelDebug)
|
||||
options.Logger = logger
|
||||
options.ExecutionId = executionID
|
||||
options.ExecutionId = "loader-invalid-regex-group"
|
||||
options.Templates = []string{templatePath}
|
||||
options.Validate = true
|
||||
options.TemplateLoadingConcurrency = 1
|
||||
@@ -211,50 +226,10 @@ func newValidateStore(t *testing.T, executionID, templatePath string) (*Store, *
|
||||
|
||||
store, err := New(NewConfig(options, catalog, executerOpts))
|
||||
require.NoError(t, err)
|
||||
return store, &output
|
||||
}
|
||||
|
||||
func TestValidateTemplatesRejectsRuntimeCompilationError(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "invalid-regex-group.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: invalid-regex-group
|
||||
|
||||
info:
|
||||
name: Invalid Regex Group
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: -1
|
||||
regex:
|
||||
- "(a)(b)"
|
||||
`), 0o600))
|
||||
|
||||
store, output := newValidateStore(t, "loader-invalid-regex-group", templatePath)
|
||||
require.Error(t, store.ValidateTemplates())
|
||||
require.Contains(t, output.String(), "regex extractor group must be >= 0")
|
||||
}
|
||||
|
||||
func TestValidateTemplatesRejectsMissingTemplatePath(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "no-such-templates")
|
||||
|
||||
store, output := newValidateStore(t, "loader-missing-template-path", templatePath)
|
||||
require.Error(t, store.ValidateTemplates())
|
||||
require.Contains(t, output.String(), "Could not find template")
|
||||
}
|
||||
|
||||
func TestValidateTemplatesAcceptsMissingPathHandledByCallback(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "no-such-templates")
|
||||
|
||||
store, _ := newValidateStore(t, "loader-missing-path-callback", templatePath)
|
||||
store.NotFoundCallback = func(string) bool { return true }
|
||||
require.NoError(t, store.ValidateTemplates())
|
||||
}
|
||||
|
||||
func TestRemoteTemplates(t *testing.T) {
|
||||
catalog := disk.NewCatalog("")
|
||||
|
||||
@@ -687,135 +662,9 @@ http:
|
||||
loaded := loadSingleTemplateForTest(t, templatePath, "loader-unsigned-flow-template")
|
||||
require.Len(t, loaded, 1)
|
||||
require.Equal(t, "unsigned-flow-template", loaded[0].ID)
|
||||
require.False(t, loaded[0].Verified)
|
||||
require.Equal(t, initialUnverifiedJavascript, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
|
||||
}
|
||||
|
||||
func contentDigestForLoaderTest(data []byte) [32]byte {
|
||||
dataDigest := sha256.Sum256(data)
|
||||
return sha256.Sum256(dataDigest[:])
|
||||
}
|
||||
|
||||
func loadTemplateWithPoisonedMetadata(t *testing.T, templatePath, executionID string, source []byte, disableUnsigned bool) []*templates.Template {
|
||||
t.Helper()
|
||||
|
||||
fileInfo, err := os.Stat(templatePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadataIndex, err := metadataindex.NewIndex(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
metadataIndex.Set(templatePath, &metadataindex.Metadata{
|
||||
ID: "poisoned",
|
||||
FilePath: templatePath,
|
||||
ModTime: fileInfo.ModTime(),
|
||||
Name: "Poisoned",
|
||||
Authors: []string{"pdteam"},
|
||||
Severity: "info",
|
||||
Verified: true,
|
||||
TemplateVerifier: "projectdiscovery/nuclei-templates",
|
||||
VerifierFingerprint: templatesigner.DefaultTemplateVerifiers[0].Fingerprint(),
|
||||
ContentDigest: contentDigestForLoaderTest(source),
|
||||
Validation: metadataindex.ValidationStrict,
|
||||
})
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Logger = &gologger.Logger{}
|
||||
options.ExecutionId = executionID
|
||||
options.DisableUnsignedTemplates = disableUnsigned
|
||||
options.TemplateLoadingConcurrency = 1
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() {
|
||||
testutils.Cleanup(options)
|
||||
})
|
||||
|
||||
catalog := disk.NewCatalog("")
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Catalog = catalog
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
executerOpts.Logger = options.Logger
|
||||
|
||||
workflowLoader, err := workflow.NewLoader(executerOpts)
|
||||
require.NoError(t, err)
|
||||
executerOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
loaderConfig := NewConfig(options, catalog, executerOpts)
|
||||
loaderConfig.MetadataIndex = metadataIndex
|
||||
store, err := New(loaderConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
loaded, err := store.LoadTemplates([]string{templatePath})
|
||||
require.NoError(t, err)
|
||||
return loaded
|
||||
}
|
||||
|
||||
func TestLoadTemplatesIgnoresSelfConsistentCacheForJavascript(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "poisoned-javascript.yaml")
|
||||
source := []byte(`id: poisoned-cached-javascript
|
||||
|
||||
info:
|
||||
name: Poisoned Cached Javascript
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- init: |
|
||||
set("init-status", "executed")
|
||||
code: |
|
||||
Export("poisoned-cached-javascript")
|
||||
`)
|
||||
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
|
||||
|
||||
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-javascript", source, false)
|
||||
require.Empty(t, loaded)
|
||||
}
|
||||
|
||||
func TestLoadTemplatesAllowsUnsignedFlowDespitePoisonedCache(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "poisoned-flow.yaml")
|
||||
source := []byte(`id: poisoned-cached-flow
|
||||
|
||||
info:
|
||||
name: Poisoned Cached Flow
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
flow: http(1)
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`)
|
||||
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
|
||||
|
||||
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-flow", source, false)
|
||||
require.Len(t, loaded, 1)
|
||||
require.Equal(t, "poisoned-cached-flow", loaded[0].ID)
|
||||
require.False(t, loaded[0].Verified)
|
||||
require.True(t, loaded[0].IsFlowTemplate())
|
||||
}
|
||||
|
||||
func TestLoadTemplatesSkipsPoisonedHttpWhenUnsignedDisabled(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "poisoned-http.yaml")
|
||||
source := []byte(`id: poisoned-cached-http
|
||||
|
||||
info:
|
||||
name: Poisoned Cached HTTP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`)
|
||||
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
|
||||
|
||||
initialUnverified := stats.GetValue(templates.SkippedUnverifiedTemplateStats)
|
||||
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-http-dut", source, true)
|
||||
require.Empty(t, loaded)
|
||||
require.Equal(t, initialUnverified+1, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
|
||||
}
|
||||
|
||||
func TestLoadTemplatesDoesNotRequireGlobalMatchersFlagToLoadTemplate(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "global-matchers.yaml")
|
||||
err := os.WriteFile(templatePath, []byte(`id: global-matchers-template
|
||||
|
||||
+13
-15
@@ -38,7 +38,19 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
|
||||
totalReqBeforeCluster := getRequestCount(templatesList) * int(target.Count())
|
||||
|
||||
finalTemplates, clusterCount := e.clusterTemplates(templatesList, noCluster)
|
||||
// attempt to cluster templates if noCluster is false
|
||||
var finalTemplates []*templates.Template
|
||||
clusterCount := 0
|
||||
if !noCluster {
|
||||
var clusterMappings map[string][]string
|
||||
finalTemplates, clusterCount, clusterMappings = templates.ClusterTemplates(templatesList, e.executerOpts)
|
||||
// Store cluster mappings in executerOpts for SDK access (thread-safe)
|
||||
if clusterMappings != nil {
|
||||
e.executerOpts.ClusterMappings = types.NewClusterMappingsMap(clusterMappings)
|
||||
}
|
||||
} else {
|
||||
finalTemplates = templatesList
|
||||
}
|
||||
|
||||
totalReqAfterClustering := getRequestCount(finalTemplates) * int(target.Count())
|
||||
|
||||
@@ -174,20 +186,6 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
return results
|
||||
}
|
||||
|
||||
// clusterTemplates merges templates that send identical requests unless
|
||||
// noCluster is set, and returns the templates to run with the number merged.
|
||||
func (e *Engine) clusterTemplates(templatesList []*templates.Template, noCluster bool) ([]*templates.Template, int) {
|
||||
if noCluster {
|
||||
return templatesList, 0
|
||||
}
|
||||
finalTemplates, clusterCount, clusterMappings := templates.ClusterTemplates(templatesList, e.executerOpts)
|
||||
// Store cluster mappings in executerOpts for SDK access (thread-safe)
|
||||
if clusterMappings != nil {
|
||||
e.executerOpts.ClusterMappings = types.NewClusterMappingsMap(clusterMappings)
|
||||
}
|
||||
return finalTemplates, clusterCount
|
||||
}
|
||||
|
||||
// expectedWork returns the requests and the tasks the target-bound templates
|
||||
// will run. It counts only the template and target pairs that per-target
|
||||
// profiles select, so progress reflects the work that will be done. A task is
|
||||
|
||||
+6
-15
@@ -382,19 +382,6 @@ func newOriginAttempt() *originAttempt {
|
||||
return attempt
|
||||
}
|
||||
|
||||
// originScopeKey returns the key an origin-scoped template runs once per, and
|
||||
// false when the template runs on every target.
|
||||
func originScopeKey(template *templates.Template, value *contextargs.MetaInput) (string, bool) {
|
||||
if template == nil || value == nil || !template.IsOriginScoped() {
|
||||
return "", false
|
||||
}
|
||||
origin := normalize.Origin(value.Input)
|
||||
if origin == "" {
|
||||
return "", false
|
||||
}
|
||||
return template.ID + "\x00" + origin + "\x00" + value.CustomIP, true
|
||||
}
|
||||
|
||||
// claimOriginScoped reports whether this caller should execute an origin-scoped
|
||||
// template. finish(true) records a successful run. finish(false) releases the
|
||||
// claim so a later target, including one that waited, can try again. Templates
|
||||
@@ -402,10 +389,14 @@ func originScopeKey(template *templates.Template, value *contextargs.MetaInput)
|
||||
// key so each address selected for one URL is tested on its own.
|
||||
func (e *Engine) claimOriginScoped(template *templates.Template, value *contextargs.MetaInput) (finish func(bool), run bool) {
|
||||
noop := func(bool) {}
|
||||
key, scoped := originScopeKey(template, value)
|
||||
if !scoped {
|
||||
if template == nil || value == nil || !template.IsOriginScoped() {
|
||||
return noop, true
|
||||
}
|
||||
origin := normalize.Origin(value.Input)
|
||||
if origin == "" {
|
||||
return noop, true
|
||||
}
|
||||
key := template.ID + "\x00" + origin + "\x00" + value.CustomIP
|
||||
actual, _ := e.originScoped.LoadOrStore(key, newOriginAttempt())
|
||||
slot := actual.(*originAttempt)
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
)
|
||||
|
||||
// PlanEntry is one unit of work a scan runs: a template, a cluster of
|
||||
// templates sharing one request, or a workflow, on one target.
|
||||
type PlanEntry struct {
|
||||
// ID identifies the entry across runs of the same plan.
|
||||
ID string `json:"id"`
|
||||
// Target is empty for self-contained templates, which run once.
|
||||
Target string `json:"target,omitempty"`
|
||||
// IP is the address the target is pinned to, when it is.
|
||||
IP string `json:"ip,omitempty"`
|
||||
Templates []string `json:"templates"`
|
||||
Protocol string `json:"protocol"`
|
||||
// Requests is what the templates declare, the count -stats starts from.
|
||||
// The scan sends fewer when a request needs a value the target lacks,
|
||||
// such as {{DN}} on an ip.
|
||||
Requests int `json:"requests"`
|
||||
// Cost is Requests weighted by how long one request of the protocol
|
||||
// takes, in http requests.
|
||||
Cost int `json:"cost"`
|
||||
// Dynamic marks entries whose requests depend on responses: workflows
|
||||
// (only the root is counted), flows and fuzzing.
|
||||
Dynamic bool `json:"dynamic,omitempty"`
|
||||
// AlternativeTo is the id of an entry running the same origin-scoped
|
||||
// template on the same origin. Workers race for an origin, so the scan
|
||||
// runs whichever of the group claims it first, and another only when
|
||||
// that run fails. Totals count the group once.
|
||||
AlternativeTo string `json:"alternative-to,omitempty"`
|
||||
}
|
||||
|
||||
// Plan reports the work ExecuteScanWithOpts would do with the same arguments,
|
||||
// without sending anything. It applies the same clustering, per-target
|
||||
// profiles and once-per-origin rule, so a pair is listed only if the scan
|
||||
// would run it. Later targets of an origin-scoped template are listed as
|
||||
// alternatives to the first, since the scan retries them when a run fails.
|
||||
func (e *Engine) Plan(templatesList []*templates.Template, target provider.InputProvider, noCluster bool, emit func(PlanEntry)) {
|
||||
finalTemplates, _ := e.clusterTemplates(templatesList, noCluster)
|
||||
// clustering does not keep an order; a plan is diffed and edited, so it does
|
||||
finalTemplates = slices.SortedFunc(slices.Values(finalTemplates), func(a, b *templates.Template) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
var targetBound []*templates.Template
|
||||
for _, template := range finalTemplates {
|
||||
if template.SelfContained {
|
||||
emit(withPlanID(planEntry(template, &contextargs.MetaInput{}, nil)))
|
||||
} else {
|
||||
targetBound = append(targetBound, template)
|
||||
}
|
||||
}
|
||||
|
||||
// entries are emitted as they are planned, so memory does not grow with
|
||||
// the plan; only the first id per origin is kept
|
||||
originIDs := make(map[string]string)
|
||||
target.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
selection := e.selectionFor(value)
|
||||
for _, template := range targetBound {
|
||||
if selection != nil && !template.SelectedBy(selection) {
|
||||
continue
|
||||
}
|
||||
entry := withPlanID(planEntry(template, value, selection))
|
||||
if key, scoped := originScopeKey(template, value); scoped {
|
||||
if id, seen := originIDs[key]; seen {
|
||||
entry.AlternativeTo = id
|
||||
} else {
|
||||
originIDs[key] = entry.ID
|
||||
}
|
||||
}
|
||||
emit(entry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func withPlanID(entry PlanEntry) PlanEntry {
|
||||
entry.ID = planEntryID(entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
// protocolCost is how many http requests one request of a protocol is worth
|
||||
// in time. Measured per call against a CDN-fronted site, where an http
|
||||
// request took about 15ms: a javascript call adds about 150ms of runtime, a
|
||||
// headless page load with waitload took about 1s. Unlisted protocols cost
|
||||
// about one round trip, like http.
|
||||
var protocolCost = map[types.ProtocolType]int{
|
||||
types.JavascriptProtocol: 10,
|
||||
types.HeadlessProtocol: 60,
|
||||
}
|
||||
|
||||
func planEntry(template *templates.Template, value *contextargs.MetaInput, selection protocols.TemplateSelection) PlanEntry {
|
||||
entry := PlanEntry{
|
||||
Target: value.Input,
|
||||
IP: value.CustomIP,
|
||||
Templates: []string{template.ID},
|
||||
Protocol: template.Type().String(),
|
||||
Requests: template.TotalRequests,
|
||||
Dynamic: template.Flow != "" || template.IsFuzzableRequest(),
|
||||
}
|
||||
weight := func(protocol types.ProtocolType) int { return max(protocolCost[protocol], 1) }
|
||||
|
||||
switch {
|
||||
case template.CompiledWorkflow != nil:
|
||||
// only the root of a workflow is certain to run; subtemplates depend
|
||||
// on what it matches
|
||||
entry.Requests = 0
|
||||
for _, workflow := range template.CompiledWorkflow.Workflows {
|
||||
for _, executer := range workflow.Executers {
|
||||
requests := executer.Executer.Requests()
|
||||
entry.Requests += requests
|
||||
entry.Cost += requests * weight(executer.TemplateType)
|
||||
}
|
||||
}
|
||||
entry.Dynamic = true
|
||||
case isCluster(template):
|
||||
cluster := template.Executer.(*templates.ClusterExecuter)
|
||||
entry.Templates = slices.Sorted(slices.Values(cluster.TemplateIDs(selection)))
|
||||
// TotalRequests of a cluster counts only its http and dns requests,
|
||||
// so an ssl cluster would show none
|
||||
entry.Requests = cluster.Requests()
|
||||
entry.Cost = entry.Requests * weight(template.Type())
|
||||
case len(template.RequestsQueue) > 0:
|
||||
// each block is weighted by its own protocol, so a template mixing
|
||||
// http with javascript is not priced as all http
|
||||
for _, request := range template.RequestsQueue {
|
||||
entry.Cost += request.Requests() * weight(request.Type())
|
||||
}
|
||||
default:
|
||||
entry.Cost = entry.Requests * weight(template.Type())
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func isCluster(template *templates.Template) bool {
|
||||
_, ok := template.Executer.(*templates.ClusterExecuter)
|
||||
return ok
|
||||
}
|
||||
|
||||
// planEntryID derives the id from what the entry runs, so planning the same
|
||||
// scan again yields the same ids.
|
||||
func planEntryID(entry PlanEntry) string {
|
||||
sum := sha256.Sum256([]byte(entry.Target + "\x00" + entry.IP + "\x00" + strings.Join(entry.Templates, "\x00")))
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlanEntryCostWeightsByProtocol(t *testing.T) {
|
||||
headless := &templates.Template{ID: "h", RequestsHeadless: []*headless.Request{{}}, TotalRequests: 2}
|
||||
require.Equal(t, 120, planEntry(headless, &contextargs.MetaInput{Input: "t"}, nil).Cost)
|
||||
|
||||
plain := &templates.Template{ID: "p", TotalRequests: 2}
|
||||
require.Equal(t, 2, planEntry(plain, &contextargs.MetaInput{Input: "t"}, nil).Cost, "unlisted protocols cost one request each")
|
||||
}
|
||||
|
||||
// countedRequest is a request block with a fixed protocol and count.
|
||||
type countedRequest struct {
|
||||
protocols.Request
|
||||
protocol tmpltypes.ProtocolType
|
||||
requests int
|
||||
}
|
||||
|
||||
func (r countedRequest) Type() tmpltypes.ProtocolType { return r.protocol }
|
||||
func (r countedRequest) Requests() int { return r.requests }
|
||||
|
||||
func TestPlanEntryWeightsEachProtocolBlock(t *testing.T) {
|
||||
// one http request and ten javascript calls: Type() reports http for the
|
||||
// whole template, which would price it as 11
|
||||
mixed := &templates.Template{
|
||||
ID: "mixed",
|
||||
RequestsHTTP: []*http.Request{{}},
|
||||
TotalRequests: 11,
|
||||
RequestsQueue: []protocols.Request{
|
||||
countedRequest{protocol: tmpltypes.HTTPProtocol, requests: 1},
|
||||
countedRequest{protocol: tmpltypes.JavascriptProtocol, requests: 10},
|
||||
},
|
||||
}
|
||||
entry := planEntry(mixed, &contextargs.MetaInput{Input: "t"}, nil)
|
||||
require.Equal(t, 11, entry.Requests)
|
||||
require.Equal(t, 101, entry.Cost)
|
||||
}
|
||||
|
||||
func TestPlanListsThePairsTheScanRuns(t *testing.T) {
|
||||
options := &types.Options{BulkSize: 2, TemplateThreads: 2, HeadlessBulkSize: 1, HeadlessTemplateThreads: 1}
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: &progressRecorder{},
|
||||
TargetScope: scopeByInput{
|
||||
"php": {paths: []string{"thinkphp.yaml"}},
|
||||
"none": {},
|
||||
},
|
||||
})
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"thinkphp.yaml", "generic.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 3,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
selfContained := &templates.Template{ID: "self.yaml", Path: "self.yaml", SelfContained: true, TotalRequests: 1}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "php"}, {Input: "none"}, {Input: "plain"}}}
|
||||
|
||||
var plan []PlanEntry
|
||||
engine.Plan(append(templatesList, selfContained), targets, true, func(entry PlanEntry) { plan = append(plan, entry) })
|
||||
|
||||
require.Empty(t, plan[0].Target, "a self-contained template runs once, with no target")
|
||||
require.Equal(t, []string{"self.yaml"}, plan[0].Templates)
|
||||
var planned []string
|
||||
ids := make(map[string]struct{})
|
||||
for _, entry := range plan[1:] {
|
||||
require.Equal(t, 3, entry.Requests)
|
||||
planned = append(planned, entry.Templates[0]+"@"+entry.Target)
|
||||
ids[entry.ID] = struct{}{}
|
||||
}
|
||||
require.Len(t, ids, len(plan)-1, "every entry has its own id")
|
||||
|
||||
var again []PlanEntry
|
||||
engine.Plan(append(templatesList, selfContained), targets, true, func(entry PlanEntry) { again = append(again, entry) })
|
||||
require.Equal(t, plan, again, "planning the same scan again yields the same entries and ids")
|
||||
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targets, true)
|
||||
require.ElementsMatch(t, runs, planned, "the plan lists exactly the pairs the scan runs")
|
||||
require.ElementsMatch(t, []string{"thinkphp.yaml@php", "thinkphp.yaml@plain", "generic.yaml@plain"}, planned)
|
||||
}
|
||||
|
||||
func TestPlanListsOriginAlternatives(t *testing.T) {
|
||||
options := &types.Options{BulkSize: 1, TemplateThreads: 1, HeadlessBulkSize: 1, HeadlessTemplateThreads: 1}
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: &progressRecorder{},
|
||||
})
|
||||
|
||||
root := &templates.Template{
|
||||
ID: "root.yaml",
|
||||
RequestsHTTP: []*http.Request{{Path: []string{"{{RootURL}}/health"}}},
|
||||
TotalRequests: 1,
|
||||
}
|
||||
require.True(t, root.IsOriginScoped())
|
||||
generic := &templates.Template{ID: "generic.yaml", TotalRequests: 1}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{
|
||||
{Input: "https://example.com/a"},
|
||||
{Input: "https://example.com/b"},
|
||||
{Input: "https://other.example/a"},
|
||||
{Input: "https://example.com/a", CustomIP: "203.0.113.5"},
|
||||
}}
|
||||
|
||||
var plan []PlanEntry
|
||||
engine.Plan([]*templates.Template{generic, root}, targets, true, func(entry PlanEntry) {
|
||||
plan = append(plan, entry)
|
||||
})
|
||||
|
||||
var roots []PlanEntry
|
||||
for _, entry := range plan {
|
||||
if entry.Templates[0] == "root.yaml" {
|
||||
roots = append(roots, entry)
|
||||
}
|
||||
}
|
||||
require.Equal(t, []PlanEntry{
|
||||
{ID: roots[0].ID, Target: "https://example.com/a", Templates: []string{"root.yaml"}, Protocol: "http", Requests: 1, Cost: 1},
|
||||
{ID: roots[1].ID, Target: "https://example.com/b", Templates: []string{"root.yaml"}, Protocol: "http", Requests: 1, Cost: 1, AlternativeTo: roots[0].ID},
|
||||
{ID: roots[2].ID, Target: "https://other.example/a", Templates: []string{"root.yaml"}, Protocol: "http", Requests: 1, Cost: 1},
|
||||
{ID: roots[3].ID, Target: "https://example.com/a", IP: "203.0.113.5", Templates: []string{"root.yaml"}, Protocol: "http", Requests: 1, Cost: 1},
|
||||
}, roots)
|
||||
require.NotEqual(t, roots[0].ID, roots[3].ID, "a pinned address is its own attempt")
|
||||
|
||||
var again []PlanEntry
|
||||
engine.Plan([]*templates.Template{generic, root}, targets, true, func(entry PlanEntry) {
|
||||
again = append(again, entry)
|
||||
})
|
||||
require.Equal(t, plan, again)
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/cookiejar"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
@@ -33,45 +32,22 @@ func (e *Engine) executeWorkflow(ctx *scan.ScanContext, w *workflows.Workflow) b
|
||||
templateThreads++
|
||||
}
|
||||
swg, _ := syncutil.New(syncutil.WithSize(templateThreads))
|
||||
// pending tracks every started step until it returns. swg only limits
|
||||
// concurrency and its slots can be released early, so completion is
|
||||
// tracked here instead.
|
||||
var pending sync.WaitGroup
|
||||
|
||||
for _, template := range w.Workflows {
|
||||
newCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
|
||||
// Root steps do not own a waitgroup slot.
|
||||
if err := e.runWorkflowStep(template, newCtx, results, swg, &pending, w, nil); err != nil {
|
||||
if err := e.runWorkflowStep(template, newCtx, results, swg, w); err != nil {
|
||||
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
|
||||
}
|
||||
}
|
||||
|
||||
pending.Wait()
|
||||
swg.Wait()
|
||||
|
||||
return results.Load()
|
||||
}
|
||||
|
||||
// slotRelease returns a func that gives back one swg slot. It is safe to call
|
||||
// from several goroutines and several times; only the first call releases.
|
||||
func slotRelease(swg *syncutil.AdaptiveWaitGroup) func() {
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(swg.Done)
|
||||
}
|
||||
}
|
||||
|
||||
// runWorkflowStep runs a workflow step for the workflow. It executes the workflow
|
||||
// in a recursive manner running all subtemplates and matchers.
|
||||
//
|
||||
// releaseSlot, when non-nil, releases the AdaptiveWaitGroup slot held by the
|
||||
// goroutine that is running this step. It must be called before this step
|
||||
// blocks on swg.AddWithContext for child steps; otherwise every in-flight
|
||||
// parent can hold a slot while waiting for another, which deadlocks when the
|
||||
// number of matching nested steps reaches TemplateThreads (see #8074).
|
||||
//
|
||||
// pending is incremented for every child step before it starts, and each child
|
||||
// calls Done when it returns, so the root can wait for the whole tree.
|
||||
func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan.ScanContext, results *atomic.Bool, swg *syncutil.AdaptiveWaitGroup, pending *sync.WaitGroup, w *workflows.Workflow, releaseSlot func()) error {
|
||||
func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan.ScanContext, results *atomic.Bool, swg *syncutil.AdaptiveWaitGroup, w *workflows.Workflow) error {
|
||||
var firstMatched bool
|
||||
var err error
|
||||
var mainErr error
|
||||
@@ -155,25 +131,18 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
continue
|
||||
}
|
||||
|
||||
// Free this step's slot before blocking on child acquires.
|
||||
if releaseSlot != nil {
|
||||
releaseSlot()
|
||||
}
|
||||
for _, subtemplate := range matcher.Subtemplates {
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return
|
||||
}
|
||||
pending.Add(1)
|
||||
|
||||
go func(subtemplate *workflows.WorkflowTemplate) {
|
||||
defer pending.Done()
|
||||
release := slotRelease(swg)
|
||||
defer release()
|
||||
defer swg.Done()
|
||||
|
||||
// create a new context with the same input but with unset callbacks
|
||||
// clone the Input so that other parallel executions won't overwrite the shared variables when subsequent templates are running
|
||||
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
|
||||
if err := e.runWorkflowStep(subtemplate, subCtx, results, swg, pending, w, release); err != nil {
|
||||
if err := e.runWorkflowStep(subtemplate, subCtx, results, swg, w); err != nil {
|
||||
gologger.Warning().Msgf(workflowStepExecutionError, subtemplate.Template, err)
|
||||
}
|
||||
}(subtemplate)
|
||||
@@ -193,27 +162,18 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
return mainErr
|
||||
}
|
||||
if len(template.Subtemplates) > 0 && firstMatched {
|
||||
// Free this step's slot before blocking on child acquires so nested
|
||||
// matching steps cannot exhaust TemplateThreads and deadlock (#8074).
|
||||
if releaseSlot != nil {
|
||||
releaseSlot()
|
||||
}
|
||||
for _, subtemplate := range template.Subtemplates {
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
pending.Add(1)
|
||||
|
||||
go func(template *workflows.WorkflowTemplate) {
|
||||
defer pending.Done()
|
||||
release := slotRelease(swg)
|
||||
defer release()
|
||||
|
||||
// create a new context with the same input but with unset callbacks
|
||||
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
|
||||
if err := e.runWorkflowStep(template, subCtx, results, swg, pending, w, release); err != nil {
|
||||
if err := e.runWorkflowStep(template, subCtx, results, swg, w); err != nil {
|
||||
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
|
||||
}
|
||||
swg.Done()
|
||||
}(subtemplate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,89 +303,6 @@ func TestWorkflowsSameStepExecutersRefreshTemplateContext(t *testing.T) {
|
||||
require.Equal(t, "foo", secondWhoami, "could not refresh workflow context into later executers in the same step")
|
||||
}
|
||||
|
||||
func TestWorkflowNestedSubtemplatesDoNotDeadlockAtConcurrencyLimit(t *testing.T) {
|
||||
// Repro for #8074: with TemplateThreads equal to the number of matching
|
||||
// nested parents, each parent used to hold a waitgroup slot while calling
|
||||
// AddWithContext for its children, so every slot was occupied by a parent
|
||||
// waiting for another slot and the workflow never finished.
|
||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||
matchOutputs := []*output.InternalWrappedEvent{{
|
||||
OperatorsResult: &operators.Result{},
|
||||
Results: []*output.ResultEvent{{}},
|
||||
}}
|
||||
|
||||
const parents = 3
|
||||
var entered atomic.Int32
|
||||
allHolding := make(chan struct{})
|
||||
releaseParents := make(chan struct{})
|
||||
leafRuns := &atomic.Int32{}
|
||||
|
||||
leaf := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
|
||||
leafRuns.Add(1)
|
||||
}}
|
||||
parentExec := &mockExecuter{
|
||||
result: true,
|
||||
outputs: matchOutputs,
|
||||
executeScanHook: func(ctx *scan.ScanContext) {
|
||||
if entered.Add(1) == parents {
|
||||
close(allHolding)
|
||||
}
|
||||
// Stay inside ExecuteWithResults so the parent still owns its
|
||||
// waitgroup slot while every peer also holds one.
|
||||
<-releaseParents
|
||||
},
|
||||
}
|
||||
mkParent := func() *workflows.WorkflowTemplate {
|
||||
return &workflows.WorkflowTemplate{
|
||||
Executers: []*workflows.ProtocolExecuterPair{{
|
||||
Executer: parentExec,
|
||||
Options: &protocols.ExecutorOptions{Progress: progressBar},
|
||||
}},
|
||||
Subtemplates: []*workflows.WorkflowTemplate{{
|
||||
Executers: []*workflows.ProtocolExecuterPair{{
|
||||
Executer: leaf,
|
||||
Options: &protocols.ExecutorOptions{Progress: progressBar},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
workflow := &workflows.Workflow{
|
||||
Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: parents}},
|
||||
Workflows: []*workflows.WorkflowTemplate{{
|
||||
Executers: []*workflows.ProtocolExecuterPair{{
|
||||
Executer: &mockExecuter{result: true, outputs: matchOutputs},
|
||||
Options: &protocols.ExecutorOptions{Progress: progressBar},
|
||||
}},
|
||||
Subtemplates: []*workflows.WorkflowTemplate{mkParent(), mkParent(), mkParent()},
|
||||
}},
|
||||
}
|
||||
|
||||
engine := &Engine{}
|
||||
input := contextargs.NewWithInput(context.Background(), "https://test.com")
|
||||
ctx := scan.NewScanContext(context.Background(), input)
|
||||
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
done <- engine.executeWorkflow(ctx, workflow)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-allHolding:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("nested parents did not all acquire TemplateThreads slots")
|
||||
}
|
||||
close(releaseParents)
|
||||
|
||||
select {
|
||||
case matched := <-done:
|
||||
require.True(t, matched, "nested workflow should report a match")
|
||||
require.EqualValues(t, parents, leafRuns.Load(), "each nested parent should run its leaf")
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("nested workflow deadlocked at TemplateThreads == matching nested parents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowSubtemplatesUnblockCancelledAcquire(t *testing.T) {
|
||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||
var started atomic.Int32
|
||||
|
||||
+18
-14
@@ -155,14 +155,23 @@ func (rule *Rule) Execute(input *ExecuteRuleInput) (err error) {
|
||||
baseInteractURLs := append([]string(nil), input.InteractURLs...)
|
||||
if rule.generator == nil {
|
||||
for _, component := range finalComponentList {
|
||||
var dataKeys map[string]struct{}
|
||||
|
||||
optionVars := rule.options.Options.Vars.AsMap()
|
||||
constants := rule.options.Constants
|
||||
dataValues := rule.options.NewVariablesScope(baseValues, optionVars, constants)
|
||||
// Render variables once, allocating their interactsh URLs. The
|
||||
// output may contain runtime data, so it is never rendered again.
|
||||
// get vars from variables while replacing interactsh urls
|
||||
evaluation := rule.options.Variables.EvaluateWithInteractshScope(dataValues, rule.options.Interactsh)
|
||||
input.Values = generators.MergeMaps(evaluation.Values, baseValues, optionVars, constants)
|
||||
input.InteractURLs = mergeInteractURLs(baseInteractURLs, evaluation.InteractURLs)
|
||||
evaluatedValues, interactURLs := evaluation.Values, evaluation.InteractURLs
|
||||
input.Values, dataKeys = mergeFuzzValueLayers(
|
||||
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueTemplate},
|
||||
fuzzValueLayer{values: baseValues, kind: fuzzValueData},
|
||||
fuzzValueLayer{values: optionVars, kind: fuzzValueData},
|
||||
fuzzValueLayer{values: constants, kind: fuzzValueData},
|
||||
)
|
||||
// Replace template-text interactsh markers without evaluating runtime values.
|
||||
input.Values, interactURLs = rule.evaluateVarsWithInteractsh(input.Values, interactURLs, dataKeys)
|
||||
input.InteractURLs = mergeInteractURLs(baseInteractURLs, interactURLs)
|
||||
|
||||
err := rule.executeRuleValues(input, component)
|
||||
if err != nil {
|
||||
@@ -231,17 +240,15 @@ func (rule *Rule) prepareGeneratorValues(values, baseValues map[string]interface
|
||||
evaluation := rule.options.Variables.EvaluateWithInteractshScope(evaluationValues, rule.options.Interactsh)
|
||||
evaluatedValues, interactURLs := evaluation.Values, evaluation.InteractURLs
|
||||
inputValues, dataKeys := mergeFuzzValueLayers(
|
||||
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueData},
|
||||
fuzzValueLayer{values: values, kind: fuzzValueTemplate},
|
||||
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueTemplate},
|
||||
fuzzValueLayer{values: baseValues, kind: fuzzValueData},
|
||||
fuzzValueLayer{values: optionVars, kind: fuzzValueData},
|
||||
fuzzValueLayer{values: constants, kind: fuzzValueData},
|
||||
)
|
||||
|
||||
// Generator values are template-authored payload definitions until this
|
||||
// render. Rendered variables, runtime/base, CLI option, and constant
|
||||
// values remain data. Payloads layer over variables so a payload that
|
||||
// shares a variable's name is still rendered.
|
||||
// render. Runtime/base, CLI option, and constant values remain data.
|
||||
return rule.evaluateVarsWithInteractsh(inputValues, interactURLs, dataKeys)
|
||||
}
|
||||
|
||||
@@ -319,7 +326,7 @@ func (rule *Rule) evaluateVars(input string) (string, error) {
|
||||
func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, interactshUrls []string, dataKeys map[string]struct{}) (map[string]interface{}, []string) {
|
||||
// Check if Interactsh options are configured
|
||||
if rule.options.Interactsh != nil {
|
||||
rendered := maps.Clone(data)
|
||||
data = maps.Clone(data)
|
||||
|
||||
interactshUrlsMap := make(map[string]struct{})
|
||||
for _, url := range interactshUrls {
|
||||
@@ -328,8 +335,7 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
|
||||
|
||||
interactshUrls = mapsutil.GetKeys(interactshUrlsMap)
|
||||
|
||||
// Render each template-text value against the unrendered input, so
|
||||
// the result does not depend on map iteration order.
|
||||
// Iterate through template-text data to replace Interactsh URL markers.
|
||||
for k, v := range data {
|
||||
if _, ok := dataKeys[k]; ok {
|
||||
continue
|
||||
@@ -343,7 +349,7 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
|
||||
InteractURLs: interactshUrls,
|
||||
})
|
||||
if err == nil {
|
||||
rendered[k] = got.Text
|
||||
data[k] = got.Text
|
||||
for _, url := range got.InteractURLs {
|
||||
if _, ok := interactshUrlsMap[url]; ok {
|
||||
continue
|
||||
@@ -354,8 +360,6 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = rendered
|
||||
}
|
||||
|
||||
return data, interactshUrls
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
package fuzz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/variables"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
// requestBodyWithDSL is request data that looks like a DSL expression.
|
||||
requestBodyWithDSL = `PLAIN-{{to_upper("abc")}}-END`
|
||||
queryRuleTarget = "http://127.0.0.1/echo?p=SEED"
|
||||
)
|
||||
|
||||
// fakeInteractshClient hands out numbered URLs without contacting a server.
|
||||
// Fuzz rules only allocate URLs through NewURLWithData, so the embedded nil
|
||||
// IClient is never called.
|
||||
type fakeInteractshClient struct {
|
||||
interactsh.IClient
|
||||
|
||||
issued int
|
||||
}
|
||||
|
||||
func (c *fakeInteractshClient) NewURLWithData(string) (string, error) {
|
||||
c.issued++
|
||||
|
||||
return fmt.Sprintf("oast%d.example", c.issued), nil
|
||||
}
|
||||
|
||||
// newQueryRule compiles a single-mode rule that replaces the p query value
|
||||
// with fuzzValue.
|
||||
func newQueryRule(
|
||||
t *testing.T,
|
||||
fuzzValue string,
|
||||
generator *generators.PayloadGenerator,
|
||||
templateVars variables.Variable,
|
||||
client interactsh.IClient,
|
||||
) *Rule {
|
||||
t.Helper()
|
||||
|
||||
rule := &Rule{
|
||||
Part: component.RequestQueryComponent,
|
||||
Type: "replace",
|
||||
Mode: "single",
|
||||
Fuzz: SliceOrMapSlice{Value: []string{fuzzValue}},
|
||||
}
|
||||
err := rule.Compile(generator, &protocols.ExecutorOptions{
|
||||
Variables: templateVars,
|
||||
Options: &types.Options{},
|
||||
Interactsh: client,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return rule
|
||||
}
|
||||
|
||||
// executeQueryRule runs rule against queryRuleTarget and returns the generated
|
||||
// requests.
|
||||
func executeQueryRule(
|
||||
t *testing.T,
|
||||
rule *Rule,
|
||||
method string,
|
||||
body io.Reader,
|
||||
values map[string]any,
|
||||
) []GeneratedRequest {
|
||||
t.Helper()
|
||||
|
||||
req, err := retryablehttp.NewRequest(method, queryRuleTarget, body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var got []GeneratedRequest
|
||||
err = rule.Execute(&ExecuteRuleInput{
|
||||
Input: contextargs.NewWithInput(context.Background(), queryRuleTarget),
|
||||
BaseRequest: req,
|
||||
Values: values,
|
||||
Callback: func(gr GeneratedRequest) bool {
|
||||
got = append(got, gr)
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return got
|
||||
}
|
||||
|
||||
// newRequestDataVariableRule returns a query rule whose template declares
|
||||
// b: "{{body}}", so b captures request data.
|
||||
func newRequestDataVariableRule(t *testing.T, fuzzValue string, payloads map[string]any) *Rule {
|
||||
t.Helper()
|
||||
|
||||
templateVars := newFuzzTestVariables()
|
||||
templateVars.Set("b", "{{body}}")
|
||||
|
||||
var generator *generators.PayloadGenerator
|
||||
|
||||
if payloads != nil {
|
||||
var err error
|
||||
|
||||
generator, err = generators.New(
|
||||
payloads, generators.BatteringRamAttack, "", disk.NewCatalog(""), "", types.DefaultOptions(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// The CLI creates an Interactsh client even with -no-interactsh.
|
||||
return newQueryRule(t, fuzzValue, generator, templateVars, &interactsh.Client{})
|
||||
}
|
||||
|
||||
// fuzzedQueryValues runs rule against a POST request whose body is
|
||||
// requestBodyWithDSL and returns the fuzzed p query value of each generated
|
||||
// request.
|
||||
func fuzzedQueryValues(t *testing.T, rule *Rule) []string {
|
||||
t.Helper()
|
||||
|
||||
generated := executeQueryRule(t, rule, http.MethodPost, strings.NewReader(requestBodyWithDSL), map[string]any{
|
||||
"body": requestBodyWithDSL,
|
||||
})
|
||||
|
||||
got := make([]string, 0, len(generated))
|
||||
for _, gr := range generated {
|
||||
got = append(got, gr.Request.URL.Query().Get("p"))
|
||||
}
|
||||
|
||||
return got
|
||||
}
|
||||
|
||||
func TestExecuteKeepsRequestDataInTemplateVariableLiteral(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("fuzz value", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := newRequestDataVariableRule(t, "{{b}}", nil)
|
||||
|
||||
require.Equal(t, []string{requestBodyWithDSL}, fuzzedQueryValues(t, rule))
|
||||
})
|
||||
|
||||
t.Run("payload value", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := newRequestDataVariableRule(t, "{{inj}}", map[string]any{
|
||||
"inj": []any{"{{b}}"},
|
||||
})
|
||||
|
||||
// Each run iterates a fresh value map. Repeat so the result
|
||||
// cannot depend on map iteration order.
|
||||
for range 256 {
|
||||
require.Equal(t, []string{requestBodyWithDSL}, fuzzedQueryValues(t, rule))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecuteRendersTemplateVariableInteractshMarker(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
templateVars := newFuzzTestVariables()
|
||||
templateVars.Set("marker", "{{interactsh-url}}")
|
||||
rule := newQueryRule(t, "{{marker}}", nil, templateVars, &fakeInteractshClient{})
|
||||
|
||||
got := executeQueryRule(t, rule, http.MethodGet, nil, nil)
|
||||
|
||||
require.Len(t, got, 1)
|
||||
require.Equal(t, []string{"oast1.example"}, got[0].InteractURLs)
|
||||
require.Equal(t, "oast1.example", got[0].Request.URL.Query().Get("p"))
|
||||
}
|
||||
|
||||
func TestPrepareGeneratorValuesRendersPayloadSharingVariableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
templateVars := newFuzzTestVariables()
|
||||
templateVars.Set("inj", "variable")
|
||||
|
||||
rule := &Rule{
|
||||
options: &protocols.ExecutorOptions{
|
||||
Variables: templateVars,
|
||||
Options: &types.Options{},
|
||||
Interactsh: &fakeInteractshClient{},
|
||||
},
|
||||
}
|
||||
|
||||
values, _ := rule.prepareGeneratorValues(map[string]any{
|
||||
"inj": `{{to_upper("payload")}}`,
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, "PAYLOAD", values["inj"])
|
||||
}
|
||||
|
||||
func TestPrepareGeneratorValuesRendersPayloadsAgainstUnrenderedInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := &Rule{
|
||||
options: &protocols.ExecutorOptions{
|
||||
Variables: newFuzzTestVariables(),
|
||||
Options: &types.Options{},
|
||||
Interactsh: &interactsh.Client{},
|
||||
},
|
||||
}
|
||||
|
||||
for range 256 {
|
||||
values, _ := rule.prepareGeneratorValues(map[string]any{
|
||||
"upper": `{{to_upper("x")}}`,
|
||||
"ref": "{{upper}}",
|
||||
}, nil)
|
||||
|
||||
require.Equal(t, "X", values["upper"])
|
||||
// ref sees the unrendered definition of upper as data.
|
||||
require.Equal(t, `{{to_upper("x")}}`, values["ref"])
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ Formats implements support for passing a number of request source as input provi
|
||||
Currently the following formats are implemented -
|
||||
|
||||
- Burp Suite XML Request/Response file
|
||||
- HAR (HTTP Archive) file exported by browsers and proxies
|
||||
- Proxify JSONL output file
|
||||
- OpenAPI Specification file
|
||||
- Postman Collection file
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package har
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
// HARFormat is an HTTP Archive (HAR 1.2) parser, the format browsers and
|
||||
// proxies export captured traffic in.
|
||||
type HARFormat struct {
|
||||
opts formats.InputFormatOptions
|
||||
}
|
||||
|
||||
// New creates a new HAR parser
|
||||
func New() *HARFormat {
|
||||
return &HARFormat{}
|
||||
}
|
||||
|
||||
var _ formats.Format = &HARFormat{}
|
||||
|
||||
type archive struct {
|
||||
Log struct {
|
||||
Entries []entry `json:"entries"`
|
||||
} `json:"log"`
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Request struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers []header `json:"headers"`
|
||||
PostData *struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"postData"`
|
||||
} `json:"request"`
|
||||
Response struct {
|
||||
Status int `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
Headers []header `json:"headers"`
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
Encoding string `json:"encoding"`
|
||||
} `json:"content"`
|
||||
} `json:"response"`
|
||||
}
|
||||
|
||||
type header struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Name returns the name of the format
|
||||
func (h *HARFormat) Name() string {
|
||||
return "har"
|
||||
}
|
||||
|
||||
func (h *HARFormat) SetOptions(options formats.InputFormatOptions) {
|
||||
h.opts = options
|
||||
}
|
||||
|
||||
// Parse parses the input and calls the provided callback
|
||||
// function for each RawRequest it discovers.
|
||||
func (h *HARFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallback, filePath string) error {
|
||||
var har archive
|
||||
if err := json.NewDecoder(input).Decode(&har); err != nil {
|
||||
return errors.Wrap(err, "could not decode har file")
|
||||
}
|
||||
|
||||
for _, e := range har.Log.Entries {
|
||||
// Browsers also log data:, blob: and extension URLs, which have no
|
||||
// host to send a request to.
|
||||
if !strings.HasPrefix(e.Request.URL, "http://") && !strings.HasPrefix(e.Request.URL, "https://") {
|
||||
continue
|
||||
}
|
||||
rawRequest, err := types.ParseRawRequestWithURL(rawRequestOf(e), e.Request.URL)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("har: Could not parse request %s: %s\n", e.Request.URL, err)
|
||||
continue
|
||||
}
|
||||
// Status 0 is a request that was blocked or never answered.
|
||||
if e.Response.Status != 0 {
|
||||
rawRequest.Response = &types.HttpResponse{Raw: rawResponseOf(e)}
|
||||
}
|
||||
resultsCb(rawRequest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawRequestOf rebuilds the request as HTTP/1.1 with an absolute target, which
|
||||
// carries the authority that HTTP/2 captures only keep in pseudo-headers.
|
||||
func rawRequestOf(e entry) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(e.Request.Method + " " + e.Request.URL + " HTTP/1.1\r\n")
|
||||
writeHeaders(&b, e.Request.Headers)
|
||||
if e.Request.PostData != nil {
|
||||
b.WriteString(e.Request.PostData.Text)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func rawResponseOf(e entry) string {
|
||||
statusText := e.Response.StatusText
|
||||
if statusText == "" {
|
||||
statusText = http.StatusText(e.Response.Status)
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("HTTP/1.1 " + strconv.Itoa(e.Response.Status) + " " + statusText + "\r\n")
|
||||
writeHeaders(&b, e.Response.Headers)
|
||||
body := e.Response.Content.Text
|
||||
if e.Response.Content.Encoding == "base64" {
|
||||
if decoded, err := base64.StdEncoding.DecodeString(body); err == nil {
|
||||
body = string(decoded)
|
||||
}
|
||||
}
|
||||
b.WriteString(body)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writeHeaders(b *strings.Builder, headers []header) {
|
||||
for _, h := range headers {
|
||||
// HTTP/2 pseudo-headers (:authority, :path, ...) are not headers.
|
||||
if strings.HasPrefix(h.Name, ":") {
|
||||
continue
|
||||
}
|
||||
b.WriteString(h.Name + ": " + h.Value + "\r\n")
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package har
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const chromeHAR = "../testdata/chrome.har"
|
||||
|
||||
func parse(t *testing.T, input string) []*types.RequestResponse {
|
||||
t.Helper()
|
||||
file, err := os.Open(input)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var got []*types.RequestResponse
|
||||
require.NoError(t, New().Parse(file, func(rr *types.RequestResponse) bool {
|
||||
got = append(got, rr)
|
||||
return false
|
||||
}, input))
|
||||
return got
|
||||
}
|
||||
|
||||
func TestHARParse(t *testing.T) {
|
||||
got := parse(t, chromeHAR)
|
||||
|
||||
urls := make([]string, 0, len(got))
|
||||
for _, rr := range got {
|
||||
urls = append(urls, rr.URL.String())
|
||||
}
|
||||
require.Equal(t, []string{
|
||||
"https://ginandjuice.shop/catalog?category=Juice",
|
||||
"https://ginandjuice.shop/login",
|
||||
"https://ginandjuice.shop/blocked.js",
|
||||
}, urls, "non-http entries such as data: URLs are skipped")
|
||||
}
|
||||
|
||||
func TestHARParseHTTP2Request(t *testing.T) {
|
||||
rr := parse(t, chromeHAR)[0]
|
||||
|
||||
require.Equal(t, "GET", rr.Request.Method)
|
||||
cookie, _ := rr.Request.Headers.Get("cookie")
|
||||
require.Equal(t, "session=abc123", cookie)
|
||||
for _, name := range rr.Request.Headers.GetKeys() {
|
||||
require.False(t, strings.HasPrefix(name, ":"), "pseudo-header %s is not a real header", name)
|
||||
}
|
||||
|
||||
req, err := rr.BuildRequest()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ginandjuice.shop", req.Host)
|
||||
}
|
||||
|
||||
func TestHARParseBodyAndResponse(t *testing.T) {
|
||||
got := parse(t, chromeHAR)
|
||||
|
||||
login := got[1]
|
||||
require.Equal(t, "POST", login.Request.Method)
|
||||
require.Equal(t, "username=carlos&password=hunter2", login.Request.Body)
|
||||
require.NotNil(t, login.Response)
|
||||
require.Contains(t, login.Response.Raw, "302 Found")
|
||||
require.Contains(t, login.Response.Raw, "Location: /my-account")
|
||||
|
||||
require.NotNil(t, got[0].Response)
|
||||
require.Contains(t, got[0].Response.Raw, "<h1>Juice</h1>", "base64 content is decoded")
|
||||
|
||||
require.Nil(t, got[2].Response, "a request that never got an answer has no response")
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package openapi
|
||||
|
||||
// Fuzz exercises OpenAPI 3.0 schema parsing and request generation. The input
|
||||
// bytes are treated as a raw schema document (YAML or JSON) and fed through the
|
||||
// format's Parse, which loads the schema and generates requests from it.
|
||||
func Fuzz(data []byte) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(data) > fuzzMaxInputSize {
|
||||
return -1
|
||||
}
|
||||
if !fuzzOpenAPIParsing(data) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
)
|
||||
|
||||
const fuzzMaxInputSize = 64 << 10
|
||||
|
||||
// fuzzOpenAPIParsing feeds data to the OpenAPI format parser as a raw schema
|
||||
// document and drives request generation. It must never panic for any input;
|
||||
// it returns true when the schema parsed and at least one request was produced.
|
||||
func fuzzOpenAPIParsing(data []byte) bool {
|
||||
if len(data) == 0 || len(data) > fuzzMaxInputSize {
|
||||
return false
|
||||
}
|
||||
|
||||
format := New()
|
||||
format.SetOptions(formats.InputFormatOptions{})
|
||||
|
||||
produced := false
|
||||
cb := func(rr *types.RequestResponse) bool {
|
||||
produced = true
|
||||
return true
|
||||
}
|
||||
if err := format.Parse(bytes.NewReader(data), cb, "fuzz.openapi"); err != nil {
|
||||
return false
|
||||
}
|
||||
return produced
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFuzzOpenAPIParsingSeedCorpus(t *testing.T) {
|
||||
entries, err := os.ReadDir("testdata/gofuzz-corpus")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, entries)
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join("testdata/gofuzz-corpus", entry.Name()))
|
||||
require.NoError(t, err)
|
||||
require.Truef(t, fuzzOpenAPIParsing(data), "seed %s should produce at least one request", entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzOpenAPIParsingRejectsOversizeInput(t *testing.T) {
|
||||
require.False(t, fuzzOpenAPIParsing(make([]byte, fuzzMaxInputSize+1)))
|
||||
}
|
||||
|
||||
// Null entries decode to nil pointers and used to panic during request
|
||||
// generation. The remaining valid entries must still produce requests.
|
||||
func TestFuzzOpenAPIParsingNullEntries(t *testing.T) {
|
||||
specs := map[string]string{
|
||||
"null server": `
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers:
|
||||
-
|
||||
- {url: "http://example.com"}
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
responses: {"200": {description: ok}}
|
||||
`,
|
||||
"null media type": `
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers: [{url: "http://example.com"}]
|
||||
paths:
|
||||
/users:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/xml:
|
||||
application/json:
|
||||
schema: {type: object, properties: {name: {type: string}}}
|
||||
responses: {"200": {description: ok}}
|
||||
`,
|
||||
}
|
||||
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.True(t, fuzzOpenAPIParsing([]byte(spec)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRequestsOnlyNullServers(t *testing.T) {
|
||||
spec := `
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers:
|
||||
-
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
responses: {"200": {description: ok}}
|
||||
`
|
||||
err := New().Parse(strings.NewReader(spec), func(*types.RequestResponse) bool { return false }, "spec.yaml")
|
||||
require.ErrorContains(t, err, "no servers found in openapi schema")
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/clbanning/mxj/v2"
|
||||
@@ -34,9 +33,7 @@ const (
|
||||
|
||||
// GenerateRequestsFromSchema generates http requests from an OpenAPI 3.0 document object
|
||||
func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOptions, callback formats.ParseReqRespCallback) error {
|
||||
// Null entries in `servers:` are skipped below, so a list of only null
|
||||
// entries has no usable server, the same as an empty list.
|
||||
if !slices.ContainsFunc(schema.Servers, func(s *openapi3.Server) bool { return s != nil }) {
|
||||
if len(schema.Servers) == 0 {
|
||||
return errors.New("no servers found in openapi schema")
|
||||
}
|
||||
globalParams, globalSecurityErr := selectSecurityParameters(schema, &schema.Security, opts.Variables)
|
||||
@@ -52,11 +49,6 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
|
||||
}
|
||||
|
||||
for _, serverURL := range schema.Servers {
|
||||
// A null entry in the `servers:` list decodes to a nil *Server, so skip
|
||||
// it rather than dereferencing it.
|
||||
if serverURL == nil {
|
||||
continue
|
||||
}
|
||||
pathURL := serverURL.URL
|
||||
// Split the server URL into baseURL and serverPath
|
||||
u, err := url.Parse(pathURL)
|
||||
@@ -276,11 +268,6 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
|
||||
if opts.op.RequestBody != nil {
|
||||
for content, value := range opts.op.RequestBody.Value.Content {
|
||||
// A null media-type entry (e.g. `content: {application/json: }`)
|
||||
// decodes to a nil *MediaType, so skip it before dereferencing.
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
cloned := req.Clone(req.Context())
|
||||
|
||||
var val interface{}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"openapi":"3.0.0","info":{"title":"t","version":"1"},"servers":[{"url":"http://example.com"}],"paths":{"/a":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"k":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}}}}
|
||||
@@ -1,7 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers: [{url: "http://example.com"}]
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
responses: {"200": {description: ok}}
|
||||
@@ -1,12 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers: [{url: "http://example.com"}]
|
||||
paths:
|
||||
/users:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/xml:
|
||||
application/json:
|
||||
schema: {type: object, properties: {name: {type: string}}}
|
||||
responses: {"200": {description: ok}}
|
||||
@@ -1,9 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers:
|
||||
-
|
||||
- {url: "http://example.com"}
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
responses: {"200": {description: ok}}
|
||||
@@ -3,9 +3,7 @@ package swagger
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi2"
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
@@ -57,7 +55,6 @@ func (j *SwaggerFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCal
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not decode openapi 2.0 schema")
|
||||
}
|
||||
dropNullEntries(schemav2)
|
||||
schema, err := openapi2conv.ToV3(schemav2)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not convert openapi 2.0 schema to 3.0")
|
||||
@@ -70,32 +67,6 @@ func (j *SwaggerFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCal
|
||||
return openapi.GenerateRequestsFromSchema(schema, j.opts, resultsCb)
|
||||
}
|
||||
|
||||
// dropNullEntries removes null entries from the maps and lists of a decoded
|
||||
// OpenAPI 2.0 document. A null value such as `paths: {/x: }` or an empty list
|
||||
// item under `parameters:` decodes to a nil pointer, which openapi2conv.ToV3
|
||||
// dereferences without checking.
|
||||
func dropNullEntries(doc *openapi2.T) {
|
||||
maps.DeleteFunc(doc.Paths, func(_ string, v *openapi2.PathItem) bool { return v == nil })
|
||||
maps.DeleteFunc(doc.Parameters, func(_ string, v *openapi2.Parameter) bool { return v == nil })
|
||||
dropNullResponses(doc.Responses)
|
||||
for _, pathItem := range doc.Paths {
|
||||
pathItem.Parameters = slices.DeleteFunc(pathItem.Parameters, func(v *openapi2.Parameter) bool { return v == nil })
|
||||
for _, op := range pathItem.Operations() {
|
||||
op.Parameters = slices.DeleteFunc(op.Parameters, func(v *openapi2.Parameter) bool { return v == nil })
|
||||
dropNullResponses(op.Responses)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropNullResponses removes null responses, and null headers of the remaining
|
||||
// responses, which openapi2conv.ToV3Response also dereferences.
|
||||
func dropNullResponses(responses map[string]*openapi2.Response) {
|
||||
maps.DeleteFunc(responses, func(_ string, v *openapi2.Response) bool { return v == nil })
|
||||
for _, response := range responses {
|
||||
maps.DeleteFunc(response.Headers, func(_ string, v *openapi2.Header) bool { return v == nil })
|
||||
}
|
||||
}
|
||||
|
||||
func decodeYAML(data []byte, target interface{}) error {
|
||||
var value interface{}
|
||||
if err := yaml.Unmarshal(data, &value); err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package swagger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
@@ -40,69 +39,3 @@ func TestSwaggerAPIParser(t *testing.T) {
|
||||
}
|
||||
require.ElementsMatch(t, gotMethodsToURLs, expectedURLs, "could not get swagger urls")
|
||||
}
|
||||
|
||||
func TestSwaggerParseNullEntries(t *testing.T) {
|
||||
// Each spec has a null map value or list item that decodes to a nil
|
||||
// pointer and used to panic inside openapi2conv.ToV3.
|
||||
specs := map[string]string{
|
||||
"null-path-item": `
|
||||
swagger: "2.0"
|
||||
info: {title: t, version: "1"}
|
||||
host: localhost
|
||||
paths:
|
||||
/x:
|
||||
`,
|
||||
"null-parameter": `
|
||||
swagger: "2.0"
|
||||
info: {title: t, version: "1"}
|
||||
host: localhost
|
||||
paths:
|
||||
/x:
|
||||
get:
|
||||
parameters:
|
||||
-
|
||||
responses: {"200": {description: ok}}
|
||||
`,
|
||||
"null-response": `
|
||||
swagger: "2.0"
|
||||
info: {title: t, version: "1"}
|
||||
host: localhost
|
||||
paths:
|
||||
/x:
|
||||
get:
|
||||
responses: {"200": }
|
||||
`,
|
||||
"null-response-header": `
|
||||
swagger: "2.0"
|
||||
info: {title: t, version: "1"}
|
||||
host: localhost
|
||||
responses:
|
||||
Shared:
|
||||
description: ok
|
||||
headers:
|
||||
X-Shared:
|
||||
paths:
|
||||
/x:
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
headers:
|
||||
X-Rate-Limit:
|
||||
`,
|
||||
}
|
||||
|
||||
for name, spec := range specs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var count int
|
||||
err := New().Parse(strings.NewReader(spec), func(request *types.RequestResponse) bool {
|
||||
count++
|
||||
return false
|
||||
}, "spec.yaml")
|
||||
require.NoError(t, err)
|
||||
if name != "null-path-item" {
|
||||
require.Equal(t, 1, count, "the operation should still produce a request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": { "name": "WebInspector", "version": "537.36" },
|
||||
"entries": [
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://ginandjuice.shop/catalog?category=Juice",
|
||||
"httpVersion": "http/2.0",
|
||||
"headers": [
|
||||
{ "name": ":authority", "value": "ginandjuice.shop" },
|
||||
{ "name": ":method", "value": "GET" },
|
||||
{ "name": ":path", "value": "/catalog?category=Juice" },
|
||||
{ "name": ":scheme", "value": "https" },
|
||||
{ "name": "accept", "value": "text/html" },
|
||||
{ "name": "cookie", "value": "session=abc123" }
|
||||
],
|
||||
"queryString": [{ "name": "category", "value": "Juice" }],
|
||||
"cookies": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": 0
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "http/2.0",
|
||||
"headers": [{ "name": "content-type", "value": "text/html; charset=utf-8" }],
|
||||
"content": { "size": 19, "mimeType": "text/html", "text": "PGgxPkp1aWNlPC9oMT4=", "encoding": "base64" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://ginandjuice.shop/login",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"headers": [
|
||||
{ "name": "Host", "value": "ginandjuice.shop" },
|
||||
{ "name": "Content-Type", "value": "application/x-www-form-urlencoded" }
|
||||
],
|
||||
"postData": { "mimeType": "application/x-www-form-urlencoded", "text": "username=carlos&password=hunter2" }
|
||||
},
|
||||
"response": {
|
||||
"status": 302,
|
||||
"statusText": "Found",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"headers": [{ "name": "Location", "value": "/my-account" }],
|
||||
"content": { "size": 0, "mimeType": "" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "data:image/png;base64,iVBORw0KGgo=",
|
||||
"httpVersion": "",
|
||||
"headers": []
|
||||
},
|
||||
"response": { "status": 200, "statusText": "", "httpVersion": "", "headers": [], "content": { "size": 0 } }
|
||||
},
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://ginandjuice.shop/blocked.js",
|
||||
"httpVersion": "",
|
||||
"headers": []
|
||||
},
|
||||
"response": { "status": 0, "statusText": "", "httpVersion": "", "headers": [], "content": { "size": 0 } }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/burp"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/har"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/json"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/openapi"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/raw"
|
||||
@@ -143,7 +142,6 @@ func (i *HttpInputProvider) Close() {}
|
||||
// Supported Providers
|
||||
var providersList = []formats.Format{
|
||||
burp.New(),
|
||||
har.New(),
|
||||
json.New(),
|
||||
yaml.New(),
|
||||
openapi.New(),
|
||||
|
||||
+138
-66
@@ -89,9 +89,8 @@ func New(opts *Options) (*ListInputProvider, error) {
|
||||
patterns: normalize.NewFingerprinter(options.MaxURLsPerPattern),
|
||||
ipOptions: &ipOptions{
|
||||
ScanAllIPs: options.ScanAllIPs,
|
||||
IPV4: len(options.IPVersion) == 0 || sliceutil.Contains(options.IPVersion, "4"),
|
||||
IPV6: len(options.IPVersion) == 0 || sliceutil.Contains(options.IPVersion, "6"),
|
||||
Auto: len(options.IPVersion) == 0,
|
||||
IPV4: sliceutil.Contains(options.IPVersion, "4"),
|
||||
IPV6: sliceutil.Contains(options.IPVersion, "6"),
|
||||
},
|
||||
excludedHosts: make(map[string]struct{}),
|
||||
profiles: targetprofile.NewRegistry(configuration.TemplatesDirectory(options, config.DefaultConfig)),
|
||||
@@ -169,61 +168,6 @@ func (i *ListInputProvider) TargetProfiles() *targetprofile.Registry {
|
||||
return i.profiles
|
||||
}
|
||||
|
||||
// selectedIPs returns address pins for a hostname. An empty pin delegates
|
||||
// DNS and connection fallback to fastdialer, which races IPv4 and IPv6.
|
||||
// Set and Del share this selection. With -iv omitted and without -sa, one
|
||||
// unpinned target is returned. -sa returns every selected address.
|
||||
func (i *ListInputProvider) selectedIPs(executionId, hostname string) []string {
|
||||
if i.ipOptions == nil || (!i.ipOptions.ScanAllIPs && (i.ipOptions.Auto || !i.ipOptions.IPV6)) {
|
||||
return []string{""}
|
||||
}
|
||||
dialers := protocolstate.GetDialersWithId(executionId)
|
||||
if dialers == nil {
|
||||
gologger.Warning().Msgf("cannot resolve %s: dialer %s not found", hostname, executionId)
|
||||
return nil
|
||||
}
|
||||
dnsData, err := dialers.Fastdialer.GetDNSData(hostname)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("DNS resolution failed for %s: %v", hostname, err)
|
||||
if i.ipOptions.IPV4 {
|
||||
return []string{""}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(dnsData.A) == 0 && len(dnsData.AAAA) == 0 {
|
||||
gologger.Warning().Msgf("DNS resolution returned no IP addresses for %s", hostname)
|
||||
if i.ipOptions.IPV4 {
|
||||
return []string{""}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if i.ipOptions.ScanAllIPs {
|
||||
var ips []string
|
||||
if i.ipOptions.IPV6 {
|
||||
ips = append(ips, dnsData.AAAA...)
|
||||
}
|
||||
if i.ipOptions.IPV4 {
|
||||
ips = append(ips, dnsData.A...)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
gologger.Warning().Msgf("no DNS addresses matching -iv selection for %s", hostname)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
var ips []string
|
||||
if i.ipOptions.IPV6 && len(dnsData.AAAA) > 0 {
|
||||
ips = append(ips, dnsData.AAAA[0])
|
||||
}
|
||||
// Explicit -iv 4,6 scans the first IPv6 address and one unpinned target.
|
||||
if i.ipOptions.IPV4 {
|
||||
ips = append(ips, "")
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
gologger.Warning().Msgf("no IPv6 DNS address found for %s", hostname)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// Set normalizes and stores passed input values
|
||||
func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
i.set(executionId, value, nil)
|
||||
@@ -258,11 +202,75 @@ func (i *ListInputProvider) set(executionId string, value string, selection *tar
|
||||
return
|
||||
}
|
||||
|
||||
for _, ip := range i.selectedIPs(executionId, urlx.Hostname()) {
|
||||
if i.ipOptions.ScanAllIPs {
|
||||
// scan all ips
|
||||
dialers := protocolstate.GetDialersWithId(executionId)
|
||||
if dialers == nil {
|
||||
gologger.Error().Msgf("dialers with executionId %s not found", executionId)
|
||||
return
|
||||
}
|
||||
|
||||
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
|
||||
if err == nil {
|
||||
if (len(dnsData.A) + len(dnsData.AAAA)) > 0 {
|
||||
var ips []string
|
||||
if i.ipOptions.IPV4 {
|
||||
ips = append(ips, dnsData.A...)
|
||||
}
|
||||
if i.ipOptions.IPV6 {
|
||||
ips = append(ips, dnsData.AAAA...)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.setItem(metaInput, selection)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
gologger.Debug().Msgf("scanAllIps: no ip's found reverting to default")
|
||||
}
|
||||
} else {
|
||||
// failed to scanallips falling back to defaults
|
||||
gologger.Debug().Msgf("scanAllIps: dns resolution failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ips := []string{}
|
||||
// only scan the target but ipv6 if it has one
|
||||
if i.ipOptions.IPV6 {
|
||||
dialers := protocolstate.GetDialersWithId(executionId)
|
||||
if dialers == nil {
|
||||
gologger.Error().Msgf("dialers with executionId %s not found", executionId)
|
||||
return
|
||||
}
|
||||
|
||||
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
|
||||
if err == nil && len(dnsData.AAAA) > 0 {
|
||||
// pick/ prefer 1st
|
||||
ips = append(ips, dnsData.AAAA[0])
|
||||
} else {
|
||||
gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %v\n", err)
|
||||
}
|
||||
}
|
||||
if i.ipOptions.IPV4 {
|
||||
// if IPV4 is enabled do not specify ip let dialer handle it
|
||||
ips = append(ips, "")
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.setItem(metaInput, selection)
|
||||
if ip != "" {
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.setItem(metaInput, selection)
|
||||
} else {
|
||||
metaInput.Input = URL
|
||||
i.setItem(metaInput, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,11 +493,75 @@ func (i *ListInputProvider) Del(executionId string, value string) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ip := range i.selectedIPs(executionId, urlx.Hostname()) {
|
||||
if i.ipOptions.ScanAllIPs {
|
||||
// scan all ips
|
||||
dialers := protocolstate.GetDialersWithId(executionId)
|
||||
if dialers == nil {
|
||||
gologger.Error().Msgf("dialers with executionId %s not found", executionId)
|
||||
return
|
||||
}
|
||||
|
||||
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
|
||||
if err == nil {
|
||||
if (len(dnsData.A) + len(dnsData.AAAA)) > 0 {
|
||||
var ips []string
|
||||
if i.ipOptions.IPV4 {
|
||||
ips = append(ips, dnsData.A...)
|
||||
}
|
||||
if i.ipOptions.IPV6 {
|
||||
ips = append(ips, dnsData.AAAA...)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = value
|
||||
metaInput.CustomIP = ip
|
||||
i.delItem(metaInput)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
gologger.Debug().Msgf("scanAllIps: no ip's found reverting to default")
|
||||
}
|
||||
} else {
|
||||
// failed to scanallips falling back to defaults
|
||||
gologger.Debug().Msgf("scanAllIps: dns resolution failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ips := []string{}
|
||||
// only scan the target but ipv6 if it has one
|
||||
if i.ipOptions.IPV6 {
|
||||
dialers := protocolstate.GetDialersWithId(executionId)
|
||||
if dialers == nil {
|
||||
gologger.Error().Msgf("dialers with executionId %s not found", executionId)
|
||||
return
|
||||
}
|
||||
|
||||
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
|
||||
if err == nil && len(dnsData.AAAA) > 0 {
|
||||
// pick/ prefer 1st
|
||||
ips = append(ips, dnsData.AAAA[0])
|
||||
} else {
|
||||
gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %v\n", err)
|
||||
}
|
||||
}
|
||||
if i.ipOptions.IPV4 {
|
||||
// if IPV4 is enabled do not specify ip let dialer handle it
|
||||
ips = append(ips, "")
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.delItem(metaInput)
|
||||
if ip != "" {
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.delItem(metaInput)
|
||||
} else {
|
||||
metaInput.Input = URL
|
||||
i.delItem(metaInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// A local deterministic DNS responder for dual-stack, A-only and AAAA-only hosts.
|
||||
type ipVersionDNSHandler struct{}
|
||||
|
||||
func (ipVersionDNSHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(r)
|
||||
if len(r.Question) == 0 {
|
||||
_ = w.WriteMsg(reply)
|
||||
return
|
||||
}
|
||||
question := r.Question[0]
|
||||
// Some resolvers try search suffixes before the absolute name. Match the
|
||||
// test host prefix rather than answering unrelated names as dual-stack.
|
||||
host := strings.ToLower(question.Name)
|
||||
if strings.HasPrefix(host, "empty.") {
|
||||
_ = w.WriteMsg(reply)
|
||||
return
|
||||
}
|
||||
isDual := strings.HasPrefix(host, "dual.")
|
||||
isV4 := strings.HasPrefix(host, "v4only.")
|
||||
isV6 := strings.HasPrefix(host, "v6only.")
|
||||
if question.Qtype == dns.TypeA && (isDual || isV4) {
|
||||
reply.Answer = append(reply.Answer, &dns.A{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, A: net.ParseIP("192.0.2.10")})
|
||||
}
|
||||
if question.Qtype == dns.TypeAAAA && (isDual || isV6) {
|
||||
reply.Answer = append(reply.Answer, &dns.AAAA{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60}, AAAA: net.ParseIP("2001:db8::10")})
|
||||
}
|
||||
_ = w.WriteMsg(reply)
|
||||
}
|
||||
|
||||
func TestIPVersionsDefaultAndExplicitSelection(t *testing.T) {
|
||||
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
started := make(chan struct{})
|
||||
server := &dns.Server{PacketConn: conn, Handler: ipVersionDNSHandler{}, NotifyStartedFunc: func() { close(started) }}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("DNS test resolver did not start")
|
||||
}
|
||||
opts := types.DefaultOptions()
|
||||
opts.ExecutionId = "ip-version-regression"
|
||||
opts.InternalResolversList = []string{conn.LocalAddr().String()}
|
||||
require.NoError(t, protocolstate.Init(opts))
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
for _, tc := range []struct {
|
||||
name, hostname string
|
||||
scanAll, ipv4, ipv6, auto bool
|
||||
want []string
|
||||
}{
|
||||
{"automatic dual-stack keeps dialer fallback", "dual.test", false, true, true, true, []string{""}},
|
||||
{"automatic IPv6-only keeps dialer fallback", "v6only.test", false, true, true, true, []string{""}},
|
||||
{"auto IPv4 fallback", "v4only.test", false, true, true, true, []string{""}},
|
||||
{"auto empty DNS response fallback", "empty.test", false, true, true, true, []string{""}},
|
||||
{"all empty DNS response fallback", "empty.test", true, true, true, true, []string{""}},
|
||||
{"all dual", "dual.test", true, true, true, true, []string{"192.0.2.10", "2001:db8::10"}},
|
||||
{"all IPv6 only", "v6only.test", true, true, true, true, []string{"2001:db8::10"}},
|
||||
{"all IPv4 only", "v4only.test", true, true, true, true, []string{"192.0.2.10"}},
|
||||
{"explicit IPv4 only", "dual.test", false, true, false, false, []string{""}},
|
||||
{"explicit IPv6 only", "dual.test", false, false, true, false, []string{"2001:db8::10"}},
|
||||
{"explicit both", "dual.test", false, true, true, false, []string{"2001:db8::10", ""}},
|
||||
{"all explicit IPv6", "dual.test", true, false, true, false, []string{"2001:db8::10"}},
|
||||
{"all explicit IPv4", "dual.test", true, true, false, false, []string{"192.0.2.10"}},
|
||||
{"IPv6 restricted on IPv4-only", "v4only.test", true, false, true, false, nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
|
||||
require.NoError(t, err)
|
||||
input := &ListInputProvider{
|
||||
hostMap: hm,
|
||||
excludedHosts: make(map[string]struct{}),
|
||||
ipOptions: &ipOptions{ScanAllIPs: tc.scanAll, IPV4: tc.ipv4, IPV6: tc.ipv6, Auto: tc.auto},
|
||||
}
|
||||
t.Cleanup(input.Close)
|
||||
input.Set(opts.ExecutionId, "https://"+tc.hostname)
|
||||
var got []string
|
||||
input.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
got = append(got, value.CustomIP)
|
||||
return true
|
||||
})
|
||||
require.ElementsMatch(t, tc.want, got)
|
||||
if tc.scanAll && len(tc.want) > 0 {
|
||||
input.Del(opts.ExecutionId, "https://"+tc.hostname)
|
||||
require.EqualValues(t, 0, input.Count(), "Del must remove the same IPs added by Set")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,4 @@ type ipOptions struct {
|
||||
ScanAllIPs bool
|
||||
IPV4 bool
|
||||
IPV6 bool
|
||||
Auto bool // -iv omitted: one unpinned target unless -sa scans every address
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package compiler_test
|
||||
|
||||
//nolint:depguard // No depguard rules are configured, so its default list allows only the standard library.
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// TestCloseWithoutConnection calls Close on clients that never connected.
|
||||
// The script must keep running past the call.
|
||||
func TestCloseWithoutConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scripts := map[string]string{
|
||||
"ssh": `
|
||||
const ssh = require('nuclei/ssh');
|
||||
ExportAs('closed', new ssh.SSHClient().Close());
|
||||
true;
|
||||
`,
|
||||
"net": `
|
||||
const net = require('nuclei/net');
|
||||
new net.NetConn().Close();
|
||||
ExportAs('closed', true);
|
||||
true;
|
||||
`,
|
||||
}
|
||||
for name, script := range scripts {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
program, err := compiler.SourceAutoMode(script, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := new(compiler.ExecuteOptions)
|
||||
opts.ExecutionId = t.Name()
|
||||
opts.Source = &script // ExportAs is defined only for sources that use it.
|
||||
opts.TimeoutVariants = types.NewTimeoutVariant(5)
|
||||
|
||||
result, err := compiler.New().ExecuteWithOptions(t.Context(), program, compiler.NewExecuteArgs(), opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := result["closed"]; got != "true" {
|
||||
t.Errorf(`result["closed"] = %v, want "true"`, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,22 +16,18 @@ import (
|
||||
syncutil "github.com/projectdiscovery/utils/sync"
|
||||
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libbytes"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libcompress"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libdcerpc"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libdcom"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libfs"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libgrpc"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libhash"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libhttp"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libikev2"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libipmi"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkerberos"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkrbforge"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkrbroast"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libldap"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmssql"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmysql"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmqtt"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libnet"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/liboracle"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libpop3"
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/klauspost/compress/zlib"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/projectdiscovery/dsl"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
httputil "github.com/projectdiscovery/utils/http"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const streamHashScript = `
|
||||
const net = require('nuclei/net');
|
||||
const compress = require('nuclei/compress');
|
||||
const hash = require('nuclei/hash');
|
||||
|
||||
const conn = net.Open('tcp', %q);
|
||||
conn.Send('GET / HTTP/1.1\r\nHost: test\r\nAccept-Encoding: gzip, deflate, br, zstd\r\n\r\n');
|
||||
let head = '';
|
||||
while (!head.endsWith('\r\n\r\n')) head += conn.RecvString(1);
|
||||
const header = (name) => (head.match(new RegExp('\r\n' + name + ': ([^\r]+)', 'i')) || [])[1];
|
||||
const length = header('Content-Length');
|
||||
const coding = [header('Content-Encoding'), header('Transfer-Encoding')].filter(Boolean).join(', ');
|
||||
|
||||
const d = compress.NewDecoder(coding);
|
||||
const sha = hash.New('sha256'), mmh3 = hash.New('mmh3');
|
||||
const take = (out) => { sha.Write(out); mmh3.Write(out); };
|
||||
if (length !== undefined) {
|
||||
for (let read = 0; read < parseInt(length); ) {
|
||||
const chunk = conn.Recv(65536);
|
||||
read += chunk.length;
|
||||
for (let out = d.Write(chunk); out.length > 0; out = d.Read()) take(out);
|
||||
}
|
||||
for (let out = d.Close(); out.length > 0; out = d.Read()) take(out);
|
||||
} else {
|
||||
while (!d.Finished()) {
|
||||
for (let out = d.Write(conn.Recv(65536)); out.length > 0; out = d.Read()) take(out);
|
||||
}
|
||||
}
|
||||
ExportAs('sha256', sha.Sum());
|
||||
ExportAs('mmh3', mmh3.Sum());
|
||||
true;
|
||||
`
|
||||
|
||||
func encodeBody(t *testing.T, encoding string, data []byte) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
var w io.WriteCloser
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
w = gzip.NewWriter(&buf)
|
||||
case "deflate":
|
||||
w = zlib.NewWriter(&buf)
|
||||
case "br":
|
||||
w = brotli.NewWriter(&buf)
|
||||
case "zstd":
|
||||
zw, err := zstd.NewWriter(&buf)
|
||||
require.NoError(t, err)
|
||||
w = zw
|
||||
case "":
|
||||
return data
|
||||
}
|
||||
_, err := w.Write(data)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.Close())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestStreamingHashMatchesHTTPEngine(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
var body []byte
|
||||
for len(body) < 6<<20 {
|
||||
body = append(body, "large compressed response body "...)
|
||||
body = append(body, byte(rng.Intn(256)))
|
||||
}
|
||||
|
||||
executionID := t.Name()
|
||||
require.NoError(t, protocolstate.Init(&types.Options{ExecutionId: executionID}))
|
||||
t.Cleanup(func() { protocolstate.Close(executionID) })
|
||||
|
||||
for _, tc := range []struct {
|
||||
encoding string
|
||||
chunked bool
|
||||
}{
|
||||
{"gzip", false},
|
||||
{"gzip", true},
|
||||
{"deflate", false},
|
||||
{"br", true},
|
||||
{"zstd", true},
|
||||
{"", true},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s chunked=%v", tc.encoding, tc.chunked), func(t *testing.T) {
|
||||
encoded := encodeBody(t, tc.encoding, body)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if tc.encoding != "" {
|
||||
w.Header().Set("Content-Encoding", tc.encoding)
|
||||
}
|
||||
if !tc.chunked {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(encoded)))
|
||||
_, _ = w.Write(encoded)
|
||||
return
|
||||
}
|
||||
// Flushing before the handler returns makes the body chunked.
|
||||
for rest := encoded; len(rest) > 0; {
|
||||
n := min(len(rest), 50000)
|
||||
_, _ = w.Write(rest[:n])
|
||||
w.(http.Flusher).Flush()
|
||||
rest = rest[n:]
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Transport: &http.Transport{DisableCompression: true}}
|
||||
resp, err := client.Get(server.URL)
|
||||
require.NoError(t, err)
|
||||
if tc.chunked {
|
||||
require.Equal(t, []string{"chunked"}, resp.TransferEncoding)
|
||||
}
|
||||
chain := httputil.NewResponseChain(resp, 64<<20)
|
||||
defer chain.Close()
|
||||
require.NoError(t, chain.Fill())
|
||||
require.Equal(t, len(body), chain.Body().Len(), "the engine must decode the whole body")
|
||||
engineBody := map[string]interface{}{"body": chain.BodyString()}
|
||||
wantSha, err := dsl.EvalExpr("sha256(body)", engineBody)
|
||||
require.NoError(t, err)
|
||||
wantMmh3, err := dsl.EvalExpr("mmh3(body)", engineBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
script := fmt.Sprintf(streamHashScript, serverURL.Host)
|
||||
compiled, err := SourceAutoMode(script, false)
|
||||
require.NoError(t, err)
|
||||
result, err := New().ExecuteWithOptions(t.Context(), compiled, NewExecuteArgs(), &ExecuteOptions{
|
||||
ExecutionId: executionID,
|
||||
Source: &script,
|
||||
TimeoutVariants: &types.Timeouts{JsCompilerExecutionTimeout: 20 * time.Second},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprint(wantSha), result["sha256"])
|
||||
require.Equal(t, fmt.Sprint(wantMmh3), result["mmh3"])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,14 @@ func NewEntityParser(dir string) (*EntityParser, error) {
|
||||
return parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||
},
|
||||
}
|
||||
|
||||
pkg, err := loadPackage(cfg, ".")
|
||||
pkgs, err := packages.Load(cfg, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) == 0 {
|
||||
return nil, errors.New("no packages found")
|
||||
}
|
||||
pkg := pkgs[0]
|
||||
|
||||
return &EntityParser{
|
||||
syntax: pkg.Syntax,
|
||||
@@ -510,7 +513,6 @@ func (p *EntityParser) extractVarsNConstants() {
|
||||
|
||||
// loadImportedPackages loads all imported packages
|
||||
func (p *EntityParser) loadImportedPackages() error {
|
||||
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
// get all import statements
|
||||
// iterate over all imports
|
||||
for _, file := range p.syntax {
|
||||
@@ -520,7 +522,7 @@ func (p *EntityParser) loadImportedPackages() error {
|
||||
// remove the quotes from the path
|
||||
path = path[1 : len(path)-1]
|
||||
// load the package
|
||||
pkg, err := loadPackage(cfg, path)
|
||||
pkg, err := loadPackage(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -541,31 +543,18 @@ func (p *EntityParser) loadImportedPackages() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadPackage loads the package that pattern names.
|
||||
//
|
||||
// packages.Load reports parse, type, and list errors in Package.Errors
|
||||
// rather than in its returned error, so loadPackage converts them into an
|
||||
// error. The parser assumes a complete syntax tree and type information.
|
||||
func loadPackage(cfg *packages.Config, pattern string) (*packages.Package, error) {
|
||||
pkgs, err := packages.Load(cfg, pattern)
|
||||
// Load the package containing the type definition
|
||||
// TODO: we don't support named imports yet
|
||||
func loadPackage(pkgPath string) (*packages.Package, error) {
|
||||
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
pkgs, err := packages.Load(cfg, pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) == 0 {
|
||||
return nil, errors.New("no packages found")
|
||||
}
|
||||
|
||||
pkg := pkgs[0]
|
||||
if len(pkg.Errors) > 0 {
|
||||
errs := make([]error, len(pkg.Errors))
|
||||
for i, e := range pkg.Errors {
|
||||
errs[i] = e
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("package %s: %w", pkg.ID, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return pkg, nil
|
||||
return pkgs[0], nil
|
||||
}
|
||||
|
||||
// exprToString converts an expression to a string
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package tsgen
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// writeModule writes src as the only package of a new Go module and
|
||||
// returns the module directory.
|
||||
func writeModule(t *testing.T, src string) string {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
files := map[string]string{
|
||||
"go.mod": "module example.com/tsgentest\n\ngo 1.21\n",
|
||||
"example.go": src,
|
||||
}
|
||||
|
||||
for name, content := range files {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600))
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestNewEntityParser(t *testing.T) {
|
||||
const src = `package example
|
||||
|
||||
import "strings"
|
||||
|
||||
// Upper returns s in upper case.
|
||||
func Upper(s string) string { return strings.ToUpper(s) }
|
||||
`
|
||||
|
||||
p, err := NewEntityParser(writeModule(t, src))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, p.Parse())
|
||||
|
||||
entities := p.GetEntities()
|
||||
|
||||
names := make([]string, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
names = append(names, e.Name)
|
||||
}
|
||||
|
||||
require.Contains(t, names, "Upper")
|
||||
}
|
||||
|
||||
func TestNewEntityParserPackageErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
}{
|
||||
{name: "syntax error", src: "package example\n\nfunc Broken( {\n\treturn 42\n}\n"},
|
||||
{name: "type error", src: "package example\n\nfunc Broken() int { return undefined }\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewEntityParser(writeModule(t, tt.src))
|
||||
require.ErrorContains(t, err, "example.go:")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadImportedPackagesMissing(t *testing.T) {
|
||||
const src = `package example; import "example.com/tsgentest/missing"`
|
||||
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "example.go", src, parser.ImportsOnly)
|
||||
require.NoError(t, err)
|
||||
p := &EntityParser{
|
||||
syntax: []*ast.File{file},
|
||||
imports: map[string]*packages.Package{},
|
||||
}
|
||||
|
||||
var pkgErr packages.Error
|
||||
|
||||
require.ErrorAs(t, p.loadImportedPackages(), &pkgErr)
|
||||
require.Contains(t, pkgErr.Msg, "example.com/tsgentest/missing")
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package compress
|
||||
|
||||
import (
|
||||
lib_compress "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/compress"
|
||||
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
var (
|
||||
module = gojs.NewGojaModule("nuclei/compress")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"NewDecoder": lib_compress.NewDecoder,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"Decoder": gojs.GetClassConstructor[lib_compress.Decoder](&lib_compress.Decoder{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) {
|
||||
module.Enable(runtime)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package hash
|
||||
|
||||
import (
|
||||
lib_hash "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/hash"
|
||||
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
var (
|
||||
module = gojs.NewGojaModule("nuclei/hash")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"New": lib_hash.New,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"Hash": gojs.GetClassConstructor[lib_hash.Hash](&lib_hash.Hash{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) {
|
||||
module.Enable(runtime)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package ipmi
|
||||
|
||||
import (
|
||||
lib_ipmi "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/ipmi"
|
||||
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
var (
|
||||
module = gojs.NewGojaModule("nuclei/ipmi")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"CheckRAKPPassword": lib_ipmi.CheckRAKPPassword,
|
||||
"FindRAKPPassword": lib_ipmi.FindRAKPPassword,
|
||||
"GetAuthCapabilities": lib_ipmi.GetAuthCapabilities,
|
||||
"GetCipherSuites": lib_ipmi.GetCipherSuites,
|
||||
"GetRAKPHash": lib_ipmi.GetRAKPHash,
|
||||
"GetSystemGUID": lib_ipmi.GetSystemGUID,
|
||||
"Ping": lib_ipmi.Ping,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"AuthCapabilities": gojs.GetClassConstructor[lib_ipmi.AuthCapabilities](&lib_ipmi.AuthCapabilities{}),
|
||||
"Client": gojs.GetClassConstructor[lib_ipmi.Client](&lib_ipmi.Client{}),
|
||||
"CommandResponse": gojs.GetClassConstructor[lib_ipmi.CommandResponse](&lib_ipmi.CommandResponse{}),
|
||||
"RAKPHash": gojs.GetClassConstructor[lib_ipmi.RAKPHash](&lib_ipmi.RAKPHash{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) {
|
||||
module.Enable(runtime)
|
||||
}
|
||||
@@ -53,12 +53,13 @@ func init() {
|
||||
"FilterWorkstationTrustAccount": lib_ldap.FilterWorkstationTrustAccount,
|
||||
|
||||
// Objects / Classes
|
||||
"Client": lib_ldap.NewClient,
|
||||
"Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}),
|
||||
"LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}),
|
||||
"LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}),
|
||||
"Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}),
|
||||
"SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}),
|
||||
"AuthenticateOptions": gojs.GetClassConstructor[lib_ldap.AuthenticateOptions](&lib_ldap.AuthenticateOptions{}),
|
||||
"Client": lib_ldap.NewClient,
|
||||
"Config": gojs.GetClassConstructor[lib_ldap.Config](&lib_ldap.Config{}),
|
||||
"LdapAttributes": gojs.GetClassConstructor[lib_ldap.LdapAttributes](&lib_ldap.LdapAttributes{}),
|
||||
"LdapEntry": gojs.GetClassConstructor[lib_ldap.LdapEntry](&lib_ldap.LdapEntry{}),
|
||||
"Metadata": gojs.GetClassConstructor[lib_ldap.Metadata](&lib_ldap.Metadata{}),
|
||||
"SearchResult": gojs.GetClassConstructor[lib_ldap.SearchResult](&lib_ldap.SearchResult{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
lib_mqtt "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/mqtt"
|
||||
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
var (
|
||||
module = gojs.NewGojaModule("nuclei/mqtt")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"IsMQTT": lib_mqtt.IsMQTT,
|
||||
"IsAnonymousAllowed": lib_mqtt.IsAnonymousAllowed,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"Client": gojs.GetClassConstructor[lib_mqtt.Client](&lib_mqtt.Client{}),
|
||||
"ConnectOptions": gojs.GetClassConstructor[lib_mqtt.ConnectOptions](&lib_mqtt.ConnectOptions{}),
|
||||
"ConnectResult": gojs.GetClassConstructor[lib_mqtt.ConnectResult](&lib_mqtt.ConnectResult{}),
|
||||
"Message": gojs.GetClassConstructor[lib_mqtt.Message](&lib_mqtt.Message{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) {
|
||||
module.Enable(runtime)
|
||||
}
|
||||
@@ -19,8 +19,9 @@ func init() {
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}),
|
||||
"MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}),
|
||||
"MSSQLClient": gojs.GetClassConstructor[lib_mssql.MSSQLClient](&lib_mssql.MSSQLClient{}),
|
||||
"MSSQLInfo": gojs.GetClassConstructor[lib_mssql.MSSQLInfo](&lib_mssql.MSSQLInfo{}),
|
||||
"MSSQLOptions": gojs.GetClassConstructor[lib_mssql.MSSQLOptions](&lib_mssql.MSSQLOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func init() {
|
||||
// Objects / Classes
|
||||
"IsOracleResponse": gojs.GetClassConstructor[lib_oracle.IsOracleResponse](&lib_oracle.IsOracleResponse{}),
|
||||
"OracleClient": gojs.GetClassConstructor[lib_oracle.OracleClient](&lib_oracle.OracleClient{}),
|
||||
"OracleOptions": gojs.GetClassConstructor[lib_oracle.OracleOptions](&lib_oracle.OracleOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ func init() {
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}),
|
||||
"PGClient": gojs.GetClassConstructor[lib_postgres.PGClient](&lib_postgres.PGClient{}),
|
||||
"PostgresOptions": gojs.GetClassConstructor[lib_postgres.PostgresOptions](&lib_postgres.PostgresOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -15,16 +15,17 @@ func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"Connect": lib_redis.Connect,
|
||||
"GetServerInfo": lib_redis.GetServerInfo,
|
||||
"GetServerInfoAuth": lib_redis.GetServerInfoAuth,
|
||||
"IsAuthenticated": lib_redis.IsAuthenticated,
|
||||
"RunLuaScript": lib_redis.RunLuaScript,
|
||||
"Connect": lib_redis.Connect, //nolint:staticcheck // SA1019: retained for JS API compatibility
|
||||
"ConnectWithOptions": lib_redis.ConnectWithOptions,
|
||||
"GetServerInfo": lib_redis.GetServerInfo,
|
||||
"GetServerInfoAuth": lib_redis.GetServerInfoAuth,
|
||||
"IsAuthenticated": lib_redis.IsAuthenticated,
|
||||
"RunLuaScript": lib_redis.RunLuaScript,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
|
||||
"RedisOptions": gojs.GetClassConstructor[lib_redis.RedisOptions](&lib_redis.RedisOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ func init() {
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}),
|
||||
"SMBClient": gojs.GetClassConstructor[lib_smb.SMBClient](&lib_smb.SMBClient{}),
|
||||
"SMBOptions": gojs.GetClassConstructor[lib_smb.SMBOptions](&lib_smb.SMBOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ func init() {
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}),
|
||||
"SSHClient": gojs.GetClassConstructor[lib_ssh.SSHClient](&lib_ssh.SSHClient{}),
|
||||
"SSHOptions": gojs.GetClassConstructor[lib_ssh.SSHOptions](&lib_ssh.SSHOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func init() {
|
||||
"IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}),
|
||||
"TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}),
|
||||
"TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}),
|
||||
"TelnetOptions": gojs.GetClassConstructor[lib_telnet.TelnetOptions](&lib_telnet.TelnetOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func init() {
|
||||
// Objects / Classes
|
||||
"IsVNCResponse": gojs.GetClassConstructor[lib_vnc.IsVNCResponse](&lib_vnc.IsVNCResponse{}),
|
||||
"VNCClient": gojs.GetClassConstructor[lib_vnc.VNCClient](&lib_vnc.VNCClient{}),
|
||||
"VNCOptions": gojs.GetClassConstructor[lib_vnc.VNCOptions](&lib_vnc.VNCOptions{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
|
||||
|
||||
/**
|
||||
* NewDecoder returns a decoder for gzip, deflate, br, zstd, chunked or
|
||||
* identity. List several comma-separated, in the order they were applied.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const d = compress.NewDecoder('br');
|
||||
* // Content-Encoding: gzip sent with Transfer-Encoding: chunked
|
||||
* const c = compress.NewDecoder('gzip, chunked');
|
||||
* ```
|
||||
*/
|
||||
export function NewDecoder(encoding: string): Decoder | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Decoder decodes a compressed stream written to it in chunks. Each call
|
||||
* returns up to about 1 MB; call Read until it returns nothing before the
|
||||
* next Write.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const hash = require('nuclei/hash');
|
||||
* const d = compress.NewDecoder('gzip');
|
||||
* const h = hash.New('sha256');
|
||||
* for (const chunk of chunks) {
|
||||
* for (let out = d.Write(chunk); out.length > 0; out = d.Read()) h.Write(out);
|
||||
* }
|
||||
* for (let out = d.Close(); out.length > 0; out = d.Read()) h.Write(out);
|
||||
* log(h.Sum());
|
||||
* ```
|
||||
*/
|
||||
export class Decoder {
|
||||
|
||||
|
||||
// Constructor of Decoder
|
||||
constructor() {}
|
||||
/**
|
||||
* Write decodes the next chunk and returns up to about 1 MB of output.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const d = compress.NewDecoder('gzip');
|
||||
* const out = d.Write(chunk);
|
||||
* ```
|
||||
*/
|
||||
public Write(chunk: Uint8Array): Uint8Array | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read returns more decoded bytes, or none once the decoder needs input.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const d = compress.NewDecoder('gzip');
|
||||
* for (let out = d.Write(chunk); out.length > 0; out = d.Read()) log(out.length);
|
||||
* ```
|
||||
*/
|
||||
public Read(): Uint8Array | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close ends the input and returns the last decoded bytes. A truncated
|
||||
* stream errors once the bytes before the cut have been returned.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const d = compress.NewDecoder('gzip');
|
||||
* d.Write(chunk);
|
||||
* for (let out = d.Close(); out.length > 0; out = d.Read()) log(out.length);
|
||||
* ```
|
||||
*/
|
||||
public Close(): Uint8Array | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finished reports whether a stream that marks its own end, such as a
|
||||
* chunked body, has ended and all its output was read.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const compress = require('nuclei/compress');
|
||||
* const d = compress.NewDecoder('gzip, chunked');
|
||||
* while (!d.Finished()) for (let out = d.Write(conn.Recv(65536)); out.length > 0; out = d.Read()) log(out.length);
|
||||
* ```
|
||||
*/
|
||||
public Finished(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
|
||||
|
||||
/**
|
||||
* New returns a running hash for md5, sha1, sha256, sha512 or mmh3. Sum
|
||||
* matches the DSL function of the same name.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const hash = require('nuclei/hash');
|
||||
* const h = hash.New('sha256');
|
||||
* ```
|
||||
*/
|
||||
export function New(algorithm: string): Hash | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hash is a running hash.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const hash = require('nuclei/hash');
|
||||
* const h = hash.New('sha256');
|
||||
* h.WriteString('hello');
|
||||
* log(h.Sum());
|
||||
* ```
|
||||
*/
|
||||
export class Hash {
|
||||
|
||||
|
||||
// Constructor of Hash
|
||||
constructor() {}
|
||||
/**
|
||||
* Write adds data to the hash.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const hash = require('nuclei/hash');
|
||||
* const h = hash.New('sha256');
|
||||
* h.Write([104, 105]);
|
||||
* ```
|
||||
*/
|
||||
public Write(data: Uint8Array): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* WriteString adds the string data to the hash.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const hash = require('nuclei/hash');
|
||||
* const h = hash.New('sha256');
|
||||
* h.WriteString('hi');
|
||||
* ```
|
||||
*/
|
||||
public WriteString(data: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sum returns the hash of the data written so far, formatted as the DSL does.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const hash = require('nuclei/hash');
|
||||
* const h = hash.New('mmh3');
|
||||
* h.WriteString('hello');
|
||||
* log(h.Sum());
|
||||
* ```
|
||||
*/
|
||||
public Sum(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
export * as bytes from './bytes';
|
||||
export * as compress from './compress';
|
||||
export * as dcom from './dcom';
|
||||
export * as dcerpc from './dcerpc';
|
||||
export * as fs from './fs';
|
||||
export * as goconsole from './goconsole';
|
||||
export * as grpc from './grpc';
|
||||
export * as hash from './hash';
|
||||
export * as http from './http';
|
||||
export * as ikev2 from './ikev2';
|
||||
export * as ipmi from './ipmi';
|
||||
export * as kerberos from './kerberos';
|
||||
export * as krbforge from './krbforge';
|
||||
export * as krbroast from './krbroast';
|
||||
export * as ldap from './ldap';
|
||||
export * as mssql from './mssql';
|
||||
export * as mysql from './mysql';
|
||||
export * as mqtt from './mqtt';
|
||||
export * as net from './net';
|
||||
export * as oracle from './oracle';
|
||||
export * as pop3 from './pop3';
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
|
||||
|
||||
/**
|
||||
* CheckRAKPPassword reports whether password produced hash. It runs offline,
|
||||
* so it can test any number of passwords without touching the BMC.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const hash = ipmi.GetRAKPHash('acme.com', 623, 'ADMIN');
|
||||
* log(ipmi.CheckRAKPPassword(hash, 'ADMIN'));
|
||||
* ```
|
||||
*/
|
||||
export function CheckRAKPPassword(hash: RAKPHash, password: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* FindRAKPPassword returns the first of passwords that produced hash, or an
|
||||
* empty string.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const hash = ipmi.GetRAKPHash('acme.com', 623, 'root');
|
||||
* log(ipmi.FindRAKPPassword(hash, ['calvin', 'root', 'admin']));
|
||||
* ```
|
||||
*/
|
||||
export function FindRAKPPassword(hash: RAKPHash, passwords: string[]): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GetAuthCapabilities returns the authentication capabilities the BMC
|
||||
* advertises on the current channel.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const caps = ipmi.GetAuthCapabilities('acme.com', 623);
|
||||
* log(caps.AnonymousLogin);
|
||||
* ```
|
||||
*/
|
||||
export function GetAuthCapabilities(ctx: any, host: string, port: number): AuthCapabilities | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GetCipherSuites returns the IDs of the RMCP+ cipher suites the BMC
|
||||
* offers. Cipher suite 0 authenticates nothing, so any password logs in.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const suites = ipmi.GetCipherSuites('acme.com', 623);
|
||||
* log(suites.includes(0));
|
||||
* ```
|
||||
*/
|
||||
export function GetCipherSuites(ctx: any, host: string, port: number): number[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GetRAKPHash starts an RMCP+ session as username and returns the HMAC the
|
||||
* BMC answers with. The session is never completed, so no login is
|
||||
* attempted; the BMC drops the half-open session after its timeout. It fails
|
||||
* when the BMC rejects the username.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const hash = ipmi.GetRAKPHash('acme.com', 623, 'ADMIN');
|
||||
* log(ipmi.CheckRAKPPassword(hash, 'ADMIN'));
|
||||
* ```
|
||||
*/
|
||||
export function GetRAKPHash(ctx: any, host: string, port: number, username: string): RAKPHash | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GetSystemGUID returns the BMC system GUID, which vendors derive from
|
||||
* serial numbers or MAC addresses.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const guid = ipmi.GetSystemGUID('acme.com', 623);
|
||||
* log(guid);
|
||||
* ```
|
||||
*/
|
||||
export function GetSystemGUID(ctx: any, host: string, port: number): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Ping reports whether the host answers an RMCP presence ping and advertises
|
||||
* IPMI support.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const isIPMI = ipmi.Ping('acme.com', 623);
|
||||
* log(isIPMI);
|
||||
* ```
|
||||
*/
|
||||
export function Ping(ctx: any, host: string, port: number): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Client holds one RMCP+ session, for commands that need a login.
|
||||
* Close it when done: BMCs allow only a few sessions at a time.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const client = new ipmi.Client();
|
||||
* client.Login('acme.com', 623, 'ADMIN', 'ADMIN', 3);
|
||||
* const id = client.Command(0x06, 0x01, []);
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
export class Client {
|
||||
|
||||
|
||||
// Constructor of Client
|
||||
constructor() {}
|
||||
/**
|
||||
* Login opens an RMCP+ session as username with the given cipher suite
|
||||
* (0, 1, 2, 3 or 6). Cipher suite 0 skips authentication, so a BMC
|
||||
* that allows it accepts any password. A rejected login is an error.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const client = new ipmi.Client();
|
||||
* client.Login('acme.com', 623, 'ADMIN', 'not-the-password', 0);
|
||||
* ```
|
||||
*/
|
||||
public Login(ctx: any, host: string, port: number, username: string, password: string, cipherSuite: number): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Command sends an IPMI request in the session, such as Get Device ID
|
||||
* (netFn 0x06, cmd 0x01) or Set Session Privilege Level (0x06, 0x3b).
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const client = new ipmi.Client();
|
||||
* client.Login('acme.com', 623, 'ADMIN', 'ADMIN', 3);
|
||||
* const admin = client.Command(0x06, 0x3b, [0x04]).CompletionCode == 0;
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
public Command(netFn: number, cmd: number, data: Uint8Array): CommandResponse | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close ends the session on the BMC, freeing its slot.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const client = new ipmi.Client();
|
||||
* client.Login('acme.com', 623, 'ADMIN', 'ADMIN', 3);
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
public Close(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* AuthCapabilities is what a BMC advertises before any login, from Get
|
||||
* Channel Authentication Capabilities.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const caps = ipmi.GetAuthCapabilities('acme.com', 623);
|
||||
* log(toJSON(caps));
|
||||
* ```
|
||||
*/
|
||||
export interface AuthCapabilities {
|
||||
|
||||
/**
|
||||
* IPMI20 is true when the BMC supports IPMI 2.0 (RMCP+).
|
||||
*/
|
||||
|
||||
IPMI20?: boolean,
|
||||
|
||||
/**
|
||||
* AuthTypes are the IPMI 1.5 authentication types the channel
|
||||
* accepts: none, md2, md5, straight (cleartext password), oem.
|
||||
*/
|
||||
|
||||
AuthTypes?: string[],
|
||||
|
||||
/**
|
||||
* AnonymousLogin is true when a null username with a null password
|
||||
* is accepted.
|
||||
*/
|
||||
|
||||
AnonymousLogin?: boolean,
|
||||
|
||||
/**
|
||||
* NullUsernames is true when accounts with an empty name are enabled.
|
||||
*/
|
||||
|
||||
NullUsernames?: boolean,
|
||||
|
||||
/**
|
||||
* NonNullUsernames is true when named accounts are enabled.
|
||||
*/
|
||||
|
||||
NonNullUsernames?: boolean,
|
||||
|
||||
/**
|
||||
* PerMessageAuth is false when the BMC skips authenticating
|
||||
* messages after login.
|
||||
*/
|
||||
|
||||
PerMessageAuth?: boolean,
|
||||
|
||||
/**
|
||||
* UserLevelAuth is false when user-level commands skip
|
||||
* authentication.
|
||||
*/
|
||||
|
||||
UserLevelAuth?: boolean,
|
||||
|
||||
/**
|
||||
* OEMID is the IANA enterprise number of the BMC vendor, or 0.
|
||||
*/
|
||||
|
||||
OEMID?: number,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* CommandResponse is the reply to an IPMI command.
|
||||
*/
|
||||
export interface CommandResponse {
|
||||
|
||||
/**
|
||||
* CompletionCode is 0 on success.
|
||||
*/
|
||||
|
||||
CompletionCode?: number,
|
||||
|
||||
Data?: Uint8Array,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* RAKPHash is the HMAC a BMC returns for a user during RMCP+ session
|
||||
* setup (CVE-2013-4786). It can be checked offline against candidate
|
||||
* passwords, which tests credentials without a login attempt.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const ipmi = require('nuclei/ipmi');
|
||||
* const hash = ipmi.GetRAKPHash('acme.com', 623, 'ADMIN');
|
||||
* log(hash.Hashcat);
|
||||
* ```
|
||||
*/
|
||||
export interface RAKPHash {
|
||||
|
||||
Username?: string,
|
||||
|
||||
/**
|
||||
* Algorithm is the HMAC the BMC used: sha1 or md5.
|
||||
*/
|
||||
|
||||
Algorithm?: string,
|
||||
|
||||
/**
|
||||
* Salt and HMAC are hex encoded; Salt is everything the HMAC covers.
|
||||
*/
|
||||
|
||||
Salt?: string,
|
||||
|
||||
HMAC?: string,
|
||||
|
||||
/**
|
||||
* Hashcat is the hash in hashcat mode 7300 format, for sha1 only.
|
||||
*/
|
||||
|
||||
Hashcat?: string,
|
||||
}
|
||||
|
||||
@@ -404,6 +404,14 @@ export class Client {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AuthenticateWithOptions authenticates using a password or an NTLM hash.
|
||||
*/
|
||||
public AuthenticateWithOptions(opts: AuthenticateOptions): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash
|
||||
* @example
|
||||
@@ -507,6 +515,21 @@ export interface Config {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* AuthenticateOptions defines credentials for LDAP authentication. Hash takes
|
||||
* precedence over Password when both are provided.
|
||||
*/
|
||||
export interface AuthenticateOptions {
|
||||
|
||||
Username?: string,
|
||||
|
||||
Password?: string,
|
||||
|
||||
Hash?: string,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* LdapAttributes represents all LDAP attributes of a particular
|
||||
* ldap entry
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
|
||||
/**
|
||||
* IsMQTT checks whether the target host:port is running an MQTT broker.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const isMqtt = mqtt.IsMQTT('acme.com', 1883);
|
||||
* log(isMqtt);
|
||||
* ```
|
||||
*/
|
||||
export function IsMQTT(host: string, port: number): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* IsAnonymousAllowed checks whether an MQTT broker permits unauthenticated / anonymous login.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const allowed = mqtt.IsAnonymousAllowed('acme.com', 1883);
|
||||
* log(allowed);
|
||||
* ```
|
||||
*/
|
||||
export function IsAnonymousAllowed(host: string, port: number): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message represents an MQTT published message.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* client.Connect('broker.hivemq.com', 1883);
|
||||
* const msgs = client.Subscribe('test/topic', 2);
|
||||
* log(msgs[0].Topic, msgs[0].Payload);
|
||||
* ```
|
||||
*/
|
||||
export interface Message {
|
||||
Topic?: string;
|
||||
Payload?: string;
|
||||
QoS?: number;
|
||||
Retain?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectOptions contains connection options for an MQTT broker.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const opts = new mqtt.ConnectOptions();
|
||||
* opts.Username = 'admin';
|
||||
* opts.Password = 'admin';
|
||||
* opts.CleanSession = true;
|
||||
* ```
|
||||
*/
|
||||
export interface ConnectOptions {
|
||||
Username?: string;
|
||||
Password?: string;
|
||||
ClientID?: string;
|
||||
CleanSession?: boolean;
|
||||
KeepAlive?: number;
|
||||
TLS?: boolean;
|
||||
InsecureSkipVerify?: boolean;
|
||||
Timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectResult is the response returned by an MQTT connection attempt.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* const res = client.Connect('acme.com', 1883);
|
||||
* log(res.Success, res.ReturnCode, res.Reason);
|
||||
* ```
|
||||
*/
|
||||
export interface ConnectResult {
|
||||
Success?: boolean;
|
||||
ReturnCode?: number;
|
||||
Reason?: string;
|
||||
SessionPresent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client is an MQTT client for testing MQTT broker misconfigurations and authentication.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* const res = client.Connect('acme.com', 1883);
|
||||
* if (res.Success) {
|
||||
* const msgs = client.Subscribe('#', 2);
|
||||
* client.Publish('nuclei/probe', 'test');
|
||||
* }
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
export class Client {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Connect connects to an MQTT broker using optional credentials and settings.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* const res = client.Connect('acme.com', 1883);
|
||||
* log(res.Success);
|
||||
* ```
|
||||
*/
|
||||
public Connect(host: string, port: number, options?: ConnectOptions): ConnectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectAuth connects to an MQTT broker with a username and password.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* const res = client.ConnectAuth('acme.com', 1883, 'admin', 'admin');
|
||||
* log(res.Success);
|
||||
* ```
|
||||
*/
|
||||
public ConnectAuth(host: string, port: number, username: string, password: string): ConnectResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe subscribes to a topic filter and collects received messages up to timeoutSec.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* client.Connect('acme.com', 1883);
|
||||
* const msgs = client.Subscribe('#', 3);
|
||||
* for (const m of msgs) {
|
||||
* log(m.Topic, m.Payload);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public Subscribe(topic: string, timeoutSec?: number): Message[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish publishes a message payload to a topic.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* client.Connect('acme.com', 1883);
|
||||
* const ok = client.Publish('nuclei/test', 'probe payload');
|
||||
* log(ok);
|
||||
* ```
|
||||
*/
|
||||
public Publish(topic: string, payload: string): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping sends an MQTT PINGREQ and awaits PINGRESP.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* client.Connect('acme.com', 1883);
|
||||
* const alive = client.Ping();
|
||||
* log(alive);
|
||||
* ```
|
||||
*/
|
||||
public Ping(): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close cleanly disconnects and closes the connection.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const mqtt = require('nuclei/mqtt');
|
||||
* const client = new mqtt.Client();
|
||||
* client.Connect('acme.com', 1883);
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
public Close(): error | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,14 @@ export class MSSQLClient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ConnectWithOptions connects to MS SQL using the supplied connection options.
|
||||
*/
|
||||
public ConnectWithOptions(opts: MSSQLOptions): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* IsMssql checks if the given host is running MS SQL database.
|
||||
* If the host is running MS SQL database, it returns true.
|
||||
@@ -95,6 +103,26 @@ export class MSSQLClient {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* MSSQLOptions defines the connection options for an MS SQL database.
|
||||
*/
|
||||
export interface MSSQLOptions {
|
||||
|
||||
Host?: string,
|
||||
|
||||
Port?: number,
|
||||
|
||||
Username?: string,
|
||||
|
||||
Password?: string,
|
||||
|
||||
DbName?: string,
|
||||
|
||||
Timeout?: number,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SQLResult Interface
|
||||
*/
|
||||
|
||||
@@ -63,6 +63,14 @@ export class MySQLClient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ConnectWithOptions connects to MySQL using the supplied connection options.
|
||||
*/
|
||||
public ConnectWithOptions(opts: MySQLOptions): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns MySQLInfo when fingerprint is successful
|
||||
* @example
|
||||
|
||||
@@ -46,7 +46,6 @@ export class NetConn {
|
||||
constructor() {}
|
||||
/**
|
||||
* Close closes the connection.
|
||||
* Close does nothing if no connection is open.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const net = require('nuclei/net');
|
||||
|
||||
@@ -41,6 +41,13 @@ export class OracleClient {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectWithOptions connects to Oracle using the supplied connection options.
|
||||
*/
|
||||
public ConnectWithOptions(opts: OracleOptions): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectWithDSN connects to an Oracle database using a DSN string
|
||||
* @example
|
||||
@@ -96,6 +103,17 @@ export class OracleClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OracleOptions defines the connection options for an Oracle database.
|
||||
*/
|
||||
export interface OracleOptions {
|
||||
Host?: string,
|
||||
Port?: number,
|
||||
ServiceName?: string,
|
||||
Username?: string,
|
||||
Password?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLResult Interface
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* PGClient is a client for Postgres database.
|
||||
* Internally client uses lib/pq driver.
|
||||
* Internally client uses go-pg/pg driver.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const postgres = require('nuclei/postgres');
|
||||
@@ -46,6 +46,14 @@ export class PGClient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ConnectWithOptions connects to Postgres using the supplied connection options.
|
||||
*/
|
||||
public ConnectWithOptions(opts: PostgresOptions): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ExecuteQuery connects to Postgres database using given credentials and database name.
|
||||
* and executes a query on the db.
|
||||
@@ -84,6 +92,28 @@ export class PGClient {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* PostgresOptions defines the connection options for a Postgres database.
|
||||
*/
|
||||
export interface PostgresOptions {
|
||||
|
||||
Host?: string,
|
||||
|
||||
Port?: number,
|
||||
|
||||
Username?: string,
|
||||
|
||||
Password?: string,
|
||||
|
||||
DbName?: string,
|
||||
|
||||
Timeout?: number,
|
||||
|
||||
SSLMode?: string,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SQLResult Interface
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,43 @@ export function Connect(host: string, port: number, password: string): boolean |
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* ConnectWithOptions tries to connect to Redis using the supplied options.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const redis = require('nuclei/redis');
|
||||
* const opts = new redis.RedisOptions();
|
||||
* opts.Host = 'acme.com';
|
||||
* opts.Port = 6379;
|
||||
* opts.Password = 'password';
|
||||
* opts.DB = 1;
|
||||
* const connected = redis.ConnectWithOptions(opts);
|
||||
* ```
|
||||
*/
|
||||
export function ConnectWithOptions(opts: RedisOptions): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* RedisOptions defines the connection options for a Redis server.
|
||||
*/
|
||||
export interface RedisOptions {
|
||||
|
||||
Host?: string,
|
||||
|
||||
Port?: number,
|
||||
|
||||
Password?: string,
|
||||
|
||||
DB?: number,
|
||||
|
||||
Timeout?: number,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* GetServerInfo returns the server info for a redis server
|
||||
* @example
|
||||
|
||||
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user