mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 04:09:43 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaef31a5ac |
No files matched your search
@@ -21,6 +21,7 @@ jobs:
|
||||
# in parallel once it passes instead of queueing behind the unit tests
|
||||
lint:
|
||||
name: "Lint"
|
||||
if: ${{ !endsWith(github.actor, '[bot]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
name: 🔤 Typos
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["dev"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
name: "Spell Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/[email protected]
|
||||
name: 🔤 Typos
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["dev"]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
name: "Spell Check"
|
||||
if: ${{ !endsWith(github.actor, '[bot]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/[email protected]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+21
-13
@@ -1,13 +1,13 @@
|
||||
<!-- Generated by securitycontext.dev. Source HEAD: df57ae6d2955864fd92a1bd2565fe9e4e9a7592b. -->
|
||||
<!-- Generated by securitycontext.dev. Source HEAD: 40d08ee493f6f84c9a6b3c7f15f22da83853130c. -->
|
||||
# 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 [df57ae6d29][c-df57ae6d29] on `dev` · 5174 commits, 548 security-relevant examined · generated 2026-11-02_
|
||||
_Analyzed [40d08ee493][c-40d08ee493] on `dev` · 5242 commits, 586 security-relevant examined · generated 2026-11-09_
|
||||
|
||||
## Recurring Security Patterns
|
||||
|
||||
**7.1% of classified fixes repeat a previously fixed pattern** (1 recurring, 13 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
|
||||
**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.
|
||||
|
||||
## Top Risks
|
||||
|
||||
@@ -15,12 +15,12 @@ Components with the strongest concentration of past fixes for one class.
|
||||
|
||||
| # | Class | Component | Severity | Fixes | Evidence |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | SSRF | `pkg/protocols/common/protocolstate/headless.go` | high | 2 | [d43c1f547f][c-d43c1f547f], [d9dabbb4a1][c-d9dabbb4a1] |
|
||||
| 2 | Arbitrary File Access | `v2/pkg/protocols/common/generators/load.go` | high | 0 (1 component gone, not counted) | [e5154d362a][c-e5154d362a] |
|
||||
| 3 | Arbitrary File Access | `pkg/js/libs/mysql/mysql.go` | high | 1 | [634cecbb28][c-634cecbb28] |
|
||||
| 4 | Code Execution | `pkg/fuzz/execute.go` | high | 1 | [ccbfb12bd0][c-ccbfb12bd0] |
|
||||
| 5 | Code Execution | `pkg/protocols/common/expressions/expressions.go` | high | 1 | [d221732016][c-d221732016] |
|
||||
| 6 | Information Disclosure | `internal/httpapi/apiendpoint.go` | high | 1 | [a20aa26024][c-a20aa26024] |
|
||||
| 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] |
|
||||
|
||||
## Fixed-Issue Fingerprints
|
||||
|
||||
@@ -28,8 +28,13 @@ Real security fixes mined from git history; use them as regression checks. `veri
|
||||
|
||||
**By component** (directories with the most fixes):
|
||||
|
||||
- `pkg/fuzz` — 2 fixes: Code Execution (2)
|
||||
- `pkg/protocols/common/protocolstate` — 2 fixes: SSRF (2)
|
||||
|
||||
- [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
|
||||
@@ -63,11 +68,12 @@ Real security fixes mined from git history; use them as regression checks. `veri
|
||||
|
||||
| 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 |
|
||||
| Code Execution | 2 | 2 | [2026-06-30][c-ccbfb12bd0] ccbfb12bd0 |
|
||||
| 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 |
|
||||
@@ -100,15 +106,17 @@ Published elsewhere, fixed before the history window, or not yet linked; full de
|
||||
| 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-df57ae6d29]: https://github.com/projectdiscovery/nuclei/commit/df57ae6d2955864fd92a1bd2565fe9e4e9a7592b
|
||||
[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-ccbfb12bd0]: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6
|
||||
[c-82d5a8acc2]: https://github.com/projectdiscovery/nuclei/commit/82d5a8acc2c756394c7fa92eae08b41330e89d69
|
||||
[c-d221732016]: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3
|
||||
[c-a20aa26024]: https://github.com/projectdiscovery/nuclei/commit/a20aa260245e5db889c5a421d4a65aeb2a64bf46
|
||||
[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
|
||||
|
||||
@@ -313,7 +313,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"),
|
||||
|
||||
@@ -18,7 +18,7 @@ require (
|
||||
github.com/alecthomas/chroma/v2 v2.27.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/andybalholm/brotli v1.2.0
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.7
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
@@ -63,7 +63,7 @@ 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/klauspost/compress v1.18.7
|
||||
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
|
||||
@@ -129,14 +129,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
|
||||
@@ -455,7 +455,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
|
||||
|
||||
@@ -170,8 +170,8 @@ 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=
|
||||
@@ -749,8 +749,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=
|
||||
@@ -1335,8 +1335,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 +1424,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 +1452,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 +1518,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 +1532,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 +1550,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 +1603,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=
|
||||
|
||||
@@ -119,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 {
|
||||
@@ -146,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
|
||||
}
|
||||
@@ -163,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")
|
||||
}
|
||||
@@ -540,22 +536,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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -147,18 +147,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 +171,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 +592,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,
|
||||
@@ -832,11 +817,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)
|
||||
|
||||
|
||||
+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)
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns/dnsclientpool"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
iputil "github.com/projectdiscovery/utils/ip"
|
||||
)
|
||||
|
||||
// Request contains a DNS protocol request to be made from a template
|
||||
@@ -207,48 +204,6 @@ func resolverHost(resolver string) string {
|
||||
return r
|
||||
}
|
||||
|
||||
// pinResolverHost replaces a resolver host name with an address that the
|
||||
// network policy allows. The policy check passes when one address of the name
|
||||
// is allowed, and the dns client resolves the name again when it dials, so it
|
||||
// could otherwise reach a denied address of the same name.
|
||||
// The lookup stops after timeout, so a slow system resolver cannot hold the
|
||||
// request.
|
||||
func pinResolverHost(executionID, resolver, host string, timeout time.Duration) (string, error) {
|
||||
// DoH and DoT resolvers dial through the scan dialer, which checks every
|
||||
// address, and DoT verifies the certificate against the name.
|
||||
if iputil.IsIP(host) || strings.HasPrefix(resolver, "doh:") || strings.HasPrefix(resolver, "dot:") {
|
||||
return resolver, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
addrs, err := net.DefaultResolver.LookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "could not resolve dns resolver %s", resolver)
|
||||
}
|
||||
prefix, address := resolver, ""
|
||||
if index := strings.LastIndex(resolver, host); index != -1 {
|
||||
prefix, address = resolver[:index], resolver[index:]
|
||||
}
|
||||
port := "53"
|
||||
if _, p, err := net.SplitHostPort(address); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if protocolstate.IsHostAllowed(executionID, addr) {
|
||||
return prefix + net.JoinHostPort(addr, port), nil
|
||||
}
|
||||
}
|
||||
return "", errors.Errorf("dns resolver %s is blocked by network policy", resolver)
|
||||
}
|
||||
|
||||
// resolverLookupTimeout is the scan timeout, or ten seconds when it is not set.
|
||||
func resolverLookupTimeout(options *protocols.ExecutorOptions) time.Duration {
|
||||
if options.Options.Timeout > 0 {
|
||||
return time.Duration(options.Options.Timeout) * time.Second
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (request *Request) getDnsClient(options *protocols.ExecutorOptions, metadata map[string]interface{}) (*dnsclientpool.Client, error) {
|
||||
dnsClientOptions := &dnsclientpool.Configuration{
|
||||
Retries: request.Retries,
|
||||
@@ -282,11 +237,6 @@ func (request *Request) getDnsClient(options *protocols.ExecutorOptions, metadat
|
||||
if !protocolstate.IsHostAllowed(options.Options.ExecutionId, host) {
|
||||
return nil, errors.Errorf("dns resolver %s is blocked by network policy", resolver)
|
||||
}
|
||||
pinned, err := pinResolverHost(options.Options.ExecutionId, resolver, host, resolverLookupTimeout(options))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolver = pinned
|
||||
}
|
||||
resolvers = append(resolvers, resolver)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func Get(options *types.Options, configuration *Configuration) (*Client, error)
|
||||
if (configuration.Retries <= 1) && len(configuration.Resolvers) == 0 {
|
||||
return cachedClient(resolversFor(options))
|
||||
}
|
||||
if len(options.InternalResolversList) == 0 && hasPolicyResolver(configuration.Resolvers) {
|
||||
if len(options.InternalResolversList) == 0 && hasDohResolver(configuration.Resolvers) {
|
||||
return getPolicyClient(options, configuration)
|
||||
}
|
||||
hash := configuration.Hash()
|
||||
|
||||
@@ -3,14 +3,12 @@ package dnsclientpool
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
@@ -21,8 +19,8 @@ import (
|
||||
)
|
||||
|
||||
// Client sends the DNS queries of a template. It sends queries for template
|
||||
// DoH and DoT resolvers itself, because the retryabledns clients dial and
|
||||
// follow redirects without the network policy of the scan.
|
||||
// DoH resolvers itself, because the retryabledns DoH client dials and follows
|
||||
// redirects without the network policy of the scan.
|
||||
type Client struct {
|
||||
*retryabledns.Client
|
||||
|
||||
@@ -31,51 +29,31 @@ type Client struct {
|
||||
index uint32
|
||||
doh *doh.Client
|
||||
transport *http.Transport
|
||||
dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// resolver is one template resolver. url is set for a DoH resolver and dot
|
||||
// for a DoT resolver, and both are empty for a resolver that retryabledns
|
||||
// queries over UDP or TCP.
|
||||
// resolver is one template resolver. url is empty for a resolver that
|
||||
// retryabledns queries over UDP, TCP or DoT.
|
||||
type resolver struct {
|
||||
url string
|
||||
method doh.Method
|
||||
dot string
|
||||
}
|
||||
|
||||
var (
|
||||
// policyClients holds the clients with DoH or DoT resolvers of each execution,
|
||||
// policyClients holds the clients with DoH resolvers of each execution,
|
||||
// because each one dials with the dialer of its execution.
|
||||
policyClients = map[string]map[string]*Client{}
|
||||
policyMu sync.Mutex
|
||||
|
||||
// dotRootCAs verifies DoT certificates, and nil uses the system roots.
|
||||
dotRootCAs *x509.CertPool
|
||||
)
|
||||
|
||||
func hasPolicyResolver(resolvers []string) bool {
|
||||
func hasDohResolver(resolvers []string) bool {
|
||||
for _, r := range resolvers {
|
||||
if strings.HasPrefix(r, "doh:") || strings.HasPrefix(r, "dot:") {
|
||||
if strings.HasPrefix(r, "doh:") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// parseDotResolver returns the host and port of a resolver such as
|
||||
// dot:dns.example or dot:dns.example:853, in the same way as retryabledns.
|
||||
func parseDotResolver(r string) (resolver, bool) {
|
||||
if !strings.HasPrefix(r, "dot:") {
|
||||
return resolver{}, false
|
||||
}
|
||||
address := strings.TrimPrefix(r, "dot:")
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
address = net.JoinHostPort(address, "853")
|
||||
}
|
||||
return resolver{dot: address}, true
|
||||
}
|
||||
|
||||
// parseDohResolver returns the URL and method of a resolver such as
|
||||
// doh:https://dns.example/dns-query:get, in the same way as retryabledns.
|
||||
func parseDohResolver(r string) (resolver, bool) {
|
||||
@@ -113,14 +91,10 @@ func getPolicyClient(options *types.Options, configuration *Configuration) (*Cli
|
||||
client.resolvers = append(client.resolvers, parsed)
|
||||
continue
|
||||
}
|
||||
if parsed, ok := parseDotResolver(r); ok {
|
||||
client.resolvers = append(client.resolvers, parsed)
|
||||
continue
|
||||
}
|
||||
client.resolvers = append(client.resolvers, resolver{})
|
||||
networkResolvers = append(networkResolvers, r)
|
||||
}
|
||||
// Trace and the UDP and TCP resolvers still go through retryabledns.
|
||||
// Trace and the non DoH resolvers still go through retryabledns.
|
||||
if len(networkResolvers) == 0 {
|
||||
networkResolvers = defaultResolvers
|
||||
}
|
||||
@@ -137,16 +111,14 @@ func getPolicyClient(options *types.Options, configuration *Configuration) (*Cli
|
||||
// The scan dialer checks every address it connects to, so redirects
|
||||
// and names that resolve to a denied address are refused too. It also
|
||||
// goes through the scan proxy when one is set.
|
||||
client.dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
dialers := protocolstate.GetDialersWithId(executionID)
|
||||
if dialers == nil || dialers.Fastdialer == nil {
|
||||
return nil, fmt.Errorf("dialers not initialized for %s", executionID)
|
||||
}
|
||||
return dialers.Fastdialer.Dial(ctx, network, address)
|
||||
}
|
||||
client.timeout = options.GetTimeouts().DialTimeout
|
||||
client.transport = &http.Transport{
|
||||
DialContext: client.dial,
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
dialers := protocolstate.GetDialersWithId(executionID)
|
||||
if dialers == nil || dialers.Fastdialer == nil {
|
||||
return nil, fmt.Errorf("dialers not initialized for %s", executionID)
|
||||
}
|
||||
return dialers.Fastdialer.Dial(ctx, network, address)
|
||||
},
|
||||
// same as the retryabledns DoH client
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402
|
||||
}
|
||||
@@ -177,8 +149,6 @@ func (c *Client) Do(msg *dns.Msg) (*dns.Msg, error) {
|
||||
r := c.resolvers[index%uint32(len(c.resolvers))]
|
||||
if r.url != "" {
|
||||
resp, err = c.doh.QueryWithDOHMsg(r.method, doh.Resolver{URL: r.url}, msg)
|
||||
} else if r.dot != "" {
|
||||
resp, err = c.queryDoT(r.dot, msg)
|
||||
} else {
|
||||
resp, err = c.Client.Do(msg)
|
||||
}
|
||||
@@ -189,35 +159,7 @@ func (c *Client) Do(msg *dns.Msg) (*dns.Msg, error) {
|
||||
return resp, retryabledns.ErrRetriesExceeded
|
||||
}
|
||||
|
||||
// queryDoT sends a DNS message over TLS to address. It dials through the scan
|
||||
// dialer and verifies the certificate against the resolver host, as the
|
||||
// retryabledns DoT client does.
|
||||
func (c *Client) queryDoT(address string, msg *dns.Msg) (*dns.Msg, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := c.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = retryabledns.DefaultOptions.Timeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
conn, err := c.dial(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn := tls.Client(conn, &tls.Config{ServerName: host, RootCAs: dotRootCAs, MinVersion: tls.VersionTLS12})
|
||||
defer func() { _ = tlsConn.Close() }()
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsClient := &dns.Client{Net: "tcp-tls", Timeout: timeout}
|
||||
resp, _, err := dnsClient.ExchangeWithConn(msg, &dns.Conn{Conn: tlsConn})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Close drops the clients with DoH or DoT resolvers of an execution.
|
||||
// Close drops the clients with DoH resolvers of an execution.
|
||||
func Close(executionID string) {
|
||||
policyMu.Lock()
|
||||
clients := policyClients[executionID]
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package dnsclientpool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newDotListener accepts TCP connections on 127.0.0.1, counts them and closes
|
||||
// them, so a test sees each DoT dial that reaches the address.
|
||||
func newDotListener(t *testing.T, hits *atomic.Int32) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = l.Close() })
|
||||
go func() {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hits.Add(1)
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
require.NoError(t, err)
|
||||
return port
|
||||
}
|
||||
|
||||
func TestDotResolverObeysNetworkPolicy(t *testing.T) {
|
||||
for _, host := range []string{"127.0.0.1", "localhost"} {
|
||||
t.Run("denied address "+host, func(t *testing.T) {
|
||||
var hits atomic.Int32
|
||||
port := newDotListener(t, &hits)
|
||||
// localhost passes the resolver check through ::1, while 127.0.0.1 is denied
|
||||
options := dohOptions(t, []string{"127.0.0.1"})
|
||||
|
||||
client, err := Get(options, &Configuration{Retries: 2, Resolvers: []string{"dot:" + net.JoinHostPort(host, port)}})
|
||||
require.NoError(t, err)
|
||||
_, _ = client.Do(txtQuery())
|
||||
require.Zero(t, hits.Load(), "DoT query reached a denied address")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("allowed address", func(t *testing.T) {
|
||||
var hits atomic.Int32
|
||||
port := newDotListener(t, &hits)
|
||||
options := dohOptions(t, nil)
|
||||
|
||||
client, err := Get(options, &Configuration{Retries: 1, Resolvers: []string{"dot:127.0.0.1:" + port}})
|
||||
require.NoError(t, err)
|
||||
_, _ = client.Do(txtQuery())
|
||||
require.Equal(t, int32(1), hits.Load())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDotResolverAnswers(t *testing.T) {
|
||||
// httptest gives a certificate for 127.0.0.1 and a pool that trusts it.
|
||||
certs := httptest.NewTLSServer(http.NotFoundHandler())
|
||||
t.Cleanup(certs.Close)
|
||||
previous := dotRootCAs
|
||||
dotRootCAs = certs.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
|
||||
t.Cleanup(func() { dotRootCAs = previous })
|
||||
|
||||
l, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: certs.TLS.Certificates, MinVersion: tls.VersionTLS12})
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
rr, _ := dns.NewRR(`example.com. 60 IN TXT "dot-answer"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
_ = w.WriteMsg(m)
|
||||
})}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
|
||||
options := dohOptions(t, nil)
|
||||
client, err := Get(options, &Configuration{Retries: 2, Resolvers: []string{"dot:" + l.Addr().String()}})
|
||||
require.NoError(t, err)
|
||||
resp, err := client.Do(txtQuery())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Answer, 1)
|
||||
require.Contains(t, resp.Answer[0].String(), "dot-answer")
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func (request *Request) execute(input *contextargs.Context, domain string, metad
|
||||
// perform trace if necessary (excluded from duration — only the query RTT is measured)
|
||||
var traceData *retryabledns.TraceData
|
||||
if request.Trace {
|
||||
traceData, err = trace(request.options.Options.ExecutionId, request.dnsClient, domain, request.question, request.TraceMaxRecursion)
|
||||
traceData, err = request.dnsClient.Trace(domain, request.question, request.TraceMaxRecursion)
|
||||
if err != nil {
|
||||
request.options.Output.Request(request.options.TemplatePath, domain, "dns", err)
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
)
|
||||
|
||||
// startDNSServer answers every query with a TXT record, or with the A records
|
||||
// in addrs when the question is for name, and counts the queries.
|
||||
func startDNSServer(t *testing.T, name string, addrs []string, hits *atomic.Int32) string {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{PacketConn: pc, Handler: dns.HandlerFunc(func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
hits.Add(1)
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
question := q.Question[0]
|
||||
switch {
|
||||
case question.Name == name && question.Qtype == dns.TypeA:
|
||||
for _, addr := range addrs {
|
||||
rr, _ := dns.NewRR(name + " 0 IN A " + addr)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
case question.Name == name:
|
||||
default:
|
||||
rr, _ := dns.NewRR(question.Name + ` 0 IN TXT "answer"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
l, err := net.Listen("tcp", pc.LocalAddr().String())
|
||||
require.NoError(t, err)
|
||||
tcpServer := &dns.Server{Listener: l, Handler: server.Handler}
|
||||
go func() { _ = tcpServer.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = tcpServer.Shutdown() })
|
||||
return pc.LocalAddr().String()
|
||||
}
|
||||
|
||||
// useSystemResolver sends the lookups of the process to the DNS server at address.
|
||||
func useSystemResolver(t *testing.T, address string) {
|
||||
t.Helper()
|
||||
previous := net.DefaultResolver
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "udp", address)
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() { net.DefaultResolver = previous })
|
||||
}
|
||||
|
||||
func TestDNSResolverNameObeysNetworkPolicy(t *testing.T) {
|
||||
// The resolver name has an allowed address and a denied address. The
|
||||
// policy check passes because one address is allowed, so the query must
|
||||
// not go to the denied one.
|
||||
var deniedHits, systemHits atomic.Int32
|
||||
denied := startDNSServer(t, "", nil, &deniedHits)
|
||||
_, port, err := net.SplitHostPort(denied)
|
||||
require.NoError(t, err)
|
||||
system := startDNSServer(t, "resolver.nuclei-test.", []string{"127.0.0.1", "127.0.0.2"}, &systemHits)
|
||||
useSystemResolver(t, system)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = []string{"127.0.0.1"}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: "dns-resolver-policy",
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}, Name: "test"},
|
||||
})
|
||||
|
||||
for _, resolver := range []string{"resolver.nuclei-test:" + port, "tcp:resolver.nuclei-test:" + port} {
|
||||
t.Run(resolver, func(t *testing.T) {
|
||||
deniedHits.Store(0)
|
||||
request := &Request{
|
||||
RequestType: DNSRequestTypeHolder{DNSRequestType: TXT},
|
||||
Class: "INET",
|
||||
Retries: 2,
|
||||
Name: "{{FQDN}}",
|
||||
Resolvers: []string{resolver},
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion("example.com.", dns.TypeTXT)
|
||||
_, _ = request.dnsClient.Do(msg)
|
||||
require.Zero(t, deniedHits.Load(), "dns query reached a denied address")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResolverNameLookupStopsAtTimeout(t *testing.T) {
|
||||
// The system resolver never answers, so the lookup that pins the resolver
|
||||
// name must stop at the timeout.
|
||||
silent, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = silent.Close() })
|
||||
useSystemResolver(t, silent.LocalAddr().String())
|
||||
|
||||
start := time.Now()
|
||||
_, err = pinResolverHost(t.Name(), "silent.nuclei-test:53", "silent.nuclei-test", time.Second)
|
||||
require.Error(t, err)
|
||||
require.Less(t, time.Since(start), 4*time.Second, "the resolver lookup did not stop at the timeout")
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns/dnsclientpool"
|
||||
"github.com/projectdiscovery/retryabledns"
|
||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
||||
)
|
||||
|
||||
// traceNameServerPort is the port of the name servers that a trace queries.
|
||||
var traceNameServerPort = "53"
|
||||
|
||||
// trace follows the delegation of host from the root servers in the same way
|
||||
// as retryabledns.Client.Trace, but it skips name server addresses that the
|
||||
// network policy denies. The name servers come from the answers, so the owner
|
||||
// of a zone could otherwise send the queries of a trace to internal addresses.
|
||||
func trace(executionID string, client *dnsclientpool.Client, host string, requestType uint16, maxRecursion int) (*retryabledns.TraceData, error) {
|
||||
var traceData retryabledns.TraceData
|
||||
host = dns.CanonicalName(host)
|
||||
servers := retryabledns.RootDNSServersIPv4
|
||||
seenNS := make(map[string]struct{})
|
||||
seenCName := make(map[string]int)
|
||||
for i := 1; i < maxRecursion; i++ {
|
||||
responses, err := client.QueryParallel(host, requestType, servers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, server := range servers {
|
||||
seenNS[server] = struct{}{}
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return &traceData, nil
|
||||
}
|
||||
for _, dnsData := range responses {
|
||||
if dnsData != nil && len(dnsData.Resolver) > 0 {
|
||||
traceData.DNSData = append(traceData.DNSData, dnsData)
|
||||
}
|
||||
}
|
||||
|
||||
var nameServers []string
|
||||
var nextCname string
|
||||
for _, dnsData := range responses {
|
||||
if dnsData == nil {
|
||||
continue
|
||||
}
|
||||
for _, ns := range dnsData.NS {
|
||||
ips, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", ns)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if !protocolstate.IsHostAllowed(executionID, ip.String()) {
|
||||
continue
|
||||
}
|
||||
nameServers = append(nameServers, net.JoinHostPort(ip.String(), traceNameServerPort))
|
||||
}
|
||||
}
|
||||
if nextCname == "" && len(dnsData.CNAME) > 0 {
|
||||
nextCname = dnsData.CNAME[0]
|
||||
}
|
||||
}
|
||||
nameServers = sliceutil.Dedupe(nameServers)
|
||||
if len(nameServers) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
server := nameServers[rand.Intn(len(nameServers))] // #nosec G404
|
||||
if _, ok := seenNS[server]; ok && nextCname == "" {
|
||||
break
|
||||
}
|
||||
servers = []string{server}
|
||||
|
||||
if nextCname != "" {
|
||||
seenCName[nextCname]++
|
||||
if seenCName[nextCname] > retryabledns.DefaultMaxPerCNAMEFollows {
|
||||
break
|
||||
}
|
||||
host = nextCname
|
||||
}
|
||||
}
|
||||
return &traceData, nil
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/retryabledns"
|
||||
)
|
||||
|
||||
func serveTraceDNS(t *testing.T, address string, handler dns.HandlerFunc) string {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", address)
|
||||
require.NoError(t, err)
|
||||
server := &dns.Server{PacketConn: pc, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
return pc.LocalAddr().String()
|
||||
}
|
||||
|
||||
func TestDNSTraceObeysNetworkPolicy(t *testing.T) {
|
||||
// The zone of the traced name delegates to a name server on 127.0.0.1.
|
||||
// The trace must query it when the policy allows the address, so that the
|
||||
// test can see a trace that ignores the policy, and must skip it when the
|
||||
// policy denies the address.
|
||||
t.Run("allowed", func(t *testing.T) {
|
||||
require.NotZero(t, runTraceAgainstLoopbackNameServer(t, nil), "trace did not query the allowed name server")
|
||||
})
|
||||
t.Run("denied", func(t *testing.T) {
|
||||
require.Zero(t, runTraceAgainstLoopbackNameServer(t, []string{"127.0.0.1"}), "trace queried a denied name server")
|
||||
})
|
||||
}
|
||||
|
||||
func runTraceAgainstLoopbackNameServer(t *testing.T, excludeTargets []string) int32 {
|
||||
t.Helper()
|
||||
var internalHits atomic.Int32
|
||||
internal := serveTraceDNS(t, "127.0.0.1:0", func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
internalHits.Add(1)
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
rr, _ := dns.NewRR(q.Question[0].Name + ` 0 IN TXT "internal"`)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
_, port, err := net.SplitHostPort(internal)
|
||||
require.NoError(t, err)
|
||||
previousPort := traceNameServerPort
|
||||
traceNameServerPort = port
|
||||
t.Cleanup(func() { traceNameServerPort = previousPort })
|
||||
|
||||
// public stands in for the root servers, the scan resolvers and the system resolver
|
||||
public := serveTraceDNS(t, "127.0.0.1:0", func(w dns.ResponseWriter, q *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(q)
|
||||
question := q.Question[0]
|
||||
var rr dns.RR
|
||||
switch {
|
||||
case question.Name == "ns.nuclei-test." && question.Qtype == dns.TypeA:
|
||||
rr, _ = dns.NewRR("ns.nuclei-test. 0 IN A 127.0.0.1")
|
||||
case question.Name == "ns.nuclei-test.":
|
||||
case question.Name == "trace.nuclei-test.":
|
||||
rr, _ = dns.NewRR("trace.nuclei-test. 0 IN NS ns.nuclei-test.")
|
||||
default:
|
||||
rr, _ = dns.NewRR(question.Name + ` 0 IN TXT "public"`)
|
||||
}
|
||||
if rr != nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
previousRoots := retryabledns.RootDNSServersIPv4
|
||||
retryabledns.RootDNSServersIPv4 = []string{public}
|
||||
t.Cleanup(func() { retryabledns.RootDNSServersIPv4 = previousRoots })
|
||||
previousResolver := net.DefaultResolver
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "udp", public)
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() { net.DefaultResolver = previousResolver })
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = excludeTargets
|
||||
options.InternalResolversList = []string{public}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
|
||||
ID: "dns-trace-policy",
|
||||
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}, Name: "test"},
|
||||
})
|
||||
|
||||
request := &Request{
|
||||
RequestType: DNSRequestTypeHolder{DNSRequestType: TXT},
|
||||
Class: "INET",
|
||||
Name: "{{FQDN}}",
|
||||
// One retry keeps the client keyed by this test's resolvers, so a
|
||||
// client cached by an earlier run cannot point at a closed server.
|
||||
Retries: 1,
|
||||
Trace: true,
|
||||
TraceMaxRecursion: 5,
|
||||
}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
var event output.InternalEvent
|
||||
input := contextargs.NewWithInput(context.Background(), "trace.nuclei-test")
|
||||
err = request.ExecuteWithResults(input, nil, nil, func(e *output.InternalWrappedEvent) {
|
||||
event = e.InternalEvent
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, event["trace"], "ns.nuclei-test", "trace did not query the root servers")
|
||||
return internalHits.Load()
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/eventcreator"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/responsehighlighter"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/render"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool"
|
||||
@@ -35,7 +34,6 @@ import (
|
||||
"github.com/projectdiscovery/tlsx/pkg/tlsx/clients"
|
||||
"github.com/projectdiscovery/tlsx/pkg/tlsx/openssl"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
iputil "github.com/projectdiscovery/utils/ip"
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
)
|
||||
|
||||
@@ -204,21 +202,6 @@ func (request *Request) GetID() string {
|
||||
return request.ID
|
||||
}
|
||||
|
||||
// allowedAddress returns the first address of host that the network policy allows.
|
||||
func (request *Request) allowedAddress(host string) (string, error) {
|
||||
dnsData, err := request.dialer.GetDNSData(host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addresses := append(append([]string{}, dnsData.A...), dnsData.AAAA...)
|
||||
for _, address := range addresses {
|
||||
if protocolstate.IsHostAllowed(request.options.Options.ExecutionId, address) {
|
||||
return address, nil
|
||||
}
|
||||
}
|
||||
return "", protocolstate.ErrHostDenied.Msgf(host)
|
||||
}
|
||||
|
||||
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
|
||||
func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
|
||||
hostPort := input.MetaInput.Input
|
||||
@@ -272,23 +255,6 @@ func (request *Request) ExecuteWithResults(input *contextargs.Context, dynamicVa
|
||||
} else {
|
||||
hostIp = host
|
||||
}
|
||||
if !iputil.IsIP(hostIp) {
|
||||
// The openssl scan mode runs the openssl binary, which resolves a host
|
||||
// name again by itself, so it gets an address the policy allows.
|
||||
address, err := request.allowedAddress(hostIp)
|
||||
if err != nil {
|
||||
requestOptions.Output.Request(requestOptions.TemplateID, input.MetaInput.Input, request.Type().String(), err)
|
||||
requestOptions.Progress.IncrementFailedRequestsBy(1)
|
||||
return errkit.Wrap(err, "could not resolve host")
|
||||
}
|
||||
hostIp = address
|
||||
} else if !protocolstate.IsHostAllowed(request.options.Options.ExecutionId, hostIp) {
|
||||
// A custom dialer can allow an address that the execution policy denies.
|
||||
err := protocolstate.ErrHostDenied.Msgf(hostIp)
|
||||
requestOptions.Output.Request(requestOptions.TemplateID, input.MetaInput.Input, request.Type().String(), err)
|
||||
requestOptions.Progress.IncrementFailedRequestsBy(1)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := request.options.HostBackoffWait(input.Context(), addressToDial); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package ssl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/fastdialer/fastdialer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/tlsx/pkg/tlsx/openssl"
|
||||
)
|
||||
|
||||
func selfSignedCertificate(t *testing.T, name string) tls.Certificate {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: name},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
require.NoError(t, err)
|
||||
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
||||
}
|
||||
|
||||
// serveTLS counts the TLS handshakes that reach address.
|
||||
func serveTLS(t *testing.T, address, name string, handshakes *atomic.Int32) string {
|
||||
t.Helper()
|
||||
listener, err := tls.Listen("tcp", address, &tls.Config{Certificates: []tls.Certificate{selfSignedCertificate(t, name)}})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = listener.Close() })
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() { _ = conn.Close() }()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if conn.(*tls.Conn).Handshake() == nil {
|
||||
handshakes.Add(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}()
|
||||
return listener.Addr().String()
|
||||
}
|
||||
|
||||
func TestSSLOpenSSLObeysNetworkPolicy(t *testing.T) {
|
||||
if !openssl.IsAvailable() {
|
||||
t.Skip("openssl binary is not installed")
|
||||
}
|
||||
addrs, err := net.LookupHost("localhost")
|
||||
if err != nil || len(addrs) < 2 {
|
||||
t.Skip("localhost does not resolve to both loopback addresses")
|
||||
}
|
||||
|
||||
// The policy allows 127.0.0.1 and denies ::1, so localhost passes the
|
||||
// dial check. The openssl binary must not connect to ::1.
|
||||
var allowedHandshakes, deniedHandshakes atomic.Int32
|
||||
allowed := serveTLS(t, "127.0.0.1:0", "allowed", &allowedHandshakes)
|
||||
_, port, err := net.SplitHostPort(allowed)
|
||||
require.NoError(t, err)
|
||||
serveTLS(t, net.JoinHostPort("::1", port), "denied", &deniedHandshakes)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = []string{"::1"}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ID: "ssl-openssl-policy"})
|
||||
executerOpts.IsMultiProtocol = true
|
||||
|
||||
request := &Request{ID: "ssl-openssl-policy", Address: "{{Host}}:{{Port}}", ScanMode: "openssl"}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
// The scan resolves names with the fastdialer, not the system resolver. On
|
||||
// Windows the hosts file has no localhost entry, so the name does not resolve.
|
||||
if !resolvesToBothLoopbacks(request) {
|
||||
t.Skip("the scan dialer does not resolve localhost to both loopback addresses")
|
||||
}
|
||||
|
||||
var event output.InternalEvent
|
||||
input := contextargs.NewWithInput(context.Background(), net.JoinHostPort("localhost", port))
|
||||
err = request.ExecuteWithResults(input, nil, nil, func(e *output.InternalWrappedEvent) {
|
||||
event = e.InternalEvent
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "allowed", event["subject_cn"])
|
||||
require.Zero(t, deniedHandshakes.Load(), "openssl connected to a denied address")
|
||||
}
|
||||
|
||||
func resolvesToBothLoopbacks(request *Request) bool {
|
||||
data, err := request.dialer.GetDNSData("localhost")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return slices.Contains(data.A, "127.0.0.1") && slices.Contains(data.AAAA, "::1")
|
||||
}
|
||||
|
||||
func TestSSLOpenSSLLiteralIPObeysExecutionPolicy(t *testing.T) {
|
||||
if !openssl.IsAvailable() {
|
||||
t.Skip("openssl binary is not installed")
|
||||
}
|
||||
|
||||
// The input pins ::1 as its custom ip, and a custom dialer with no deny
|
||||
// list would connect to it. The execution policy denies it, so the openssl
|
||||
// binary must not connect to it.
|
||||
var deniedHandshakes atomic.Int32
|
||||
denied := serveTLS(t, "[::1]:0", "denied", &deniedHandshakes)
|
||||
_, port, err := net.SplitHostPort(denied)
|
||||
require.NoError(t, err)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = []string{"::1"}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ID: "ssl-openssl-literal-ip"})
|
||||
executerOpts.IsMultiProtocol = true
|
||||
customDialer, err := fastdialer.NewDialer(fastdialer.DefaultOptions)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(customDialer.Close)
|
||||
executerOpts.CustomFastdialer = customDialer
|
||||
|
||||
request := &Request{ID: "ssl-openssl-literal-ip", Address: "{{Host}}:{{Port}}", ScanMode: "openssl"}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
input := contextargs.NewWithInput(context.Background(), net.JoinHostPort("localhost", port))
|
||||
input.MetaInput.CustomIP = "::1"
|
||||
err = request.ExecuteWithResults(input, nil, nil, func(*output.InternalWrappedEvent) {})
|
||||
require.Error(t, err)
|
||||
require.Zero(t, deniedHandshakes.Load(), "openssl connected to an address the execution policy denies")
|
||||
}
|
||||
|
||||
func TestSSLOpenSSLCustomIPObeysNetworkPolicy(t *testing.T) {
|
||||
if !openssl.IsAvailable() {
|
||||
t.Skip("openssl binary is not installed")
|
||||
}
|
||||
|
||||
// The input pins the denied address ::1 as its custom ip, so the request
|
||||
// skips the name lookup. The openssl binary must still not connect to it.
|
||||
var deniedHandshakes atomic.Int32
|
||||
denied := serveTLS(t, "[::1]:0", "denied", &deniedHandshakes)
|
||||
_, port, err := net.SplitHostPort(denied)
|
||||
require.NoError(t, err)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.SetExecutionID(t.Name())
|
||||
options.ExcludeTargets = []string{"::1"}
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ID: "ssl-openssl-custom-ip"})
|
||||
executerOpts.IsMultiProtocol = true
|
||||
|
||||
request := &Request{ID: "ssl-openssl-custom-ip", Address: "{{Host}}:{{Port}}", ScanMode: "openssl"}
|
||||
require.NoError(t, request.Compile(executerOpts))
|
||||
|
||||
input := contextargs.NewWithInput(context.Background(), net.JoinHostPort("localhost", port))
|
||||
input.MetaInput.CustomIP = "::1"
|
||||
_ = request.ExecuteWithResults(input, nil, nil, func(*output.InternalWrappedEvent) {})
|
||||
require.Zero(t, deniedHandshakes.Load(), "openssl connected to a denied custom ip")
|
||||
}
|
||||
@@ -289,18 +289,6 @@ func (template *Template) SelectedBy(selection protocols.TemplateSelection) bool
|
||||
return selection.Allows(template.Path)
|
||||
}
|
||||
|
||||
// TemplateIDs returns the IDs of the members selection selects; a nil
|
||||
// selection selects every member.
|
||||
func (e *ClusterExecuter) TemplateIDs(selection protocols.TemplateSelection) []string {
|
||||
ids := make([]string, 0, len(e.operators))
|
||||
for _, operator := range e.operators {
|
||||
if selection == nil || selection.Allows(operator.templatePath) {
|
||||
ids = append(ids, operator.templateID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// operatorsFor returns the members selected for input by per-target profiles.
|
||||
func (e *ClusterExecuter) operatorsFor(input *contextargs.MetaInput) []*clusteredOperator {
|
||||
if e.options.TargetScope == nil {
|
||||
|
||||
@@ -251,9 +251,6 @@ type Options struct {
|
||||
Silent bool
|
||||
// Validate validates the templates passed to nuclei.
|
||||
Validate bool
|
||||
// PlanFile is where the work a scan would do is written as JSON lines;
|
||||
// nuclei exits after writing it, without sending requests.
|
||||
PlanFile string
|
||||
// NoStrictSyntax disables strict syntax check on nuclei templates (allows custom key-value pairs).
|
||||
NoStrictSyntax bool
|
||||
// Verbose flag indicates whether to show verbose output or not
|
||||
@@ -712,7 +709,6 @@ func (options *Options) Copy() *Options {
|
||||
AutomaticScan: options.AutomaticScan,
|
||||
Silent: options.Silent,
|
||||
Validate: options.Validate,
|
||||
PlanFile: options.PlanFile,
|
||||
NoStrictSyntax: options.NoStrictSyntax,
|
||||
Verbose: options.Verbose,
|
||||
VerboseVerbose: options.VerboseVerbose,
|
||||
|
||||
@@ -17,13 +17,6 @@ func TestCopyPreservesPayloadRandomOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyPreservesPlanFile(t *testing.T) {
|
||||
options := &Options{PlanFile: "plan.jsonl"}
|
||||
if options.Copy().PlanFile != "plan.jsonl" {
|
||||
t.Fatal("Copy must preserve PlanFile, or an embedded caller loses plan mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONMapRejectsKeysSharingAStringForm(t *testing.T) {
|
||||
metadata := map[string]interface{}{
|
||||
"labels": []interface{}{map[interface{}]interface{}{1: "int", 1.0: "float64"}},
|
||||
|
||||
Reference in new issue
Block a user