Compare commits

...
Author SHA1 Message Date
Sandeep Singh 8ff0e52c0b chore: add automatic security context updates 2026-09-05 06:45:37 +05:30
Sandeep Singh fdd7bd1d66 chore: add repository security context 2026-09-05 06:45:36 +05:30
Sandeep Singh 138f546080 chore: add repository security context 2026-09-03 06:12:44 +05:30
dw1 b04fc59ad0 refactor: adopt XDG storage and layered configs (#7684)
* refactor: adopt XDG storage and layered configs

Load XDG system configuration (and /etc/nuclei/config.yaml) before the
active user configuration, selected config, profile, and command line.
Preserve explicit command-line values, replace collections between
layers, and defer stateful option handling until the final values are
known. Keep callback flags and authentication setup limited to the CLI.

Move configuration loading, profile resolution, inline targets, and
inline secrets into internal/configuration. Keep NUCLEI_CONFIG_DIR as
a compatibility override for the user configuration directory.

Use platform directories for configuration, template data, persistent
state, and cache data. Discover default template roots through
XDG_DATA_HOME and XDG_DATA_DIRS without assigning ownership semantics
to the selected path.

Store template state in XDG_STATE_HOME and migrate the legacy
.templates-config.json file when needed. Persist the active template
path, keep custom provider templates below that root, and apply the
same installation, update, health-check, and reset behavior to every
root.

Keep .nuclei-ignore with the active template root. Derive its local
hash from the file and pass the remote hash through the update check
without persisting either value.

Move generated resume and crash-recovery files into persistent state
and use XDG_CACHE_HOME for the catalog index. Write template state and
cache snapshots atomically with private permissions.

Validate every reset target before deleting any path. Reject broad or
unsafe targets, preserve explicit resume paths, and remove generated
inline-secret directories on normal, interrupted, and fatal exits.

Isolate configuration, state, cache, and template paths in functional
and integration tests, and add coverage for layered precedence, state
migration, active-root lifecycle, reset safety, and private file modes.

Closes #6755

Signed-off-by: Dwi Siswanto <[email protected]>

* chore(cmd): use memProfile+ext instead

Signed-off-by: Dwi Siswanto <[email protected]>

* fix(lib): gate SDK version check

Signed-off-by: Dwi Siswanto <[email protected]>

---------

Signed-off-by: Dwi Siswanto <[email protected]>
2026-09-03 03:21:50 +07:00
Mzack9999 2a11278647 Merge pull request #7683 from threatmate/tm/restore-idle-conn-pool
Size the per-host idle connection pool to actual concurrency
2026-09-02 19:45:06 +04:00
Stan Ivanov 83dbc85a08 Let -c and payload threads raise the idle connection pool
Review asked for the pool to be sized from the configured concurrency instead of
a constant, as max(TemplateThreads, request Threads) with TemplateThreads
defaulting to 25. The first half of that is right and is what this does now:
TemplateThreads and a template's payload Threads both raise the pool, which fixes
-c 800 being served by a 500-deep pool.

They cannot also lower it, because -c is not an upper bound on how many
connections the transport has open. http.Transport starts a dial whenever a
request does not find an idle connection; if one frees up first the request
takes it and the racing dial still completes. Measured against a keep-alive
target at -c 25, 1,965 templates, 5,388 requests, counting at the target:

  requests in flight   peaked at 22-31
  open connections     peaked at 121-178

Sizing the pool at -c therefore discards most of what the transport opened:

  pool 500 (floor)   414 connections    13.0 req/conn
  pool 25  (= -c)  3,070 connections     1.8 req/conn
  pool 4   (v3.11.1) 4,352 connections   1.2 req/conn

So the floor stays at 500 and the configured concurrency raises it. A pool that
is never filled costs nothing: it is a ceiling on retained connections, not an
allocation, and the transport only ever holds connections it actually opened.

Tests: TestIdleConnPoolSize is the requested table (unset, -c 50, -c 800,
payload threads raising the pool, payload threads never shrinking it);
TestIdleConnPoolSizeIsWiredToTransport asserts the size reaches http.Transport
rather than only the helper; TestIdleConnPoolAllowsKeepAliveReuseWithPayloadThreads
covers the client that payload templates share -- Configuration.Threads is in the
client cache key, so every template with the same payload thread count piles onto
one pool. It holds each group of requests at a cyclic barrier so the overlap is
sustained for the whole run; with a single-shot barrier a pool that is too small
still passes, because once requests stop overlapping a shallow pool is enough.
2026-09-02 10:19:04 -04:00
github-actions[bot]andehsandeep 764f5a9744 chore: refresh security context (#7688)
Co-authored-by: ehsandeep <[email protected]>
2026-09-01 22:16:44 +05:30
Sandeep Singh 3e1e0e81f0 Add workflow to maintain security context
This workflow maintains the security context by analyzing the repository for vulnerabilities and updating the SECURITY_CONTEXT.md file accordingly.
2026-09-01 21:50:52 +05:30
Mzack9999 a2f9070f68 Merge pull request #7677 from dev-rehaann/fix/disable-cookie-shared-jar
fix(http): honor disable-cookie with shared jars
2026-09-01 17:13:21 +04:00
Stanimir Ivanov c07c84f1d1 Make the idle-pool test fail loudly instead of vacuously
The connection-count assertion held trivially in two cases the test did not
rule out: if every request errored (client.Do errors were swallowed, so zero
successful requests also means zero connections), or if the requests happened
to serialise, since the semaphore caps concurrency without forcing overlap.

Requests now block in the handler until "concurrency" of them have arrived, so
that many connections are genuinely required, and the test asserts every
request succeeded before looking at the connection count. A timer releases the
barrier so a shortfall fails on the assertions rather than hanging.

Verified both ways: reverting the pool to 4 fails on the connection count (400
requests over 176 connections), and pointing the client at a closed server
fails on "requests must succeed" rather than passing on zero connections.
2026-08-31 20:15:52 -04:00
Stanimir Ivanov d443462fda Add regression test for per-host idle connection pool sizing
Counts connections accepted by the server rather than requests served,
because the regression is invisible in request counts: the scan still issues
every request, it just opens a new connection for most of them.

Fails at MaxIdleConnsPerHost=4 (400 requests over 157 connections) and passes
with the pool sized to concurrency (400 requests over 50 connections).
2026-08-31 19:24:21 -04:00
Stanimir Ivanov 8bd6fabb04 Size the per-host idle connection pool to actual concurrency
The per-host client pool introduced in v3.10.0 sets MaxIdleConns and
MaxIdleConnsPerHost to 4. http.Transport closes a connection returned to a
host whose idle pool is already full, so an idle pool smaller than the number
of requests in flight against that host silently defeats keep-alive: every
request past the fourth pays a fresh TCP (and TLS) handshake instead of
reusing a pooled connection.

Clients are scoped to a single host and, in host-spray mode, every template
runs against that host, so concurrency is TemplateThreads multiplied by a
template's payload parallelism. With the defaults that is far above 4.

Measured against a local keep-alive target (6,674 templates, host-spray,
TemplateThreads=50), counting connections accepted by the target:

  before: 17,564 connections for 19,997 requests (1.14 requests/connection)
  after:      885 connections for 19,997 requests (22.6 requests/connection)

Identical request count and identical findings (7 and 7), so this is purely
connection reuse. The connection churn also defeats NAT: behind a Cloud NAT
gateway the pre-fix rate of ~147 new connections/sec per scanner exhausted
per-endpoint port mappings and the gateway began dropping outbound SYNs.

configuration.Threads may now only raise the pool, never lower it, since one
template's thread count does not bound total concurrency against the host.
2026-08-31 18:50:18 -04:00
Mzack9999 98d264c8a7 Merge pull request #7666 from buzhimingdeaikun/codex/fix-readme-grammar
docs: fix grammar in README
2026-08-31 17:53:39 +04:00
Mzack9999 d0d5100607 fix wording 2026-08-31 17:45:58 +04:00
dependabot[bot] 3f6e6931dd chore(deps): bump the modules group with 11 updates (#7682)
Bumps the modules group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/clistats](https://github.com/projectdiscovery/clistats) | `0.1.5` | `0.1.6` |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.17` | `0.5.18` |
| [github.com/projectdiscovery/hmap](https://github.com/projectdiscovery/hmap) | `0.0.101` | `0.0.102` |
| [github.com/projectdiscovery/retryabledns](https://github.com/projectdiscovery/retryabledns) | `1.0.115` | `1.0.116` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.23` | `1.3.24` |
| [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) | `0.8.21` | `0.8.22` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.47` | `0.1.48` |
| [github.com/projectdiscovery/useragent](https://github.com/projectdiscovery/useragent) | `0.0.108` | `0.0.109` |
| [github.com/projectdiscovery/utils](https://github.com/projectdiscovery/utils) | `0.11.1` | `0.11.2` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.94` | `0.2.95` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.49` | `1.2.50` |


Updates `github.com/projectdiscovery/clistats` from 0.1.5 to 0.1.6
- [Release notes](https://github.com/projectdiscovery/clistats/releases)
- [Commits](https://github.com/projectdiscovery/clistats/compare/v0.1.5...v0.1.6)

Updates `github.com/projectdiscovery/fastdialer` from 0.5.17 to 0.5.18
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.17...v0.5.18)

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

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

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.23 to 1.3.24
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.23...v1.3.24)

Updates `github.com/projectdiscovery/dsl` from 0.8.21 to 0.8.22
- [Release notes](https://github.com/projectdiscovery/dsl/releases)
- [Commits](https://github.com/projectdiscovery/dsl/compare/v0.8.21...v0.8.22)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.47 to 0.1.48
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.47...v0.1.48)

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

Updates `github.com/projectdiscovery/utils` from 0.11.1 to 0.11.2
- [Release notes](https://github.com/projectdiscovery/utils/releases)
- [Changelog](https://github.com/projectdiscovery/utils/blob/main/CHANGELOG.md)
- [Commits](https://github.com/projectdiscovery/utils/compare/v0.11.1...v0.11.2)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.94 to 0.2.95
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.94...v0.2.95)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.49 to 1.2.50
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.49...v1.2.50)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/clistats
  dependency-version: 0.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/hmap
  dependency-version: 0.0.102
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryabledns
  dependency-version: 1.0.116
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/dsl
  dependency-version: 0.8.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/useragent
  dependency-version: 0.0.109
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/utils
  dependency-version: 0.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.95
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.50
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-31 06:19:09 +00:00
Rehan Khan efd160c266 Merge branch 'dev' into fix/disable-cookie-shared-jar 2026-08-27 20:22:19 +05:00
Dogan Can Bakir da279d42a3 Merge pull request #7585 from projectdiscovery/5414-passive-exports
passive exports
2026-08-26 06:39:25 +03:00
Dogan Can Bakir f146fb3a06 Merge pull request #7674 from projectdiscovery/7673-template-id-error
fix(templates): invalid template id error message
2026-08-26 05:59:10 +03:00
Dogan Can Bakir 6a5c443f95 Merge pull request #7629 from projectdiscovery/7590-raw-http-input
feat(input): add raw http request input mode
2026-08-26 05:14:49 +03:00
Rehan Khan 71ccb1d568 chore: remove redundant test comment
Keep the regression test focused on behavior without restating the method name.

Signed-off-by: Muhammad Rehan Khan <[email protected]>
Signed-off-by: Rehan Khan <[email protected]>
2026-08-25 18:38:54 +05:00
Rehan Khan a976ca8d3f fix(http): honor disable-cookie with shared jars
Honor disable-cookie when a workflow supplies a shared cookie jar. Add an integration fixture proving that a cookie set by one request is not attached to a later request with cookie handling disabled.

Signed-off-by: Muhammad Rehan Khan <[email protected]>
Signed-off-by: Rehan Khan <[email protected]>
2026-08-25 16:06:26 +05:00
Mzack9999 6d6bfd7950 fix lint 2026-08-24 12:49:10 +04:00
dependabot[bot] 84b464e6bd chore(deps): bump the modules group with 4 updates (#7675)
Bumps the modules group with 4 updates: [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer), [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy), [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) and [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck).


Updates `github.com/projectdiscovery/fastdialer` from 0.5.16 to 0.5.17
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.16...v0.5.17)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.46 to 0.1.47
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.46...v0.1.47)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.93 to 0.2.94
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.93...v0.2.94)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.48 to 1.2.49
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.48...v1.2.49)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.94
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.49
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-24 06:17:16 +00:00
Doğan Can Bakır 85fb938c39 fix(templates): render regex in invalid template id error 2026-08-24 07:52:04 +03:00
coriatwood bb79d8ed71 build: bump Go version in Dockerfile (#7669) 2026-08-20 15:52:23 +07:00
buzhimingdeaikun 7a8da3cd00 Fix grammatical errors in README.md 2026-08-19 02:23:52 +08:00
dependabot[bot] 34866efc22 chore(deps): bump the modules group with 5 updates (#7664)
Bumps the modules group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.15` | `0.5.16` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.22` | `1.3.23` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.45` | `0.1.46` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.92` | `0.2.93` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.47` | `1.2.48` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.15 to 0.5.16
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.15...v0.5.16)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.22 to 1.3.23
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.22...v1.3.23)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.45 to 0.1.46
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.45...v0.1.46)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.92 to 0.2.93
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.92...v0.2.93)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.47 to 1.2.48
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.47...v1.2.48)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.46
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.93
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.48
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 06:18:28 +00:00
Dwi Siswanto fd0aff232e perf(templates): reuse parsed templates across engines (#7661)
* test(templates): add parse across engine local caches bench test

Signed-off-by: Dwi Siswanto <[email protected]>

* perf(templates): reuse parsed templates across engines

Clone cached parsed templates before compiling
them with engine-local options to avoid reopening
and decoding the same source for every engine
without sharing mutable compiled state.

Signed-off-by: Dwi Siswanto <[email protected]>

* fix(templates): preserve aliases when cloning templates

Use one visited map for the full clone and
register maps and slices before walking their
contents to preserve shared values and handles
cycles without infinite recursion.

Signed-off-by: Dwi Siswanto <[email protected]>

---------

Signed-off-by: Dwi Siswanto <[email protected]>
2026-08-17 01:47:00 +07:00
Mzack9999 265b3a3dec Merge pull request #7659 from dwisiswant0/dwisiswant0/fix/lib/avoid-caching-engine-state-in-shared-parsers
fix(lib): avoid caching engine state in shared parsers
2026-08-15 11:19:38 +04:00
Dwi Siswanto dc0ebd19a5 fix(lib): avoid caching engine state in shared parsers
Compiled template cache entries retain mutable
`ExecutorOptions` and can keep closed engines
alive.

Move those entries into an engine-local cache.
Leave the caller-owned parsed cache intact and
purge only compiled entries on `Close`.

Fixes #7656

Signed-off-by: Dwi Siswanto <[email protected]>
2026-08-14 15:03:03 +07:00
Mzack9999 05e0e8387b Merge pull request #7657 from lucasonline0/fix/7654-proxy-keepalive
fix(http): avoid proxy stalls from keep-alive reuse
2026-08-13 20:27:12 +04:00
Lucas Madureira f1007e48d3 fix(http): avoid proxy stalls from keep-alive reuse 2026-08-13 15:02:26 +00:00
Dogan Can Bakir a3f1367fcd Merge pull request #7627 from projectdiscovery/7626-raw-request-parsing
fix(input): parse raw request line and headers by name, not position
2026-08-12 01:50:55 +03:00
Dwi Siswanto 1027207f5e fix(raw): preserve abs targets in unsafe requests (#7652)
* fix(raw): preserve abs targets in unsafe requests

Keep absolute request targets unchanged on the
wire. Derive the effective URL from the scan
target authority and the raw target path and query,
without merging the input path or query. Also
handle mixed-case schemes and single-label hosts.

Fixes #7382
Fixes #7649

Signed-off-by: Dwi Siswanto <[email protected]>

* test(raw): add unsafe w/ full URL w/o path test

Signed-off-by: Dwi Siswanto <[email protected]>

---------

Signed-off-by: Dwi Siswanto <[email protected]>
2026-08-11 17:08:00 +07:00
dependabot[bot] 73be40cae7 chore(deps): bump crate-ci/typos in the workflows group (#7648)
Bumps the workflows group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.48.0 to 1.49.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.48.0...v1.49.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: workflows
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-11 17:07:18 +07:00
Dwi Siswanto 0df0976899 chore(deps): bump [email protected] (#7653)
Signed-off-by: Dwi Siswanto <[email protected]>
2026-08-11 17:06:40 +07:00
dependabot[bot] dd7153518e chore(deps): bump the modules group with 10 updates (#7647)
Bumps the modules group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/clistats](https://github.com/projectdiscovery/clistats) | `0.1.4` | `0.1.5` |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.14` | `0.5.15` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.21` | `1.3.22` |
| [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) | `0.8.20` | `0.8.21` |
| [github.com/projectdiscovery/gologger](https://github.com/projectdiscovery/gologger) | `1.1.71` | `1.1.72` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.44` | `0.1.45` |
| [github.com/projectdiscovery/tlsx](https://github.com/projectdiscovery/tlsx) | `1.2.2` | `1.3.2` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.91` | `0.2.92` |
| [github.com/projectdiscovery/blackrock](https://github.com/projectdiscovery/blackrock) | `0.0.1` | `0.0.2` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.46` | `1.2.47` |


Updates `github.com/projectdiscovery/clistats` from 0.1.4 to 0.1.5
- [Release notes](https://github.com/projectdiscovery/clistats/releases)
- [Commits](https://github.com/projectdiscovery/clistats/compare/v0.1.4...v0.1.5)

Updates `github.com/projectdiscovery/fastdialer` from 0.5.14 to 0.5.15
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.14...v0.5.15)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.21 to 1.3.22
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.21...v1.3.22)

Updates `github.com/projectdiscovery/dsl` from 0.8.20 to 0.8.21
- [Release notes](https://github.com/projectdiscovery/dsl/releases)
- [Commits](https://github.com/projectdiscovery/dsl/compare/v0.8.20...v0.8.21)

Updates `github.com/projectdiscovery/gologger` from 1.1.71 to 1.1.72
- [Release notes](https://github.com/projectdiscovery/gologger/releases)
- [Commits](https://github.com/projectdiscovery/gologger/compare/v1.1.71...v1.1.72)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.44 to 0.1.45
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.44...v0.1.45)

Updates `github.com/projectdiscovery/tlsx` from 1.2.2 to 1.3.2
- [Release notes](https://github.com/projectdiscovery/tlsx/releases)
- [Commits](https://github.com/projectdiscovery/tlsx/commits)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.91 to 0.2.92
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.91...v0.2.92)

Updates `github.com/projectdiscovery/blackrock` from 0.0.1 to 0.0.2
- [Release notes](https://github.com/projectdiscovery/blackrock/releases)
- [Commits](https://github.com/projectdiscovery/blackrock/compare/v0.0.1...v0.0.2)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.46 to 1.2.47
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.46...v1.2.47)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/clistats
  dependency-version: 0.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/dsl
  dependency-version: 0.8.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/gologger
  dependency-version: 1.1.72
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/tlsx
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.92
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/blackrock
  dependency-version: 0.0.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.47
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 06:19:34 +00:00
dependabot[bot] dfdfa5fd65 chore(deps): bump github.com/go-git/go-git/v5 (#7644)
Bumps the go_modules group with 1 update in the / directory: [github.com/go-git/go-git/v5](https://github.com/go-git/go-git).


Updates `github.com/go-git/go-git/v5` from 5.19.1 to 5.19.2
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.19.1...v5.19.2)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.2
  dependency-type: direct:production
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 05:55:28 +00:00
Sandeep Singh a8c88feb4a Merge pull request #7641 from projectdiscovery/dev
release prep
2026-08-08 17:50:01 +05:30
PDTeamX 5bbca25bea version update 2026-08-08 17:49:30 +05:30
Mzack9999 64bc83e004 Merge pull request #7635 from projectdiscovery/dependabot/github_actions/dev/workflows-84c3d95bcf
chore(deps): bump actions/stale from 10 to 11 in the workflows group
2026-08-05 14:07:54 +04:00
Mzack9999 37bba07662 Merge pull request #7636 from dwisiswant0/dwisiswant0/feat/templates/bind-javascript-execution-to-verified-content
feat(templates): bind js execution to verified content
2026-08-05 13:38:09 +04:00
Dwi Siswanto 9de96e4dda feat(templates): bind js execution to verified content
Verify signatures before protocol compilation so
unsigned init blocks cannot run, and enforce the
same check at request execution.

Bind cached results to the template, imported file
contents, and verifier key. Use the loaded import
snapshot for signing and verification, and apply
the executable-template re-signing rule to
JavaScript.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-08-04 10:24:11 +07:00
dependabot[bot] 4a6f7c5a86 chore(deps): bump the modules group with 3 updates (#7634)
Bumps the modules group with 3 updates: [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer), [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) and [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy).


Updates `github.com/projectdiscovery/fastdialer` from 0.5.13 to 0.5.14
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.13...v0.5.14)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.20 to 1.3.21
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.20...v1.3.21)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.43 to 0.1.44
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.43...v0.1.44)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-03 06:18:57 +00:00
Mzack9999 379687596f Normalize schemes 2026-08-03 10:14:40 +04:00
Mzack9999 890147c20e Merge pull request #7632 from projectdiscovery/chore/bump-kin-openapi
chore(deps): bump modules with known advisories
2026-08-03 10:09:12 +04:00
dependabot[bot] 89c2054bc1 chore(deps): bump actions/stale from 10 to 11 in the workflows group
Bumps the workflows group with 1 update: [actions/stale](https://github.com/actions/stale).


Updates `actions/stale` from 10 to 11
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: workflows
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-03 05:55:36 +00:00
Doğan Can Bakır 938b19d470 chore(deps): bump modules with known advisories
govulncheck on dev reports x/text and goldmark as reachable, and compress and
otel as imported. Remaining findings are the unmaintained x/crypto/openpgp
advisory, which has no fixed version and is reached only through go-github's
init, and stdlib patches that the build toolchain supplies.
2026-07-31 16:05:01 +03:00
Doğan Can Bakır 780e20b33a feat(input): add raw http request input mode
Fuzzing takes the request shape from the input, so a bare URL is always fuzzed
as a GET and anything else needed a spec, captured traffic or a hand-written
proxify document. -im http reads requests as they appear on the wire, which is
what Burp and browser devtools already put on the clipboard.
2026-07-31 15:28:36 +03:00
Doğan Can Bakır 49f1d49a1d fix(input): parse raw request line and headers by name, not position
The second line was always consumed as the Host line, so any request-shaped
input whose Host header sat elsewhere lost that header silently and skipped
fuzzing. Reading values by skipping one byte past the colon also truncated
unspaced values and panicked on valueless headers, crashing the scan. Absolute
request targets, as used by proxy captures, were appended to the authority
instead of replacing it.
2026-07-30 15:09:00 +03:00
Mzack9999 ba05210f16 Merge pull request #7616 from projectdiscovery/dwisiswant0/fix/loader/compile-requests-during-validation
fix(loader): compile requests during validation
2026-07-27 16:09:49 +04:00
Mzack9999 e557207b41 add nuclei/http js client (#7568)
* add http

* harden ntlm

* fix bounds
2026-07-27 15:47:02 +04:00
Mzack9999 4c7e1687ad rawhttp bump (#7593) 2026-07-27 15:46:12 +04:00
Dwi Siswanto 479c8187a5 fix(loader): compile requests during validation
Do not let parsed template cache hits bypass
protocol compilation, making `-validate` reject
templates with runtime compilation errors.

Fixes #7602

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-27 16:42:19 +07:00
ghost 7936ec36d8 chore(js): update memoized functions 🤖 2026-07-27 09:07:18 +00:00
Mzack9999 e6e558dc23 Improve MSSQL JS fingerprint via TDS prelogin (#7577)
* mssql fingerprint

* review fixes

* fix typo

* fix test
2026-07-27 13:07:03 +04:00
ghost 9d9012ce29 docs: update syntax & JSON schema 🤖 2026-07-27 08:14:48 +00:00
Mzack9999 671b875d23 Improve MySQL JS fingerprint with native handshake parsing (#7575)
* Native fingerprint

* fix typo
2026-07-27 12:10:26 +04:00
dependabot[bot] 367dfdb436 chore(deps): bump the modules group with 5 updates (#7615)
Bumps the modules group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/rawhttp](https://github.com/projectdiscovery/rawhttp) | `0.1.90` | `0.1.91` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.19` | `1.3.20` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.42` | `0.1.43` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.90` | `0.2.91` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.45` | `1.2.46` |


Updates `github.com/projectdiscovery/rawhttp` from 0.1.90 to 0.1.91
- [Release notes](https://github.com/projectdiscovery/rawhttp/releases)
- [Commits](https://github.com/projectdiscovery/rawhttp/compare/v0.1.90...v0.1.91)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.19 to 1.3.20
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.19...v1.3.20)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.42 to 0.1.43
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.42...v0.1.43)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.90 to 0.2.91
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.90...v0.2.91)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.45 to 1.2.46
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.45...v1.2.46)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/rawhttp
  dependency-version: 0.1.91
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.43
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.91
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.46
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 06:18:27 +00:00
ghost ad1447b329 chore(js): update memoized functions 🤖 2026-07-26 15:51:55 +00:00
Mzack9999 1f39f73e93 extend js smb for #4707 (#7565)
* extend smb

* enum services

* unify session

* bridge files

* document matrix

* enum processes

* test session

* test enums

* test bridge

* fix ci

* address review

* fix lint
2026-07-26 19:51:32 +04:00
Mzack9999 437897d054 Print config directories in -hc and verbose modes (#7578)
* config directories

* lint review
2026-07-26 17:57:29 +07:00
Mzack9999 31636218ee Merge pull request #7582 from projectdiscovery/5121-cli-leaktest
Add CLI runner goroutine leak test
2026-07-26 14:43:03 +04:00
Mzack9999 eb1bb88ce5 Merge pull request #7601 from DPS0340/fix/buildrequest-nil-request
fix: avoid panic in BuildRequest when request response has no request
2026-07-26 14:42:06 +04:00
Mzack9999 0ff225c1c8 review fixes 2026-07-26 14:24:08 +04:00
Mzack9999 5ba1d41108 merge dev 2026-07-26 14:22:29 +04:00
Jiho Lee 6f5a8ec5d4 fix: avoid panic in BuildRequest when request response has no request 2026-07-26 17:16:46 +09:00
ghost dd78cd5959 docs: update syntax & JSON schema 🤖 2026-07-26 08:04:11 +00:00
Mzack9999 4d4b88f6ca Merge pull request #7608 from projectdiscovery/dwisiswant0/feat/lib/reuse-metadata-cache-across-thread-safe-scans
feat(lib): reuse metadata cache across thread-safe scans
2026-07-26 11:59:53 +04:00
Mzack9999 e2e3574f59 Merge pull request #7609 from projectdiscovery/dependabot/go_modules/go_modules-715adc688b
chore(deps): bump github.com/getkin/kin-openapi from 0.132.0 to 0.144.0 in the go_modules group across 1 directory
2026-07-26 11:55:53 +04:00
Mzack9999 250c2a5fd7 Merge pull request #7586 from projectdiscovery/5440-http-tls-metadata
http tlsmeta
2026-07-26 11:53:07 +04:00
Mzack9999 06ff996374 serve errors 2026-07-26 11:04:52 +04:00
Mzack9999 62d6adde14 flake tests 2026-07-26 10:43:30 +04:00
Dwi Siswanto 16e4a7acc9 fix(openapi): exclusive bound checks
kin-openapi now represents exclusive minimum and
maximum as `ExclusiveBound` values. Use `IsTrue()`
to preserve their boolean semantics when
generating numeric examples.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-26 13:03:16 +07:00
dependabot[bot] c6ef995875 chore(deps): bump github.com/getkin/kin-openapi
Bumps the go_modules group with 1 update in the / directory: [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi).


Updates `github.com/getkin/kin-openapi` from 0.132.0 to 0.144.0
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.132.0...v0.144.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.144.0
  dependency-type: direct:production
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-26 04:31:18 +00:00
Mzack9999 89d43000cf cli: wire -max-time soft termination via goflags (#7589)
* max time

* goflags bump
2026-07-26 11:29:23 +07:00
Mzack9999 63bfe19f40 docs: missing CLI flags in README (#7596) 2026-07-26 11:27:33 +07:00
Dwi Siswanto f239d6e7c6 feat(lib): reuse metadata cache across thread-safe scans
Keep one engine-owned metadata index across SDK
calls and persist it at shutdown instead of
loading and saving a new index for each execution.

Track dirty state and parser validation so
compatible metadata can skip redundant filtering
parses w/o trusting lax entries in strict mode.
Defensively copy cached values, invalidate stale
entries synchronously, and remove the unused
ephemeral engine allocation.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-26 11:11:10 +07:00
Dwi Siswanto 6e990151b4 test(lib): reuse metadata index in BenchmarkExecuteNucleiWithOptsCtx
Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-26 11:10:47 +07:00
Dwi Siswanto 6b66f91147 test(lib): add BenchmarkExecuteNucleiWithOptsCtx
Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-26 11:10:07 +07:00
Mzack9999 60f73840e5 typos whitelist 2026-07-25 12:26:06 +04:00
Mzack9999 6ecdb94740 Merge pull request #7587 from projectdiscovery/5651-sdk-result-callback
sdk: per-execution result callback for thread-safe engine
2026-07-25 12:23:27 +04:00
dependabot[bot] 7cab28d225 chore(deps): bump google.golang.org/grpc (#7594)
Bumps the go_modules group with 1 update in the / directory: [google.golang.org/grpc](https://github.com/grpc/grpc-go).


Updates `google.golang.org/grpc` from 1.79.3 to 1.82.1
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.79.3...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: direct:production
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 03:15:52 +00:00
Mzack9999 da2e6910d2 sdk callback 2026-07-24 17:22:54 +04:00
Mzack9999 a12f7fb110 http tlsmeta 2026-07-24 16:47:11 +04:00
Mzack9999 c2ac1d859f passive exports 2026-07-24 16:33:16 +04:00
Mzack9999 35a87ba411 cli leaktest 2026-07-24 15:40:24 +04:00
ghost bcf20899ec docs: update syntax & JSON schema 🤖 2026-07-24 06:08:04 +00:00
Mzack9999 44e6f5445a dnssec types (#7564) 2026-07-24 10:06:17 +04:00
Dwi SiswantoandMzack9999 6d81cc3b62 fix(index): limit include-tags to matching tag exclusions (#7536)
* fix(index): limit include-tags to matching tag exclusions

IncludeTags in the metadata index was treated as
a forced include, so `-itags` could bypass
explicit ID/path filters and other include
criteria, while also let a template thru when it
had both an included tag and a different excluded
tag.

Match the parsed-template filter instead: an
included tag only cancels exclusion for that same
tag. Other exclusions and normal include filters
still apply, while IncludeTemplates remains the
explicit path override.

Fixes #7534

Signed-off-by: Dwi Siswanto <[email protected]>

* Skip include-tags functional parity until release

---------

Signed-off-by: Dwi Siswanto <[email protected]>
Co-authored-by: Mzack9999 <[email protected]>
2026-07-23 19:23:01 +07:00
Mzack9999 2710ba232b Improving lua script with args and values (#6561)
* improving lua script with args and values

* fixing comments

* keys to string

* fixing context

* fix(test): drop log() calls inflating redis-lua-script result count

The two log() calls each emitted a [JS] stdout line that the integration
harness counted as a result, yielding 3 results vs the expected 1. The
matcher reads `response` from the final expression value (not stdout), so
returning the get RunLuaScript call directly keeps the assertion intact
with a single result.

* fix flaky

* harden sdk tests

Avoid shared templates-dir update races across parallel go test packages and ignore known leveldb/ratelimit/memguardian goroutines in goleak.

* fix sdk templates

Keep goleak ignores, drop DisableUpdateCheck so CI can install templates, and serialize UpdateIfOutdated across processes.
2026-07-22 23:41:23 +04:00
stringsbuilderandMzack9999 ce470c32f8 refactor: replace Split in loops with more efficient SplitSeq (#7278)
* refactor: replace Split in loops with more efficient SplitSeq

Signed-off-by: stringsbuilder <[email protected]>

* minor refactor

* strict checksum

* fix typo

* harden sdk tests

Avoid shared templates-dir update races across parallel go test packages and ignore known leveldb/ratelimit/memguardian goroutines in goleak.

* bind splitseq temps

Keep SplitSeq inputs in locals so iterator substrings cannot outlive a temporary string expression.

* fix sdk templates

Keep goleak ignores, drop DisableUpdateCheck so CI can install templates, and serialize UpdateIfOutdated across processes.

---------

Signed-off-by: stringsbuilder <[email protected]>
Co-authored-by: Mzack9999 <[email protected]>
2026-07-22 23:18:56 +04:00
ghost 17092671a8 docs: update syntax & JSON schema 🤖 2026-07-22 14:24:39 +00:00
Dwi SiswantoandMzack9999 679ee5ef3a feat(protocols): add duration fields to other events (#7428)
* feat(protocols): add duration fields to other events

Add matcher and extractor duration data[1] for
DNS, SSL, WHOIS, TCP, WebSocket, and Headless
requests.

Track step durations for TCP inputs, WebSocket
message exchanges, and Headless actions. The
`duration` field holds the most recently measured
operation.

Expose request ID-prefixed duration values through
the existing template context path, and add
protocol tests for duration extraction, aliases,
and request ID handling.

[1]: The data exposed is `duration`, `duration_N`,
    `<request-id>_duration`, and
    `<request-id>_duration_<N>`.

Closes #5413
Closes #7422

Signed-off-by: Dwi Siswanto <[email protected]>

* test(protocols): added deterministic 10ms test-server delays

Signed-off-by: Dwi Siswanto <[email protected]>

* test: added a shared integration-test delay constant

Signed-off-by: Dwi Siswanto <[email protected]>

---------

Signed-off-by: Dwi Siswanto <[email protected]>
Co-authored-by: Mzack9999 <[email protected]>
2026-07-22 18:22:58 +04:00
james bobandMzack9999 be21943ee4 fix: stop busy-spinning hang monitor (#7559)
* fix(monitor): stop busy-spinning hang monitor

* test(integration): serialize dsl matcher variable case

* drop serial

---------

Co-authored-by: Mzack9999 <[email protected]>
2026-07-22 17:39:03 +04:00
SynvoyaandSynvoya f447c23a66 Fix case-insensitive word matcher missing dynamic (rendered) values (#7523)
MatchWords lowercased the corpus and literal words but not the rendered template word, so a case-insensitive word matcher backed by a {{var}} that resolved to an uppercase value never matched (false negative). Lowercase the rendered word when CaseInsensitive is set.

Co-authored-by: Synvoya <[email protected]>
2026-07-22 16:59:30 +04:00
james bob 6d7a0fa435 fix(progress): avoid panic for disabled stats interval (#7557) 2026-07-21 19:11:36 +07:00
dependabot[bot] 4e422a9308 chore(deps): bump the modules group with 3 updates (#7554)
Bumps the modules group with 3 updates: [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go), [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) and [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck).


Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.18 to 1.3.19
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.18...v1.3.19)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.89 to 0.2.90
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.89...v0.2.90)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.44 to 1.2.45
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.44...v1.2.45)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.90
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.45
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 14:01:43 +07:00
dependabot[bot] a360efc658 chore(deps): bump github.com/go-jose/go-jose/v4 (#7555)
Bumps the go_modules group with 1 update in the / directory: [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose).


Updates `github.com/go-jose/go-jose/v4` from 4.1.3 to 4.1.4
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 06:47:38 +00:00
dependabot[bot] 435ae4c042 chore(deps): bump crate-ci/typos (#7529)
Bumps the workflows group with 1 update in the / directory: [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `crate-ci/typos` from 1.47.2 to 1.48.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.47.2...v1.48.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: workflows
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 06:18:35 +00:00
Zain Nadeem 0888e6244d fix(input): guard raw request body newline trimming (#7525) 2026-07-18 05:36:23 +07:00
james bobandjames-yusuke c928dcc652 fix(fuzz): preserve form parameters with shared prefixes (#7515)
Co-authored-by: james-yusuke <[email protected]>
2026-07-17 01:13:08 +07:00
Arpit JainandMzack9999 8171606055 Guard against a negative regex extractor group to avoid an index-out-of-range panic (#7531)
* Guard against a negative regex extractor group to avoid an index-out-of-range panic

A community template with group: -1 crashes the scan since the protocol path
has no recover. ExtractRegex indexes match[e.RegexGroup], and the len(match) <
groupPlusOne guard never catches a negative group, so match[-1] panics with
index out of range [-1]. A negative group is meaningless, so return no results
early instead.

Signed-off-by: Arpit Jain <[email protected]>

* Harden extractors

---------

Signed-off-by: Arpit Jain <[email protected]>
Co-authored-by: Mzack9999 <[email protected]>
2026-07-16 12:51:13 +04:00
Dwi Siswanto a1291fa1ab feat(headless): render element locators before lookup (#7549)
Render locator arguments thru the action renderer
for every element lookup mode (selector, regex,
XPath, JavaScript, and search locators). Keep
rendered values local and reuse selectors instead
of rendering them twice.

Render `waitvisible` locators on the owning page
so Interactsh URLs remain attached to the request.
Use a local URL source in tests to avoid network
access.

Closes #7542

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-16 11:18:48 +04:00
Dwi Siswanto bcd580703d fix(signer): avoid deprecated ECDSA coordinate access (#7537)
Build the signer fragment from `PublicKey.Bytes()`
instead of reading the raw ECDSA X coordinate.

Keep the existing fragment format by hashing the
trimmed X-coordinate bytes, and propagate key
encoding errors thru signing & verification.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-15 18:21:22 +04:00
dependabot[bot] 9c47e6c122 chore(deps): bump the go_modules group across 1 directory with 2 updates (#7533)
Bumps the go_modules group with 1 update in the / directory: [google.golang.org/grpc](https://github.com/grpc/grpc-go).


Updates `google.golang.org/grpc` from 1.72.2 to 1.79.3
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.2...v1.79.3)

Updates `github.com/go-jose/go-jose/v4` from 4.0.5 to 4.1.3
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.5...v4.1.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: direct:production
  dependency-group: go_modules
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.3
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 06:53:15 +00:00
dependabot[bot] 23e7624f04 chore(deps): bump the modules group with 5 updates (#7532)
Bumps the modules group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.12` | `0.5.13` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.17` | `1.3.18` |
| [github.com/projectdiscovery/httpx](https://github.com/projectdiscovery/httpx) | `1.9.0` | `1.10.0` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.88` | `0.2.89` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.43` | `1.2.44` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.12 to 0.5.13
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.12...v0.5.13)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.17 to 1.3.18
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.17...v1.3.18)

Updates `github.com/projectdiscovery/httpx` from 1.9.0 to 1.10.0
- [Release notes](https://github.com/projectdiscovery/httpx/releases)
- [Commits](https://github.com/projectdiscovery/httpx/compare/v1.9.0...v1.10.0)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.88 to 0.2.89
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.88...v0.2.89)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.43 to 1.2.44
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.43...v1.2.44)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/httpx
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.89
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.44
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 06:21:42 +00:00
Mzack9999 85723be139 gRPC JavaScript library (#7527)
* grpc library

* stabilize test
2026-07-08 01:55:14 +07:00
dependabot[bot] 25587f94b1 chore(deps): bump the modules group with 5 updates (#7528)
Bumps the modules group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.11` | `0.5.12` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.16` | `1.3.17` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.41` | `0.1.42` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.2.87` | `0.2.88` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.2.42` | `1.2.43` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.11 to 0.5.12
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.11...v0.5.12)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.16 to 1.3.17
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.16...v1.3.17)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.41 to 0.1.42
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.41...v0.1.42)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.87 to 0.2.88
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.2.87...v0.2.88)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.42 to 1.2.43
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.2.42...v1.2.43)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.42
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.88
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.43
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 06:20:03 +00:00
Sandeep Singh 57db383502 Merge pull request #7522 from projectdiscovery/dev
v3.11.0 (security hardening for javascript protocol)
2026-07-06 11:41:09 +05:30
PDTeamX e4a83ad38d version update 2026-07-03 19:02:17 +05:30
Dwi Siswanto fac0b7196d feat(templates): require signatures for javascript templates (#7514)
* feat(templates): require signatures for javascript templates

Unsigned javascript templates should not be
accepted into execution paths when template
signing is enforced.

Apply the signature check to javascript protocol
templates in both direct loading and workflows,
and report those skips separately from code
templates.

Signed-off-by: Dwi Siswanto <[email protected]>

* fix(signer): normalize line endings when signing

The verifier already canonicalizes CRLF to LF
before checking the digest, but the signer still
hashed the raw file content. Templates signed from
a CRLF checkout could fail verification later,
which showed up on Windows after javascript
fixtures started being signed in the integration
harness.

Use the same canonical content for signing and
verification, and add a CRLF sign-and-verify
regression.

Signed-off-by: Dwi Siswanto <[email protected]>

---------

Signed-off-by: Dwi Siswanto <[email protected]>
2026-07-03 05:50:04 +05:30
Mzack9999 1427b35cdb General maintenance and cleanup (#7469)
* general maintenance

* address review
2026-07-03 05:48:17 +05:30
Dogan Can Bakir ffd6e0b18d chore(deps): bump x/crypto to v0.52.0 and go-pkcs12 to v0.7.2 (#7516) 2026-07-03 00:55:07 +05:30
281 changed files with 18494 additions and 1873 deletions

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
name: Maintain security context
on:
workflow_dispatch:
schedule:
- cron: "17 4 * * 1"
permissions:
contents: write
pull-requests: write
concurrency:
group: security-context
cancel-in-progress: true
jobs:
refresh:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
# The API analyzes the repository default branch, so use that branch for
# both scheduled and manually dispatched updates.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.repository.default_branch }}
# Analyze this commit on securitycontext.dev, wait for the result, and
# download it. The run's own token authorizes private repositories —
# no secrets to configure.
- name: Generate SECURITY_CONTEXT.md
id: context
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
sc() { curl --fail-with-body -sS -H "X-GitHub-Token: $GITHUB_TOKEN" "$@"; }
base="https://securitycontext.dev"
head="$(git rev-parse HEAD)"
sc -X POST "$base/api/v1/context?wait=60" -H "Content-Type: application/json" \
-H "X-Secc-Client: github-action" --data "{\"repo\":\"$GITHUB_REPOSITORY\",\"head\":\"$head\"}" >/dev/null
for _ in {1..120}; do
state="$(sc "$base/api/v1/context/$GITHUB_REPOSITORY")"
status="$(jq -r .status <<<"$state")"
if [ "$status" = "error" ]; then
jq -r '.error // "analysis failed"' <<<"$state" >&2
exit 1
fi
if [ "$status" = "ready" ] && [ "$(jq -r '.summary.head_sha // empty' <<<"$state")" = "$head" ]; then
sc -L "$base/r/$GITHUB_REPOSITORY.md" -o SECURITY_CONTEXT.md
# A private report is access-gated on the web, so the pull request
# links the rendered file on this branch instead.
jq -r '"visibility=" + (.visibility // "public")' <<<"$state" >> "$GITHUB_OUTPUT"
exit 0
fi
sleep 15
done
echo "Timed out waiting for analysis of $head" >&2
exit 1
# A pull request should mean the analysis changed. When only the volatile
# header moved (timestamps, analyzed commit, commit counts), restore the
# committed file so no PR is opened and any header-only PR closes.
- name: Skip the update unless the analysis changed
id: report
run: |
changed=true
if [ -n "$(git ls-files SECURITY_CONTEXT.md)" ] && git diff --quiet -I '^<!-- Generated' -I '^_Generated' -I '^_Analyzed commit' -I '^- Commits analyzed' -I '^- Commits flagged' -- SECURITY_CONTEXT.md; then
git checkout -- SECURITY_CONTEXT.md
changed=false
fi
echo "changed=$changed" >> "$GITHUB_OUTPUT"
# Point every supported root agent-instructions file at the context.
# Create AGENTS.md as the cross-agent default when the repo has none.
- name: Reference the context from agent instructions
id: agents
env:
REPORT_CHANGED: ${{ steps.report.outputs.changed }}
run: |
line="Before writing or reviewing code, read SECURITY_CONTEXT.md. Check new and modified code against its historical fixes and recurring vulnerability patterns. Do not reintroduce a previously fixed issue or a similar variant."
touched=""
paths="SECURITY_CONTEXT.md"
instruction_files=""
for f in AGENTS.md CLAUDE.md; do
if [ -f "$f" ]; then instruction_files="${instruction_files} $f"; fi
done
if [ -z "$instruction_files" ]; then
printf '%s\n' '# Agent instructions' > AGENTS.md
instruction_files="AGENTS.md"
fi
for f in $instruction_files; do
paths="$(printf '%s\n%s' "$paths" "$f")"
if grep -q "SECURITY_CONTEXT.md" "$f"; then continue; fi
printf '\n%s\n' "$line" >> "$f"
if [ -n "$touched" ]; then touched="$touched and $f"; else touched="$f"; fi
done
echo "touched=$touched" >> "$GITHUB_OUTPUT"
# Title the pull request for what it actually carries: when the
# analysis is unchanged, the only edit is the agent-instructions line.
if [ "$REPORT_CHANGED" = "false" ]; then
echo "subject=chore: reference security context from agent instructions" >> "$GITHUB_OUTPUT"
else
echo "subject=chore: refresh security context" >> "$GITHUB_OUTPUT"
fi
{
echo "paths<<SC_EOF"
echo "$paths"
echo "SC_EOF"
} >> "$GITHUB_OUTPUT"
- name: Open an update pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
branch: security-context/refresh
commit-message: ${{ steps.agents.outputs.subject }}
title: ${{ steps.agents.outputs.subject }}
body: |
Refreshes `SECURITY_CONTEXT.md`, this repository's security-fix history as working context, so agents and reviewers don't reintroduce fixes that already shipped.
${{ steps.context.outputs.visibility == 'private' && format('[Read the updated file]({0}/{1}/blob/security-context/refresh/SECURITY_CONTEXT.md)', github.server_url, github.repository) || format('[Full report for this repository](https://securitycontext.dev/r/{0})', github.repository) }}
${{ steps.agents.outputs.touched != '' && format('Also adds a one-line reference to {0} so your coding agent picks this up automatically.', steps.agents.outputs.touched) || 'The repository agent instructions already reference `SECURITY_CONTEXT.md`.' }}
---
_Your agent doesn't know this repo's security history. [Security Context](https://securitycontext.dev) keeps it where your agent can see it, so old vulnerabilities and their variants don't come back in new code._
add-paths: ${{ steps.agents.outputs.paths }}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
- uses: actions/stale@v11
with:
days-before-stale: 90
days-before-close: 7
+1 -1
View File
@@ -17,4 +17,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: crate-ci/[email protected]7.2
- uses: crate-ci/[email protected]9.0
+2 -1
View File
@@ -81,4 +81,5 @@ Each protocol (HTTP, DNS, Network, etc.) implements:
- **internal/tests/functional/** - CI-only native functional comparison suite and testcase assets
- **pkg/fuzz/** - Fuzzing engine and DAST capabilities
- **pkg/input/** - Input processing for various formats (Burp, OpenAPI, etc.)
- **pkg/reporting/** - Result export and issue tracking integrations
- **pkg/reporting/** - Result export and issue tracking integrations
See SECURITY_CONTEXT.md for this repo's known vulnerabilities and recurring weak spots. Check it before writing or reviewing security-sensitive code.
+1 -1
View File
@@ -1,5 +1,5 @@
# Build
FROM golang:1.24-alpine AS builder
FROM golang:1.27-alpine AS builder
RUN apk add build-base
WORKDIR /app
+11 -5
View File
@@ -64,11 +64,11 @@
### **1. Nuclei CLI**
_Install Nuclei on your machine. Get started by following the installation guide [**`here`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme). Additionally, We provide [**`a free cloud tier`**](https://cloud.projectdiscovery.io/sign-up) and comes with a generous monthly free limits:_
_Install Nuclei on your machine. Get started by following the installation guide [**`here`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme). Additionally, we provide [**`a free cloud tier`**](https://cloud.projectdiscovery.io/sign-up) that comes with generous monthly free limits:_
- Store and visualize your vulnerability findings
- Write and manage your nuclei templates
- Access latest nuclei templates
- Write and manage your Nuclei templates
- Access the latest Nuclei templates
- Discover and store your targets
> [!Important]
@@ -91,7 +91,7 @@ _For security teams and enterprises, we provide a cloud-hosted service built on
- We're constantly [**`adding new features`**](https://feedback.projectdiscovery.io/changelog)!
- **Ideal for:** Pentesters, security teams, and enterprises
[**`Sign up to Pro`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) or [**`Talk to our team`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) if you have large organization and complex requirements.
[**`Sign up to Pro`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) or [**`Talk to our team`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) if you have a large organization and complex requirements.
<br>
<br>
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+7 -1
View File
@@ -140,15 +140,18 @@ Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
-targets-inline string inline multiline target list (for use in template profiles)
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-ro, -required-only use only required fields in input format when generating requests
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
TEMPLATES:
-nt, -new-templates run only new templates added in latest nuclei-templates release
@@ -205,6 +208,7 @@ 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
-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
CONFIGURATIONS:
@@ -277,6 +281,7 @@ UNCOVER:
RATE-LIMIT:
-rl, -rate-limit int maximum number of requests to send per second (default 150)
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
@@ -301,6 +306,7 @@ OPTIMIZATIONS:
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
-irt, -input-read-timeout value timeout on input read (default 3m0s)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
+302
View File
@@ -0,0 +1,302 @@
<!-- Generated by securitycontext.dev. Source HEAD: b04fc59ad056c3d818aa103a85164c633e856277. -->
# 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.
_Generated 2026-09-03 00:44 UTC by secc_
_Analyzed commit: b04fc59ad056c3d818aa103a85164c633e856277_
**Remediation coverage: 26.5%**
_9 of 34 historical fixes have no remaining unguarded variant; 522 open remediation leads to verify. Measures fix consistency, not a vulnerability verdict._
- Commits analyzed: **4820**
- Commits flagged for review: **415**
- Confirmed historical security fixes: **38**
- Known published CVEs: **6**
- Project type: **client**, excluding 34 availability-only (DoS/crash) finding(s) as out-of-scope for a non-server project
## Summary
Nuclei's historical security posture is dominated by sandbox and trust-boundary enforcement failures: template-driven code execution, local file access, and network egress are repeatedly gated inconsistently across loaders, protocol runtimes, and JS helper libraries. Signature verification bypasses (CVE-2024-43405, CVE-2024-27920) and expression/DSL injection (CVE-2026-41645, CVE-2026-41282) show that untrusted template content and response data are persistently re-evaluated or executed. Path traversal and arbitrary file write/read issues cluster in file-loading and output-writing paths, often due to weak prefix checks or missing sandbox flags. The most critical theme is asymmetric enforcement: a guard applied on one entry point (e.g., direct template load) is frequently missing on sibling paths (workflows, JS runtime, protocol sinks, helper libraries).
## Recurring Security Patterns
**18.4% of classified fixes repeat a previously fixed pattern** (7 recurring, 31 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
- **Code Execution / Signature Bypass:** [fac0b7196d](https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37) → [9de96e4dda](https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76) — matched on `CWE-347`, `component:pkg/catalog/loader/loader.go`
- **Auth Bypass:** [1c440e755a](https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645) → [fac0b7196d](https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37) — matched on `CWE-347`, `component:pkg/catalog/loader/loader.go`
- **SSRF:** [a167e6c57b](https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770) → [9def9af7e3](https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572) — matched on `class:ssrf`, `CWE-918`, `component:pkg/js/libs/ldap/ldap.go`
- **Path Traversal:** [e510bc207f](https://github.com/projectdiscovery/nuclei/commit/e510bc207f4336058f7b1536ddbb3942b394b05c) → [f663d1c9cf](https://github.com/projectdiscovery/nuclei/commit/f663d1c9cf77da53533bf328750621b4a9fa996d) — matched on `class:path traversal`, `CWE-22`, `sink:os openfile`
- **Path Traversal:** [e7fb40a413](https://github.com/projectdiscovery/nuclei/commit/e7fb40a41308f6c41267e858d1781eb67f7e0572) → [e5154d362a](https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648) — matched on `class:path traversal`, `CWE-22`, `component:v2/pkg/protocols/common/generators`
- **Insecure File Permissions:** [4bccb6cf8a](https://github.com/projectdiscovery/nuclei/commit/4bccb6cf8a830724b3bda624cb3bfe43eff31683) → [af4854f90d](https://github.com/projectdiscovery/nuclei/commit/af4854f90de728bb08689d5cae3dec4acfdb6b9a) — matched on `CWE-732`, `sink:os openfile`
- **Path Traversal:** [f9c214a66f](https://github.com/projectdiscovery/nuclei/commit/f9c214a66f37a00fc29fe64232b690e6285ec59c) → [e510bc207f](https://github.com/projectdiscovery/nuclei/commit/e510bc207f4336058f7b1536ddbb3942b394b05c) — matched on `class:path traversal`, `CWE-22`, `sink:os openfile`
## Top Risks
### 1. Code Execution / Signature Bypass: `pkg/catalog/loader/loader.go, pkg/templates/compile.go, pkg/protocols/javascript/js.go` (critical)
Unsigned or unverified templates with JavaScript init blocks were compiled and executed; verification results were not bound to content digests, allowing cache reuse of stale verification. This is the root of multiple RCE and signature bypass CVEs.
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, CVE-2024-43405, CVE-2024-27920
### 2. Remote Code Execution: `pkg/protocols/code, pkg/protocols, pkg/templates/workflows.go` (high)
Code protocol execution sink lacked gating for signature verification and the -code flag; workflows could load unsigned code templates; dynamic runtime values were recursively evaluated as DSL expressions. Multiple independent paths to local code execution.
Evidence: 1427b35cdbcb11a63c338700fe8e228ada8ec59d, ccbfb12bd01447ba25f6e755dfb2bee677736da6, e86f38299765b82ad724fdb701557e0eaff3884d
### 3. Path Traversal: `pkg/catalog/config, pkg/js/compiler, v2/pkg/protocols/common/generators, v2/internal/runner/update.go` (high)
Weak lexical prefix checks allowed sibling paths to bypass containment; JS runtime allowed local file reads via require(); payload loading and template updater lacked path canonicalization, enabling arbitrary file read/write and Zip Slip.
Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, e5154d362af900e83b30fb79028bfab1a7f0c648, f9c214a66f37a00fc29fe64232b690e6285ec59c
### 4. Expression Language Injection: `pkg/protocols/common/expressions` (high)
Template-authored expressions were parsed after placeholder substitution, allowing untrusted response-derived data to be re-evaluated as DSL expressions. Related CVEs confirm multi-step template injection with -env-vars.
Evidence: d2217320162d5782ca7cb95bef9dda17063818f3, CVE-2026-41645, CVE-2026-41282
### 5. SSRF: `pkg/js/libs/ldap/ldap.go, pkg/js/libs/smb/smb.go` (high)
LDAP host validation checked the domain instead of the actual target address; SMB dialing lacked host allowlist checks. JS helper libraries bypassed the global network policy.
Evidence: a167e6c57b80f5dc7be231231b62d7c594ee2770, a677fca192ee732a7b0a278f725517fa998b7897
### 6. Sandbox Escape: `v2/pkg/protocols/common/generators/validate.go, v2/internal/runner/processor.go` (high)
Go-template payload generators and Tengo scripting in workflows allowed unsafe functions and arbitrary imports, escaping the intended sandbox and enabling system command execution.
Evidence: aefa2717f7d6d1702e3008ac18ebdcc3660dc256, 96049a32fa70aef0483d4180d386a57b3c2d973a
## Shared Attack Surfaces
Capabilities guarded by more than one past fix on different code paths. A guard enforced on one entry point is often missing on a sibling; verify each alternate path before assuming it's covered.
### 1. Privileged-type opt-in for template execution (code, JavaScript, self-contained, file-protocol, DAST)
- Guard that should hold everywhere: signature verification (Verify, parsed.IsUnsignedJavascriptTemplate), -code flag, parsed.SelfContained flag, parsed.HasFileProtocol() flag, DASTServerToken length >= 16
- Entry points: `pkg/catalog/loader/loader.go`, `pkg/templates/compile.go`, `pkg/protocols/code`, `pkg/protocols/javascript/js.go`, `pkg/templates/workflows.go`
- Check: Verify the workflow sub-template loader enforces signature verification and the -code flag for code templates, AND the self-contained and file-protocol opt-ins, not just the code-template one. Also check the DAST-mode loader path for the same full set.
- Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, fac0b7196d2fbca56c8af8ff8e1899c111041a37, 1427b35cdbcb11a63c338700fe8e228ada8ec59d, 1c440e755a97471c56fb0276ee8a8c4132974645, 63687c2ce07e464f6b38f797d81a8b92597f6ef9, e86f38299765b82ad724fdb701557e0eaff3884d
### 2. Local file access sandbox (LFA) across JS runtime, payloads, extractors, headless
- Guard that should hold everywhere: path canonicalization + prefix check, LfaAllowed flag, template-directory confinement, file:// opt-in, FilesInput opt-in
- Entry points: `pkg/js/compiler`, `pkg/js/global/scripts.go`, `v2/pkg/protocols/common/generators`, `pkg/operators/extractors`, `v2/pkg/protocols/headless/engine/page_actions.go`, `pkg/protocols/common/protocolstate/file.go`
- Check: Inspect the JS runtime's require() source loader and the headless page-actions path: they must enforce the same canonical path containment and LFA flag as the payload generator and extractor paths.
- Evidence: 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, e5154d362af900e83b30fb79028bfab1a7f0c648, e7fb40a41308f6c41267e858d1781eb67f7e0572, f663d1c9cf77da53533bf328750621b4a9fa996d, d3928e080d819f25306bfbaecc399f03516db5aa, 5b89811b906f086e47fdf5357a5ee367f8f1daef
### 3. Network egress policy (host allowlist) for JS helper protocols and remote template loading
- Guard that should hold everywhere: protocolstate.IsHostAllowed, dialers.Fastdialer.Dial/DialTLSWithConfig, remote-template-domain allowlist
- Entry points: `pkg/js/libs/ldap/ldap.go`, `pkg/js/libs/smb/smb.go`, `pkg/js/libs/mysql/mysql.go`, `v2/pkg/catalog/loader`
- Check: Verify the MySQL DSN and LDAP dial paths check the actual target host (not just a derived domain) and that the remote-template loader applies the domain allowlist before http.Get, matching the SMB dial check.
- Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, a677fca192ee732a7b0a278f725517fa998b7897, 9611926a9a1ca99e8ed554d1b26fbeff60e06368, d1303797c0e5f88343a1f2ed781955dd086df03d
### 4. Template expression evaluation boundary (response data vs. template DSL)
- Guard that should hold everywhere: collect expressions before placeholder substitution (FindExpressions), treat runtime values as literal data (expressions.Evaluate)
- Entry points: `pkg/protocols/common/expressions`, `pkg/protocols`
- Check: Inspect multi-step template processing where HTTP response data flows into expression evaluation: ensure expressions are collected before substitution on every path, including headless and JavaScript protocol steps.
- Evidence: d2217320162d5782ca7cb95bef9dda17063818f3, ccbfb12bd01447ba25f6e755dfb2bee677736da6
## Historical Vulnerability Classes
| Class | Count |
|---|---|
| Path Traversal | 10 |
| Auth Bypass | 6 |
| Remote Code Execution | 3 |
| Sandbox Escape | 3 |
| SSRF | 2 |
| Arbitrary Code Execution | 1 |
| Arbitrary File Read | 1 |
| Code Execution | 1 |
| Code Execution / Signature Bypass | 1 |
| Cryptographically Weak PRNG | 1 |
| Expression Language Injection | 1 |
| Information Disclosure | 1 |
| Insecure File Permissions | 1 |
| Insecure Permissions | 1 |
| Race Condition | 1 |
| SQL Injection | 1 |
| Sandbox Bypass | 1 |
| Server-Side Request Forgery | 1 |
| Signature Bypass | 1 |
## Fixed-Issue Fingerprints
These are real security fixes mined from git history. Use them as regression checks.
- **Code Execution / Signature Bypass** (critical) [CWE-347]: This commit fixes a signature verification bypass that allowed unsigned templates with JavaScript init blocks to be compiled and executed, by binding verification results to content digests and enforcing verification before compilation.
- sink: `Template compilation and JavaScript execution of unsigned or unverified templates`
- fix: Signature verification enforced before protocol compilation and at request execution; verification result bound to content digest and verifier fingerprint to prevent cache reuse of stale verification.
- commit: https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76 (2026-08-04)
- **Auth Bypass** (high) [CWE-347]: Enforce template signature verification for javascript-protocol templates before loading and executing them.
- sink: `execution path`
- fix: signature check for javascript templates
- commit: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37 (2026-07-03)
- **Remote Code Execution** (high) [CWE-94]: Enforce signature verification and the code execution flag (-code) directly within the code protocol execution sink to prevent execution of untrusted templates via indirect SDK/API paths.
- sink: `ExecuteWithResults`
- fix: gated check for enabled and signature-verified templates
- commit: https://github.com/projectdiscovery/nuclei/commit/1427b35cdbcb11a63c338700fe8e228ada8ec59d (2026-07-03)
- **Remote Code Execution** (high) [CWE-94]: Fixes a template-injection risk where dynamic runtime values/extracted payloads were recursively evaluated as DSL expressions instead of being treated as literal data.
- sink: `expressions.Evaluate`
- fix: template rendering boundary
- commit: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6 (2026-06-30)
- **Path Traversal** (medium) [CWE-22]: Enforce Nuclei's local file sandbox restrictions on the Kerberos credential cache (ccache) output paths in krbforge helper to prevent arbitrary file writes.
- sink: `github.com/Mzack9999/goimpacket/pkg/kerberos.CreateTicket`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122 (2026-06-24)
- **SSRF** (medium) [CWE-918]: Enforce the configured network policy and local network restrictions on LDAP/LDAPS/LDAPI protocols prior to dialing hosts.
- sink: `dialers.Fastdialer.Dial`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572 (2026-06-24)
- **Arbitrary File Read** (high) [CWE-200]: Enforces local file access sandbox restrictions on MySQL connections by disabling the allowAllFiles option in the DSN unless the local file access (-lfa) flag is set.
- sink: `sql.Open`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/9611926a9a1ca99e8ed554d1b26fbeff60e06368 (2026-06-19)
- **Code Execution** (high) [CWE-347]: Fixes a signature validation bypass that allowed unsigned Code templates to execute arbitrary local commands when run under DAST mode.
- sink: `LoadTemplatesWithTags`
- fix: signature verification before execution
- commit: https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645 (2026-06-19)
- **Path Traversal** (medium) [CWE-22]: Fixes path containment validation vulnerabilities across several filesystem boundaries by replacing weak lexical prefix checks (which allowed sibling paths of trusted directories to bypass security restrictions) with path-aware canonical containment checks.
- sink: `filepath.Rel`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206 (2026-06-04)
- **Path Traversal** (high) [CWE-22]: Fixes a sandbox bypass in the JavaScript runtime compiler and template runner that allowed local file access and module importing regardless of restrictions by canonicalizing symlinks and clearing cached modules between runtime executions.
- sink: `require.DefaultSourceLoader`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d (2026-04-10)
- **Expression Language Injection** (high) [CWE-917]: Fixes an expression injection vulnerability by parsing and collecting template-authored expressions before placeholder substitution, preventing untrusted response-derived data from being re-evaluated as DSL expressions.
- sink: `pkg/protocols/common/expressions/expressions.go`
- fix: collect expressions before placeholder substitution
- commit: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3 (2026-03-16)
- **Cryptographically Weak PRNG** (medium) [CWE-338]: Replaces the cryptographically insecure math/rand PRNG with crypto/rand in JavaScript helper functions to prevent predictable token generation.
- sink: `math/rand.Intn`
- fix: crypto/rand replacement
- commit: https://github.com/projectdiscovery/nuclei/commit/44ef9ce34ffcfdfca3a96626abc73d99dd0e83ab (2026-03-15)
- **SQL Injection** (medium) [CWE-89]: Fixes a path-based fuzzing component encoding logic issue that prevented successful detection and isolated fuzz testing of SQL Injection paths.
- sink: `pkg/testutils/fuzzplayground/sqli_test.go`
- fix: path component fuzzing fix
- commit: https://github.com/projectdiscovery/nuclei/commit/19247ae74b99c5871b62d7c465a63edb446ceba1 (2025-08-25)
- **Auth Bypass** (high) [CWE-341]: Fix a race condition in concurrent Nuclei engine executions where the global `LfaAllowed` state could be shared across different execution contexts, allowing unauthorized local file access.
- sink: `protocolstate.NormalizePath`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/5b89811b906f086e47fdf5357a5ee367f8f1daef (2025-07-18)
- **Auth Bypass** (medium) [CWE-307]: Enforce a minimum length restriction of 16 characters on the DAST server token option to prevent weak or easily guessable authentication credentials.
- sink: `options.DASTServerToken`
- fix: minimum token length constraint
- commit: https://github.com/projectdiscovery/nuclei/commit/5f0b7eb19b6cf160c8b277c9774f8e624d2069cf (2025-02-13)
- **Auth Bypass** (medium) [CWE-862]: Disables self-contained and file-protocol templates by default to enforce safer execution boundaries and prevent unauthorized local file access or unexpected outbound connections unless explicitly enabled.
- sink: `store.config.ExecutorOptions.Options`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/63687c2ce07e464f6b38f797d81a8b92597f6ef9 (2024-11-19)
- **Race Condition** (medium) [CWE-362]: Fixes a race condition in the HTTP request execution where shared connection configurations (including cookie jars and timeouts) were concurrently modified, which could lead to cookie leakage across concurrent sessions.
- sink: `connConfiguration.Connection.SetCookieJar`
- fix: struct cloning
- commit: https://github.com/projectdiscovery/nuclei/commit/3064788d35a8ec8623eb3a54eca920ea289f1259 (2024-08-19)
- **Signature Bypass** (high) [CWE-347]: Fixes a template signature verification bypass by ensuring the digest/signature is strictly parsed from the end of the template rather than extracted via an arbitrary regex match anywhere in the file.
- sink: `Verify`
- fix: signature extraction logic refinement
- commit: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a (2024-08-19)
- **Auth Bypass** (high) [CWE-566]: Fixes a Go loop variable reference bug in FileAuthProvider that causes all registered domains to use the credentials of the last secret entry in the configuration list.
- sink: `f.compiled[domain]`
- fix: loop variable copy
- commit: https://github.com/projectdiscovery/nuclei/commit/350fa4c10c43c0888561dfe1ee5fa473d5023f73 (2024-08-05)
- **Arbitrary Code Execution** (high) [CWE-94]: Enforces validation checks for code-execution templates referenced within workflows, preventing unauthorized local code execution via untrusted workflows.
- sink: `workflowTemplates = append(workflowTemplates, template)`
- fix: verification and flag checks before loading code templates in workflows
- commit: https://github.com/projectdiscovery/nuclei/commit/e86f38299765b82ad724fdb701557e0eaff3884d (2024-03-01)
- **SSRF** (high) [CWE-918]: Fixes a security bypass in the LDAP client where host validation checked the domain instead of the actual target domain controller address, allowing SSRF to restricted networks.
- sink: `c.newLdapSession`
- fix: validate target host instead of domain name
- commit: https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770 (2024-01-20)
- **Server-Side Request Forgery** (high) [CWE-918]: Fixes an SSRF/network policy bypass in SMB helper functions by introducing `protocolstate.IsHostAllowed` checks prior to dialing.
- sink: `protocolstate.Dialer.Dial`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/a677fca192ee732a7b0a278f725517fa998b7897 (2024-01-18)
- **Path Traversal** (high) [CWE-22]: Removes the insecure file extraction `to` feature from extractors and enforces local file access (LFA) boundary checks on headless screenshot file writes to prevent arbitrary file write vulnerabilities via malicious templates.
- sink: `os.OpenFile`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/f663d1c9cf77da53533bf328750621b4a9fa996d (2024-01-05)
- **Sandbox Bypass** (high) [CWE-693]: Disables the 'eval' function and strengthens file validation within the JavaScript sandbox environment to prevent sandbox escape and unauthorized code execution.
- sink: `eval`
- fix: disable eval function
- commit: https://github.com/projectdiscovery/nuclei/commit/c35162c8ef061a96dd74b54c8f022c1a41cf5833 (2023-10-13)
- **Path Traversal** (high) [CWE-22]: Restricts local file input actions and file:// protocol access in headless browser protocols unless explicitly enabled via command-line flags.
- sink: `p.FilesInput`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/d3928e080d819f25306bfbaecc399f03516db5aa (2023-08-25)
- **Path Traversal** (medium) [CWE-22]: Implements a file-access sandbox to prevent malicious templates from reading arbitrary local files via payloads by restricting access to the template directory unless explicitly allowed.
- sink: `os.Open`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648 (2023-07-14)
- **Auth Bypass** (high) [CWE-347]: Introduces a code protocol for template execution alongside a cryptographic signature verification mechanism (RSA/ECDSA) to ensure that only signed, trusted templates can execute local commands or scripts.
- sink: `v2/pkg/protocols/code/code.go`
- fix: introducing command + trivial trust store mechanism
- commit: https://github.com/projectdiscovery/nuclei/commit/a7fb15d0bd548257fe70af3cdddd469f21a75e6a (2023-06-09)
- **Path Traversal** (medium) [CWE-22]: Introduces a sandbox restriction that validates and confines template payload file paths to the template directory, preventing arbitrary local file reads when safe template execution is enabled.
- sink: `generator.loadPayloadsFromFile(pt)`
- fix: path canonicalization + prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/e7fb40a41308f6c41267e858d1781eb67f7e0572 (2022-11-16)
- **Insecure File Permissions** (medium) [CWE-732]: Restricts the file permissions of written debug data files from world-writable/readable (os.ModePerm) to owner-readable/writable (0644).
- sink: `os.OpenFile`
- fix: restrict file permissions to 0644
- commit: https://github.com/projectdiscovery/nuclei/commit/af4854f90de728bb08689d5cae3dec4acfdb6b9a (2022-06-20)
- **Path Traversal** (medium) [CWE-22]: Sanitizes special and directory traversal characters from generated markdown filenames to prevent arbitrary file write/path traversal.
- sink: `os.OpenFile`
- fix: character replacement/sanitization
- commit: https://github.com/projectdiscovery/nuclei/commit/e510bc207f4336058f7b1536ddbb3942b394b05c (2022-04-06)
- **Remote Code Execution** (high) [CWE-829]: Implements a domain restriction mechanism (remote-template-domain allowlist) to prevent loading and executing arbitrary untrusted remote templates from arbitrary hostnames.
- sink: `http.Get`
- fix: host allowlist
- commit: https://github.com/projectdiscovery/nuclei/commit/d1303797c0e5f88343a1f2ed781955dd086df03d (2022-01-12)
- **Information Disclosure** (medium) [CWE-532]: Prevents disclosure of sensitive AWS credentials by pruning internal-only signing variables (aws-id and aws-secret) from the event metadata payload.
- sink: `eventcreator.CreateEventWithAdditionalOptions`
- fix: output pruning
- commit: https://github.com/projectdiscovery/nuclei/commit/c26a1ac21c32f716635738387d5e7d369d7538d1 (2022-01-09)
- **Path Traversal** (medium) [CWE-22]: Replaces filepath.Join with path.Join when processing HTTP request paths to prevent platform-specific path manipulation issues on Windows.
- sink: `filepath.Join`
- fix: replace filepath with path
- commit: https://github.com/projectdiscovery/nuclei/commit/d15298648527f4edd1015e077201551b3a6eab06 (2021-12-16)
- **Sandbox Escape** (low) [CWE-250]: Enables Chromium's security sandbox by default in headless mode, disabling it only when running as root on Linux where it is strictly required.
- sink: `chromeLauncher`
- fix: conditional sandbox disablement based on OS and user privileges
- commit: https://github.com/projectdiscovery/nuclei/commit/df55f7a2eb591a7949b5c66a9c54db54aa28f450 (2021-12-05)
- **Path Traversal** (high) [CWE-22]: Fixes a Zip Slip (Path Traversal) vulnerability in the template updater by validating and sanitizing paths before writing unzipped files.
- sink: `os.OpenFile`
- fix: path validation in calculateTemplateAbsolutePath
- commit: https://github.com/projectdiscovery/nuclei/commit/f9c214a66f37a00fc29fe64232b690e6285ec59c (2021-11-25)
- **Insecure Permissions** (medium) [CWE-732]: Fixes an insecure file and directory creation permissions vulnerability by replacing overly permissive permissions (0777/ModePerm) with restrictive defaults (0644 for files, 0755 for directories).
- sink: `os.OpenFile,ioutil.WriteFile,os.MkdirAll`
- fix: restrictive file/directory permissions (0644/0755)
- commit: https://github.com/projectdiscovery/nuclei/commit/4bccb6cf8a830724b3bda624cb3bfe43eff31683 (2021-11-24)
- **Sandbox Escape** (high) [CWE-116]: Introduces payload validation for template generators to restrict execution of unsafe functions within Nuclei templates.
- sink: `Go-template compiler/executor`
- fix: input validation / payload sandbox checks
- commit: https://github.com/projectdiscovery/nuclei/commit/aefa2717f7d6d1702e3008ac18ebdcc3660dc256 (2020-12-29)
- **Sandbox Escape** (high) [CWE-265]: Restricts the Tengo scripting engine imports in workflows to a safe subset of standard library modules, preventing arbitrary system command execution.
- sink: `tengo.NewScript`
- fix: import allowlist restriction
- commit: https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a (2020-12-13)
## Known CVEs
- **CVE-2026-41645** (medium 5.3) CWE-94
- Nuclei 3.0.0 to < 3.8.0 contains an expression injection vulnerability caused by reuse of HTTP response data with helper/function syntax in multi-step templates, letting malicious target servers execu...
- **CVE-2026-41646** (medium 5.5) CWE-284
- Nuclei 3.0.0 to < 3.8.0 contains a local file read vulnerability caused by JavaScript protocol runtime allowing require() to access local .js and .json files, letting attackers read restricted files, ...
- **CVE-2026-41282** (medium 4.0) CWE-94
- ProjectDiscovery Nuclei < 3.8.0 contains a template injection caused by unsafe use of -env-vars in multi-step templates against untrusted targets, letting attackers inject DSL expressions, exploit req...
- **CVE-2024-43405** (high 7.4) CWE-78
- Nuclei versions prior to 3.3.2 contain a signature verification bypass caused by discrepancies in newline handling between the verification process and YAML parser, letting attackers inject malicious ...
- **CVE-2024-27920** (high 7.4) CWE-78
- projectdiscovery/nuclei v3 contains a vulnerability caused by execution of unsigned code templates through workflows, letting malicious code run on the user's system, exploit requires using custom wor...
- **CVE-2023-37896** (high 7.5) CWE-22
- Nuclei prior to version 2.9.9 contains a sandbox sanitization issue caused by relative path handling in payload loading, letting arbitrary files be read on the filesystem when used as Go SDK, exploit ...
---
_Fixed-issue fingerprints and CVE matches are derived from real data; LLM-written prose is advisory and should be verified against the cited commits._
+117
View File
@@ -1009,6 +1009,26 @@ Part Definitions:
- <code>all</code> - HTTP response body + headers
- <code>cookies_from_response</code> - HTTP response cookies in name:value format
- <code>headers_from_response</code> - HTTP response headers in name:value format
- <code>tls_version</code> - TLS version negotiated for the HTTP connection
- <code>cipher</code> - TLS cipher suite negotiated for the HTTP connection
- <code>sni</code> - SNI value used in the TLS handshake
- <code>subject_cn</code> - Leaf certificate subject common name
- <code>subject_dn</code> - Leaf certificate subject distinguished name
- <code>subject_an</code> - Leaf certificate subject alternative names
- <code>subject_org</code> - Leaf certificate subject organization
- <code>issuer_cn</code> - Leaf certificate issuer common name
- <code>issuer_dn</code> - Leaf certificate issuer distinguished name
- <code>issuer_org</code> - Leaf certificate issuer organization
- <code>serial</code> - Leaf certificate serial number
- <code>fingerprint_hash</code> - Leaf certificate fingerprint hashes (md5/sha1/sha256)
- <code>not_before</code> - Leaf certificate not-before timestamp
- <code>not_after</code> - Leaf certificate not-after timestamp
- <code>expired</code> - Whether the leaf certificate has expired
- <code>self_signed</code> - Whether the leaf certificate is self-signed
- <code>mismatched</code> - Whether the leaf certificate does not match the SNI hostname
- <code>domains</code> - Deduplicated domains from subject CN and SANs
- <code>wildcard_certificate</code> - Whether the leaf certificate is a wildcard certificate
- <code>emails</code> - Email addresses embedded in the leaf certificate
<hr />
@@ -2606,6 +2626,7 @@ Part Definitions:
- <code>host</code> - Host is the input to the template
- <code>matched</code> - Matched is the input which was matched upon
- <code>request</code> - Request contains the DNS request in text format
- <code>duration</code> - Protocol operation duration in seconds
- <code>type</code> - Type is the type of request made
- <code>rcode</code> - Rcode field returned for the DNS request
- <code>question</code> - Question contains the DNS question field
@@ -2898,6 +2919,16 @@ Enum Values:
- <code>ANY</code>
- <code>SRV</code>
- <code>RRSIG</code>
- <code>NSEC</code>
- <code>DNSKEY</code>
- <code>NSEC3</code>
- <code>NSEC3PARAM</code>
</div>
<hr />
@@ -3071,6 +3102,82 @@ NoRecursive specifies whether to not do recursive checks if folders are provided
<hr />
<div class="dd">
<code>smb-user</code> <i>string</i>
</div>
<div class="dt">
SMBUser authenticates to remote SMB shares when the file input is a UNC
or smb:// path (issue #6142). Guest/anon: empty password.
Examples:
```yaml
smb-user: auditor
```
</div>
<hr />
<div class="dd">
<code>smb-password</code> <i>string</i>
</div>
<div class="dt">
SMBPassword is the password for SMB file targets.
</div>
<hr />
<div class="dd">
<code>smb-domain</code> <i>string</i>
</div>
<div class="dt">
SMBDomain is the optional NTLM domain / workgroup.
</div>
<hr />
<div class="dd">
<code>smb-hash</code> <i>string</i>
</div>
<div class="dt">
SMBHash enables pass-the-hash (overrides smb-password when set).
</div>
<hr />
<div class="dd">
<code>smb-port</code> <i>int</i>
</div>
<div class="dt">
SMBPort overrides the default SMB port (445) for UNC targets.
</div>
<hr />
@@ -3109,6 +3216,7 @@ Part Definitions:
- <code>matched</code> - Matched is the input which was matched upon
- <code>type</code> - Type is the type of request made
- <code>request</code> - Network request made from the client
- <code>duration</code> - Latest measured operation duration in seconds
- <code>body,all,data</code> - Network response received from server (default)
- <code>raw</code> - Full Network protocol data
@@ -3501,6 +3609,7 @@ Part Definitions:
- <code>matched</code> - Matched is the input which was matched upon
- <code>type</code> - Type is the type of request made
- <code>req</code> - Headless request made from the client
- <code>duration</code> - Latest measured operation duration in seconds
- <code>resp,body,data</code> - Headless response received from client (default)
<hr />
@@ -3881,6 +3990,7 @@ Part Definitions:
- <code>matched</code> - Matched is the input which was matched upon
- <code>type</code> - Type is the type of request made
- <code>timestamp</code> - Timestamp is the time when the request was made
- <code>duration</code> - Protocol operation duration in seconds
- <code>response</code> - JSON SSL protocol handshake details
- <code>cipher</code> - Cipher is the encryption algorithm used
- <code>domains</code> - Domains are the list of domain names in the certificate
@@ -4080,6 +4190,7 @@ Part Definitions:
- <code>type</code> - Type is the type of request made
- <code>success</code> - Success specifies whether websocket connection was successful
- <code>request</code> - Websocket request made to the server
- <code>duration</code> - Latest measured operation duration in seconds
- <code>response</code> - Websocket response received from the server
- <code>host</code> - Host is the input to the template
- <code>matched</code> - Matched is the input which was matched upon
@@ -4254,8 +4365,14 @@ Appears in:
Part Definitions:
- <code>type</code> - Type is the type of request made
- <code>host</code> - Host is the input to the template
- <code>response</code> - WHOIS response data
- <code>duration</code> - Protocol operation duration in seconds
<hr />
<div class="dd">
+1
View File
@@ -25,6 +25,7 @@ extend-exclude = [
[default.extend-identifiers]
# Go identifiers that cannot be renamed without breaking API
MisMatched = "MisMatched"
IsMisMatchedCert = "IsMisMatchedCert" # tlsx clients API
NoopWriter = "NoopWriter"
AllowdTypes = "AllowdTypes"
# Deprecated alias kept for backward compatibility
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"os"
"path/filepath"
"testing"
"github.com/adrg/xdg"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func TestAuthValueIsCLIOnly(t *testing.T) {
oldConfigHome := xdg.ConfigHome
oldConfigDirs := xdg.ConfigDirs
t.Cleanup(func() {
xdg.ConfigHome = oldConfigHome
xdg.ConfigDirs = oldConfigDirs
})
const apiKey = "12345678-1234-1234-1234-123456789012"
tests := []struct {
name string
args func(string) []string
file string
want string
}{
{name: "explicit config", args: func(path string) []string { return []string{"-config=" + path} }, file: "auth: true\n"},
{name: "profile", args: func(path string) []string { return []string{"-profile=" + path} }, file: "auth: " + apiKey + "\n"},
{name: "CLI", args: func(string) []string { return []string{"-auth=" + apiKey} }, want: apiKey},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
xdg.ConfigHome = filepath.Join(dir, "config-home")
xdg.ConfigDirs = nil
selectedPath := filepath.Join(dir, "selected.yaml")
if test.file != "" {
if err := os.WriteFile(selectedPath, []byte(test.file), 0o600); err != nil {
t.Fatalf("write selected configuration: %v", err)
}
}
var auth string
var selected configuration.Selection
flagSet := goflags.NewFlagSet()
flagSet.DynamicVar(&auth, "auth", "true", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
cfg := &catalogconfig.Config{Logger: gologger.DefaultLogger}
cfg.SetConfigDir(filepath.Join(xdg.ConfigHome, catalogconfig.BinaryName))
warnings, err := configuration.Load(cfg, flagSet, &selected, func(path string) (string, error) {
return path, nil
}, test.args(selectedPath)...)
if err != nil {
t.Fatalf("load configuration: %v", err)
}
if len(warnings) != 0 {
t.Fatalf("configuration warnings: %v", warnings)
}
if got := cliOnlyDynamicValue(flagSet, "auth", auth); got != test.want {
t.Fatalf("auth value = %q, want %q", got, test.want)
}
})
}
}
+190 -278
View File
@@ -2,6 +2,7 @@ package main
import (
"bufio"
"flag"
"fmt"
"io/fs"
"os"
@@ -11,11 +12,11 @@ import (
"runtime/pprof"
"runtime/trace"
"strings"
"sync"
"time"
"github.com/projectdiscovery/gologger"
_pdcp "github.com/projectdiscovery/nuclei/v3/internal/pdcp"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
"github.com/projectdiscovery/utils/auth/pdcp"
"github.com/projectdiscovery/utils/env"
_ "github.com/projectdiscovery/utils/pprof"
@@ -25,6 +26,7 @@ import (
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/interactsh/pkg/client"
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
"github.com/projectdiscovery/nuclei/v3/internal/runner"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
@@ -47,27 +49,22 @@ import (
)
var (
cfgFile string
templateProfile string
memProfile string // optional profile file path
options = &types.Options{}
inlineSecretsTempFiles []string
memProfile string // optional profile file path
options = &types.Options{}
inlineSecretsTempDir string
inlineSecretsTempMu sync.Mutex
)
func main() {
options.Logger = gologger.DefaultLogger
defer func() {
for _, f := range inlineSecretsTempFiles {
_ = os.Remove(f)
}
}()
defer cleanupInlineSecretsDirs()
// enables CLI specific configs mostly interactive behavior
config.CurrentAppMode = config.AppModeCLI
if err := runner.ConfigureOptions(); err != nil {
options.Logger.Fatal().Msgf("Could not initialize options: %s\n", err)
fatalf("Could not initialize options: %s\n", err)
}
_ = readConfig()
@@ -83,7 +80,7 @@ func main() {
templates.UseOptionsForSigner(options)
tsigner, err := signer.NewTemplateSigner(nil, nil) // will read from env , config or generate new keys
if err != nil {
options.Logger.Fatal().Msgf("couldn't initialize signer crypto engine: %s\n", err)
fatalf("Could not initialize signer crypto engine: %s\n", err)
}
successCounter := 0
@@ -122,7 +119,7 @@ func main() {
createProfileFile := func(ext, profileType string) *os.File {
f, err := os.Create(memProfile + ext)
if err != nil {
options.Logger.Fatal().Msgf("profile: could not create %s profile %q file: %v", profileType, f.Name(), err)
fatalf("profile: could not create %s profile %q file: %v", profileType, memProfile+ext, err)
}
return f
}
@@ -136,18 +133,18 @@ func main() {
// Start tracing
if err := trace.Start(traceFile); err != nil {
options.Logger.Fatal().Msgf("profile: could not start trace: %v", err)
fatalf("profile: could not start trace: %v", err)
}
// Start CPU profiling
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
options.Logger.Fatal().Msgf("profile: could not start CPU profile: %v", err)
fatalf("profile: could not start CPU profile: %v", err)
}
defer func() {
// Start heap memory snapshot
if err := pprof.WriteHeapProfile(memProfileFile); err != nil {
options.Logger.Fatal().Msgf("profile: could not write memory profile: %v", err)
fatalf("profile: could not write memory profile: %v", err)
}
pprof.StopCPUProfile()
@@ -169,14 +166,14 @@ func main() {
if options.ScanUploadFile != "" {
if err := runner.UploadResultsToCloud(options); err != nil {
options.Logger.Fatal().Msgf("could not upload scan results to cloud dashboard: %s\n", err)
fatalf("Could not upload scan results to cloud dashboard: %s\n", err)
}
return
}
nucleiRunner, err := runner.New(options)
if err != nil {
options.Logger.Fatal().Msgf("Could not create runner: %s\n", err)
fatalf("Could not create runner: %s\n", err)
}
if nucleiRunner == nil {
return
@@ -187,7 +184,7 @@ func main() {
cancel := stackMonitor.Start(10 * time.Second)
defer cancel()
stackMonitor.RegisterCallback(func(dumpID string) error {
resumeFileName := fmt.Sprintf("crash-resume-file-%s.dump", dumpID)
resumeFileName := types.DefaultCrashResumeFilePath(dumpID)
if options.EnableCloudUpload {
options.Logger.Info().Msgf("Uploading scan results to cloud...")
}
@@ -213,6 +210,7 @@ func main() {
options.Logger.Info().Msgf("CTRL+C pressed: Exiting\n")
if options.DASTServer {
nucleiRunner.Close()
cleanupInlineSecretsDirs()
os.Exit(1)
}
@@ -228,17 +226,15 @@ func main() {
options.Logger.Error().Msgf("Couldn't create resume file: %s\n", err)
}
}
for _, f := range inlineSecretsTempFiles {
_ = os.Remove(f)
}
cleanupInlineSecretsDirs()
os.Exit(1)
}()
if err := nucleiRunner.RunEnumeration(); err != nil {
if options.Validate {
options.Logger.Fatal().Msgf("Could not validate templates: %s\n", err)
fatalf("Could not validate templates: %s\n", err)
} else {
options.Logger.Fatal().Msgf("Could not run nuclei: %s\n", err)
fatalf("Could not run nuclei: %s\n", err)
}
}
nucleiRunner.Close()
@@ -249,10 +245,12 @@ func main() {
}
func readConfig() *goflags.FlagSet {
var selectedFiles configuration.Selection
// when true updates nuclei binary to latest version
var updateNucleiBinary bool
var pdcpauth string
var disableUpdateCheck bool
var resetRequested bool
var pdcpAuth string
var fuzzFlag bool
flagSet := goflags.NewFlagSet()
@@ -260,6 +258,9 @@ func readConfig() *goflags.FlagSet {
flagSet.SetDescription(`Nuclei is a fast, template based vulnerability scanner focusing
on extensive configurability, massive extensibility and ease of use.`)
// Shared PD flags (e.g. -max-time soft-kill via interrupt).
flagSet.AddCommonFlags()
/* TODO Important: The defined default values, especially for slice/array types are NOT DEFAULT VALUES, but rather implicit values to which the user input is appended.
This can be very confusing and should be addressed
*/
@@ -345,8 +346,8 @@ on extensive configurability, massive extensibility and ease of use.`)
)
flagSet.CreateGroup("configs", "Configurations",
flagSet.StringVar(&cfgFile, "config", "", "path to the nuclei configuration file"),
flagSet.StringVarP(&templateProfile, "profile", "tp", "", "template profile config file to run"),
flagSet.StringVar(&selectedFiles.Config, "config", "", "path to the nuclei configuration file"),
flagSet.StringVarP(&selectedFiles.Profile, "profile", "tp", "", "template profile config file to run"),
flagSet.BoolVarP(&options.ListTemplateProfiles, "profile-list", "tpl", false, "list community template profiles"),
flagSet.BoolVarP(&options.FollowRedirects, "follow-redirects", "fr", false, "enable following redirects for http templates"),
flagSet.BoolVarP(&options.FollowHostRedirects, "follow-host-redirects", "fhr", false, "follow redirects on the same host"),
@@ -375,7 +376,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.SourceIP, "source-ip", "sip", "", "source ip address to use for network scan"),
flagSet.IntVarP(&options.ResponseReadSize, "response-size-read", "rsr", 0, "max response size to read in bytes"),
flagSet.IntVarP(&options.ResponseSaveSize, "response-size-save", "rss", unitutils.Mega, "max response size to read in bytes"),
flagSet.CallbackVar(resetCallback, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
flagSet.CallbackVar(func() { resetRequested = true }, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
)
@@ -486,7 +487,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&updateNucleiBinary, "update", "up", false, "update nuclei engine to the latest released version"),
flagSet.BoolVarP(&options.UpdateTemplates, "update-templates", "ut", false, "update nuclei-templates to latest released version"),
flagSet.StringVarP(&options.NewTemplatesDirectory, "update-template-dir", "ud", "", "custom directory to install / update nuclei-templates"),
flagSet.CallbackVarP(disableUpdatesCallback, "disable-update-check", "duc", "disable automatic nuclei/templates update check"),
flagSet.BoolVarP(&disableUpdateCheck, "disable-update-check", "duc", false, "disable automatic nuclei/templates update check"),
)
flagSet.CreateGroup("Honeypot", "Honeypot",
@@ -504,7 +505,7 @@ on extensive configurability, massive extensibility and ease of use.`)
)
flagSet.CreateGroup("cloud", "Cloud",
flagSet.DynamicVar(&pdcpauth, "auth", "true", "configure projectdiscovery cloud (pdcp) api key"),
flagSet.DynamicVar(&pdcpAuth, "auth", "true", "configure projectdiscovery cloud (pdcp) api key"),
flagSet.StringVarP(&options.TeamID, "team-id", "tid", _pdcp.TeamIDEnv, "upload scan results to given team id (optional)"),
flagSet.BoolVarP(&options.EnableCloudUpload, "cloud-upload", "cup", false, "upload scan results to pdcp dashboard [DEPRECATED use -dashboard]"),
flagSet.StringVarP(&options.ScanID, "scan-id", "sid", "", "upload scan results to existing scan id (optional)"),
@@ -537,12 +538,32 @@ Run nuclei with sorted Markdown outputs (with environment variables):
Additional documentation is available at: https://docs.nuclei.sh/getting-started/running
`)
// nuclei has multiple migrations
// ex: resume.cfg moved to platform standard cache dir from config dir
// ex: config.yaml moved to platform standard config dir from linux specific config dir
// and hence it will be attempted in config package during init
// Nuclei handles config-path compatibility before parsing its layered files.
goflags.DisableAutoConfigMigration = true
_ = flagSet.Parse()
warnings, err := configuration.Load(config.DefaultConfig, flagSet, &selectedFiles, func(path string) (string, error) {
return configuration.ResolveProfilePath(path, configuration.TemplatesDirectory(options, config.DefaultConfig))
})
for _, warning := range warnings {
options.Logger.Warning().Msgf("Could not load configuration: %s\n", warning)
}
if err != nil {
fatalf("Could not load configuration: %s\n", err)
}
pdcpAuth = cliOnlyDynamicValue(flagSet, "auth", pdcpAuth)
if disableUpdateCheck {
config.DefaultConfig.DisableUpdateCheck()
}
if templatesDir, overridden := configuration.TemplatesDirectoryOverride(options); overridden {
config.DefaultConfig.SetTemplatesDir(templatesDir)
}
if resetRequested {
resetCallback()
}
// when fuzz flag is enabled, set the dast flag to true
if fuzzFlag {
@@ -556,13 +577,13 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
}
// api key hierarchy: cli flag > env var > .pdcp/credential file
if pdcpauth == "true" {
if pdcpAuth == "true" {
runner.AuthWithPDCP()
} else if len(pdcpauth) == 36 {
} else if len(pdcpAuth) == 36 {
ph := pdcp.PDCPCredHandler{}
if _, err := ph.GetCreds(); err == pdcp.ErrNoCreds {
apiServer := env.GetEnvOrDefault("PDCP_API_SERVER", pdcp.DefaultApiServer)
if validatedCreds, err := ph.ValidateAPIKey(pdcpauth, apiServer, config.BinaryName); err == nil {
if validatedCreds, err := ph.ValidateAPIKey(pdcpAuth, apiServer, config.BinaryName); err == nil {
_ = ph.SaveCreds(validatedCreds)
}
}
@@ -573,7 +594,7 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
h := &pdcp.PDCPCredHandler{}
_, err := h.GetCreds()
if err != nil {
options.Logger.Fatal().Msg("To utilize the `-ai` flag, please configure your API key with the `-auth` flag or set the `PDCP_API_KEY` environment variable")
fatalf("To utilize the `-ai` flag, please configure your API key with the `-auth` flag or set the `PDCP_API_KEY` environment variable")
}
}
@@ -592,143 +613,34 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
runner.NucleiToolUpdateCallback()
}
tempSecretsDir, err := configuration.ApplyProfile(selectedFiles.Profile, options)
if err != nil {
fatalf("Could not process template profile: %s\n", err)
}
if tempSecretsDir != "" {
setInlineSecretsTempDir(tempSecretsDir)
}
if options.LeaveDefaultPorts {
http.LeaveDefaultPorts = true
}
if customConfigDir := os.Getenv(config.NucleiConfigDirEnv); customConfigDir != "" {
config.DefaultConfig.SetConfigDir(customConfigDir)
readFlagsConfig(flagSet)
}
if cfgFile != "" {
if !fileutil.FileExists(cfgFile) {
options.Logger.Fatal().Msgf("given config file '%s' does not exist", cfgFile)
}
// merge config file with flags
if err := flagSet.MergeConfigFile(cfgFile); err != nil {
options.Logger.Fatal().Msgf("Could not read config: %s\n", err)
}
if !options.Vars.IsEmpty() {
// Maybe we should add vars to the config file as well even if they are set via flags?
file, err := os.Open(cfgFile)
if err != nil {
gologger.Fatal().Msgf("Could not open config file: %s\n", err)
}
defer func() {
_ = file.Close()
}()
data := make(map[string]interface{})
err = yaml.NewDecoder(file).Decode(&data)
if err != nil {
gologger.Fatal().Msgf("Could not decode config file: %s\n", err)
}
variables := data["var"]
if variables != nil {
if varSlice, ok := variables.([]interface{}); ok {
for _, value := range varSlice {
if strVal, ok := value.(string); ok {
err = options.Vars.Set(strVal)
if err != nil {
gologger.Warning().Msgf("Could not set variable from config file: %s\n", err)
}
} else {
gologger.Warning().Msgf("Skipping non-string variable in config: %#v", value)
}
}
} else {
gologger.Warning().Msgf("No 'var' section found in config file: %s", cfgFile)
}
}
}
}
templatesDir := options.NewTemplatesDirectory
if templatesDir == "" {
templatesDir = os.Getenv(config.NucleiTemplatesDirEnv)
}
if templatesDir != "" {
config.DefaultConfig.SetTemplatesDir(templatesDir)
}
defaultProfilesPath := filepath.Join(config.DefaultConfig.GetTemplateDir(), "profiles")
if templateProfile != "" {
if filepath.Ext(templateProfile) == "" {
if tp := findProfilePathById(templateProfile, defaultProfilesPath); tp != "" {
templateProfile = tp
} else {
options.Logger.Fatal().Msgf("'%s' is not a profile-id or profile path", templateProfile)
}
}
if !filepath.IsAbs(templateProfile) {
if filepath.Dir(templateProfile) == "profiles" {
defaultProfilesPath = filepath.Join(config.DefaultConfig.GetTemplateDir())
}
currentDir, err := os.Getwd()
if err == nil && fileutil.FileExists(filepath.Join(currentDir, templateProfile)) {
templateProfile = filepath.Join(currentDir, templateProfile)
} else {
templateProfile = filepath.Join(defaultProfilesPath, templateProfile)
}
}
if !fileutil.FileExists(templateProfile) {
options.Logger.Fatal().Msgf("given template profile file '%s' does not exist", templateProfile)
}
if err := flagSet.MergeConfigFile(templateProfile); err != nil {
options.Logger.Fatal().Msgf("Could not read template profile: %s\n", err)
}
// Process inline target list from profile.
// Supports both the dedicated targets-inline key and multiline
// content in the list key (which normally holds a file path).
if options.InlineTargetsList != "" {
inlineTargets := strings.Split(strings.TrimSpace(options.InlineTargetsList), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
options.Targets = append(options.Targets, target)
}
}
}
if strings.Contains(options.TargetsFilePath, "\n") {
// list key has multiline content, treat as inline targets
inlineTargets := strings.Split(strings.TrimSpace(options.TargetsFilePath), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
options.Targets = append(options.Targets, target)
}
}
options.TargetsFilePath = ""
}
// Process inline secrets from profile YAML
tempSecretsFile, err := processInlineSecretsFromProfile(templateProfile, options)
if err != nil {
options.Logger.Fatal().Msgf("Could not process inline secrets: %s\n", err)
}
if tempSecretsFile != "" {
inlineSecretsTempFiles = append(inlineSecretsTempFiles, tempSecretsFile)
}
}
if len(options.SecretsFile) > 0 {
for _, secretFile := range options.SecretsFile {
if !fileutil.FileExists(secretFile) {
options.Logger.Fatal().Msgf("given secrets file '%s' does not exist", secretFile)
fatalf("Secrets file '%s' does not exist", secretFile)
}
}
}
cleanupOldResumeFiles()
return flagSet
}
// cleanupOldResumeFiles cleans up resume files older than 10 days.
func cleanupOldResumeFiles() {
root := config.DefaultConfig.GetCacheDir()
root := config.DefaultConfig.GetStateDir()
filter := fileutil.FileFilters{
OlderThan: 24 * time.Hour * 10, // cleanup on the 10th day
Prefix: "resume-",
@@ -736,46 +648,10 @@ func cleanupOldResumeFiles() {
_ = fileutil.DeleteFilesOlderThan(root, filter)
}
// readFlagsConfig reads the config file from the default config dir and copies it to the current config dir.
func readFlagsConfig(flagset *goflags.FlagSet) {
// check if config.yaml file exists
defaultCfgFile, err := flagset.GetConfigFilePath()
if err != nil {
// something went wrong either dir is not readable or something else went wrong upstream in `goflags`
// warn and exit in this case
options.Logger.Warning().Msgf("Could not read config file: %s\n", err)
return
}
cfgFile := config.DefaultConfig.GetFlagsConfigFilePath()
if !fileutil.FileExists(cfgFile) {
if !fileutil.FileExists(defaultCfgFile) {
// if default config does not exist, warn and exit
options.Logger.Warning().Msgf("missing default config file : %s", defaultCfgFile)
return
}
// if does not exist copy it from the default config
if err = fileutil.CopyFile(defaultCfgFile, cfgFile); err != nil {
options.Logger.Warning().Msgf("Could not copy config file: %s\n", err)
}
return
}
// if config file exists, merge it with the default config
if err = flagset.MergeConfigFile(cfgFile); err != nil {
options.Logger.Warning().Msgf("failed to merge configfile with flags got: %s\n", err)
}
}
// disableUpdatesCallback disables the update check.
func disableUpdatesCallback() {
config.DefaultConfig.DisableUpdateCheck()
}
// printVersion prints the nuclei version and exits.
func printVersion() {
options.Logger.Info().Msgf("Nuclei Engine Version: %s", config.Version)
options.Logger.Info().Msgf("Nuclei Config Directory: %s", config.DefaultConfig.GetConfigDir())
options.Logger.Info().Msgf("Nuclei Cache Directory: %s", config.DefaultConfig.GetCacheDir()) // cache dir contains resume files
options.Logger.Info().Msgf("PDCP Directory: %s", pdcp.PDCPDir)
runner.LogDirectoryInfo(options.Logger)
os.Exit(0)
}
@@ -799,119 +675,155 @@ func printTemplateVersion() {
os.Exit(0)
}
type resetPath struct {
name string
path string
}
func resetCallback() {
cfg := config.DefaultConfig
templateRoot := cfg.TemplatesDirectory
templateAction := fmt.Sprintf("4. Active nuclei-templates at %v", templateRoot)
warning := fmt.Sprintf(`
Using '-reset' will delete all nuclei configurations files and all nuclei-templates
Using '-reset' will delete Nuclei configuration, state, cache, and template files
Following files will be deleted:
1. All config files at %v
2. All cache files (including resume state) at %v
3. All nuclei-templates at %v
2. All state files (including generated resume state) at %v
3. All cache files at %v
%s
Note: Make sure you have backup of your custom nuclei-templates before proceeding
`, config.DefaultConfig.GetConfigDir(), config.DefaultConfig.GetCacheDir(), config.DefaultConfig.TemplatesDirectory)
`, cfg.GetConfigDir(), cfg.GetStateDir(), cfg.GetCacheDir(), templateAction)
options.Logger.Print().Msg(warning)
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Are you sure you want to continue? [y/n]: ")
resp, err := reader.ReadString('\n')
if err != nil {
options.Logger.Fatal().Msgf("could not read response: %s", err)
fatalf("Could not read response: %s", err)
}
resp = strings.TrimSpace(resp)
if stringsutil.EqualFoldAny(resp, "y", "yes") {
break
}
if stringsutil.EqualFoldAny(resp, "n", "no", "") {
fmt.Println("Exiting...")
os.Exit(0)
}
}
err := os.RemoveAll(config.DefaultConfig.GetConfigDir())
if err != nil {
options.Logger.Fatal().Msgf("could not delete config dir: %s", err)
resetPaths := []resetPath{
{name: "config", path: cfg.GetConfigDir()},
{name: "state", path: cfg.GetStateDir()},
{name: "cache", path: cfg.GetCacheDir()},
{name: "templates", path: templateRoot},
}
err = os.RemoveAll(config.DefaultConfig.GetCacheDir())
if err != nil {
options.Logger.Fatal().Msgf("could not delete cache dir: %s", err)
if err := removeResetPaths(resetPaths); err != nil {
fatalf("Could not reset paths: %s", err)
}
err = os.RemoveAll(config.DefaultConfig.TemplatesDirectory)
if err != nil {
options.Logger.Fatal().Msgf("could not delete templates dir: %s", err)
}
options.Logger.Info().Msgf("Successfully deleted all nuclei configurations files and nuclei-templates")
options.Logger.Info().Msgf("Successfully reset Nuclei files")
os.Exit(0)
}
func findProfilePathById(profileId, templatesDir string) string {
var profilePath string
err := filepath.WalkDir(templatesDir, func(iterItem string, d fs.DirEntry, err error) error {
ext := filepath.Ext(iterItem)
isYaml := ext == extensions.YAML || ext == extensions.YML
if err != nil || d.IsDir() || !isYaml {
// skip non yaml files
return nil
func removeResetPaths(paths []resetPath) error {
for _, path := range paths {
if err := validateResetPath(path.path); err != nil {
return fmt.Errorf("error validating %s path: %w", path.name, err)
}
if strings.TrimSuffix(filepath.Base(iterItem), ext) == profileId {
profilePath = iterItem
return fmt.Errorf("FOUND")
}
for _, path := range paths {
if err := os.RemoveAll(path.path); err != nil {
return fmt.Errorf("error deleting %s path: %w", path.name, err)
}
}
return nil
}
func validateResetPath(path string) error {
if path == "" {
return fmt.Errorf("path is empty")
}
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolve %q: %w", path, err)
}
absPath = filepath.Clean(absPath)
volume := filepath.VolumeName(absPath)
if absPath == filepath.Clean(volume+string(filepath.Separator)) {
return fmt.Errorf("path %q is a filesystem root", absPath)
}
relativePath := strings.Trim(filepath.ToSlash(strings.TrimPrefix(absPath, volume)), "/")
if relativePath == "" || len(strings.Split(relativePath, "/")) < 2 {
return fmt.Errorf("path %q is too broad", absPath)
}
if homeDir, err := os.UserHomeDir(); err == nil && absPath == filepath.Clean(homeDir) {
return fmt.Errorf("path %q is the user home directory", absPath)
}
if absPath == filepath.Clean(os.TempDir()) {
return fmt.Errorf("path %q is the system temporary directory", absPath)
}
if cwd, err := os.Getwd(); err == nil && absPath == filepath.Clean(cwd) {
return fmt.Errorf("path %q is the current working directory", absPath)
}
if resolvedPath, err := filepath.EvalSymlinks(absPath); err == nil && resolvedPath != absPath {
if err := validateResetPath(resolvedPath); err != nil {
return fmt.Errorf("resolved path %q is unsafe: %w", resolvedPath, err)
}
}
return nil
}
func cleanupInlineSecretsDirs() {
inlineSecretsTempMu.Lock()
dir := inlineSecretsTempDir
inlineSecretsTempDir = ""
inlineSecretsTempMu.Unlock()
if dir != "" {
_ = os.RemoveAll(dir)
}
}
func setInlineSecretsTempDir(dir string) {
inlineSecretsTempMu.Lock()
inlineSecretsTempDir = dir
inlineSecretsTempMu.Unlock()
}
func cliOnlyDynamicValue(flagSet *goflags.FlagSet, name, value string) string {
found := false
flagSet.CommandLine.Visit(func(current *flag.Flag) {
if current.Name == name {
found = true
}
return nil
})
if err != nil && err.Error() != "FOUND" {
options.Logger.Error().Msgf("%s\n", err)
if !found {
return ""
}
return profilePath
return value
}
// profileSecrets is a helper struct to extract secrets section from a template profile YAML
type profileSecrets struct {
Secrets interface{} `yaml:"secrets"`
}
// processInlineSecretsFromProfile parses the profile YAML file for inline secrets
// and creates a temporary secrets file compatible with nuclei's auth provider.
// Returns the path to the temp file or empty string if no secrets found.
func processInlineSecretsFromProfile(profilePath string, options *types.Options) (string, error) {
data, err := os.ReadFile(profilePath)
if err != nil {
return "", fmt.Errorf("could not read profile file: %w", err)
}
var profile profileSecrets
if err := yaml.Unmarshal(data, &profile); err != nil {
return "", fmt.Errorf("could not parse profile YAML: %w", err)
}
if profile.Secrets == nil {
return "", nil
}
secretsData, err := yaml.Marshal(profile.Secrets)
if err != nil {
return "", fmt.Errorf("could not marshal inline secrets: %w", err)
}
tempDir := filepath.Join(os.TempDir(), "nuclei-secrets")
if err := os.MkdirAll(tempDir, 0700); err != nil {
return "", fmt.Errorf("could not create temp directory: %w", err)
}
tempFile, err := os.CreateTemp(tempDir, "inline-secrets-*.yaml")
if err != nil {
return "", fmt.Errorf("could not create temp secrets file: %w", err)
}
defer func() {
_ = tempFile.Close()
}()
if _, err := tempFile.Write(secretsData); err != nil {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
return "", fmt.Errorf("could not write to temp secrets file: %w", err)
}
options.SecretsFile = append(options.SecretsFile, tempFile.Name())
return tempFile.Name(), nil
func fatalf(format string, args ...any) {
cleanupInlineSecretsDirs()
options.Logger.Fatal().Msgf(format, args...)
}
+21 -5
View File
@@ -34,10 +34,15 @@ func TestMain(m *testing.M) {
panic(err)
}
dummyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
targetURL = dummyServer.URL
// Only start the shared httptest for benchmarks so unit tests (e.g. goleak)
// are not polluted by a long-lived accept loop.
var dummyServer *httptest.Server
if isBenchmarkRun() {
dummyServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
targetURL = dummyServer.URL
}
// Execute tests
@@ -45,13 +50,24 @@ func TestMain(m *testing.M) {
// Tear down
dummyServer.Close()
if dummyServer != nil {
dummyServer.Close()
}
_ = os.RemoveAll(projectPath)
_ = os.Unsetenv("DISABLE_STDOUT")
os.Exit(exitCode)
}
func isBenchmarkRun() bool {
for _, arg := range os.Args[1:] {
if strings.HasPrefix(arg, "-test.bench") {
return true
}
}
return false
}
// getUniqFilename generates a unique filename by appending .N if file exists
// Similar to wget's behavior: file.cpu.prof, file.cpu.1.prof, file.cpu.2.prof, etc.
func getUniqFilename(basePath string) string {
+90
View File
@@ -0,0 +1,90 @@
package main_test
import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/nuclei/v3/internal/runner"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/rs/xid"
"github.com/stretchr/testify/require"
"github.com/tarunKoyalwar/goleak"
)
const cliLeakTestChildEnv = "NUCLEI_CLI_LEAKTEST_CHILD"
var cliKnownLeaks = []goleak.Option{
goleak.Pretty(),
goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
goleak.IgnoreAnyContainingPkg("github.com/hashicorp/golang-lru/v2/expirable"),
goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),
goleak.IgnoreAnyFunction("github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate.StartActiveMemGuardian.func1"),
}
// TestCLIRunnerGoroutineLeak runs a minimal CLI-path nuclei scan via internal/runner
// and asserts no unexpected goroutine leaks remain after Close.
//
// The body runs in a child process so shared package state from other tests
// does not pollute goleak (same approach as lib/tests SDK leak coverage).
func TestCLIRunnerGoroutineLeak(t *testing.T) {
if os.Getenv(cliLeakTestChildEnv) == "1" {
runCLIRunnerLeakTest(t)
return
}
cmd := exec.Command(os.Args[0], "-test.run=^TestCLIRunnerGoroutineLeak$", "-test.count=1", "-test.v")
cmd.Env = append(os.Environ(), cliLeakTestChildEnv+"=1")
out, err := cmd.CombinedOutput()
require.NoError(t, err, "CLI leak test child failed:\n%s", string(out))
}
func runCLIRunnerLeakTest(t *testing.T) {
gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent)
_ = os.Setenv("DISABLE_STDOUT", "true")
t.Cleanup(func() { _ = os.Unsetenv("DISABLE_STDOUT") })
config.DefaultConfig.DisableUpdateCheck()
defer func() {
time.Sleep(2 * time.Second)
goleak.VerifyNone(t, cliKnownLeaks...)
}()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
_, thisFile, _, ok := runtime.Caller(0)
require.True(t, ok)
templatePath := filepath.Join(filepath.Dir(thisFile), "testdata", "leaktest", "basic-http.yaml")
options := getDefaultOptions()
options.Targets = []string{server.URL}
options.Templates = []string{templatePath}
options.NoInteractsh = true
options.DisableStdin = true
options.BulkSize = 1
options.TemplateThreads = 1
options.PayloadConcurrency = 1
options.ExecutionId = xid.New().String()
runner.ParseOptions(options)
nucleiRunner, err := runner.New(options)
require.NoError(t, err)
require.NotNil(t, nucleiRunner)
defer nucleiRunner.Close()
require.NoError(t, nucleiRunner.RunEnumeration())
}
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"testing"
"time"
"github.com/projectdiscovery/goflags"
"github.com/stretchr/testify/require"
)
// Ensures nuclei's goflags dependency still exposes AddCommonFlags / -max-time,
// which is what cmd/nuclei wires via flagSet.AddCommonFlags().
func TestMaxTimeCommonFlagAvailable(t *testing.T) {
flagSet := goflags.NewFlagSet()
common := flagSet.AddCommonFlags()
require.NotNil(t, common)
require.NoError(t, flagSet.Parse("-max-time", "1h30m"))
require.Equal(t, 90*time.Minute, common.MaxTime)
flagSet = goflags.NewFlagSet()
common = flagSet.AddCommonFlags()
require.NoError(t, flagSet.Parse("-mt", "45s"))
require.Equal(t, 45*time.Second, common.MaxTime)
}
+184
View File
@@ -0,0 +1,184 @@
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"sync"
"testing"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
const inlineSecretsFatalChildEnv = "NUCLEI_INLINE_SECRETS_FATAL_CHILD"
func TestValidateResetPathRejectsBroadTargets(t *testing.T) {
if err := validateResetPath(""); err == nil {
t.Fatal("empty reset path was accepted")
}
if err := validateResetPath(string(filepath.Separator)); err == nil {
t.Fatal("filesystem root reset path was accepted")
}
if err := validateResetPath(filepath.Join(string(filepath.Separator), "var")); err == nil {
t.Fatal("broad top-level reset path was accepted")
}
if err := validateResetPath(os.TempDir()); err == nil {
t.Fatal("system temporary directory reset path was accepted")
}
if cwd, err := os.Getwd(); err == nil {
if err := validateResetPath(cwd); err == nil {
t.Fatal("current working directory reset path was accepted")
}
}
if homeDir, err := os.UserHomeDir(); err == nil {
if err := validateResetPath(homeDir); err == nil {
t.Fatal("user home reset path was accepted")
}
}
if err := validateResetPath(t.TempDir()); err != nil {
t.Fatalf("scoped reset path was rejected: %v", err)
}
}
func TestRemoveResetPathsValidatesAllTargetsBeforeDeletion(t *testing.T) {
root := t.TempDir()
configDir := filepath.Join(root, "config")
if err := os.Mkdir(configDir, 0o700); err != nil {
t.Fatalf("create config directory: %v", err)
}
paths := []resetPath{
{name: "config", path: configDir},
{name: "unsafe", path: string(filepath.Separator)},
}
if err := removeResetPaths(paths); err == nil {
t.Fatal("unsafe reset target was accepted")
}
if _, err := os.Stat(configDir); err != nil {
t.Fatalf("config directory was removed before validation completed: %v", err)
}
}
func TestCleanupInlineSecretsDirsRemovesPrivateDirectory(t *testing.T) {
secretsDir := filepath.Join(t.TempDir(), "nuclei-secrets-run")
if err := os.Mkdir(secretsDir, 0o700); err != nil {
t.Fatalf("create inline secrets directory: %v", err)
}
secretsPath := filepath.Join(secretsDir, "inline-secrets.yaml")
if err := os.WriteFile(secretsPath, []byte("static: []\n"), 0o600); err != nil {
t.Fatalf("write inline secrets file: %v", err)
}
inlineSecretsTempMu.Lock()
previous := inlineSecretsTempDir
inlineSecretsTempDir = secretsDir
inlineSecretsTempMu.Unlock()
t.Cleanup(func() {
inlineSecretsTempMu.Lock()
inlineSecretsTempDir = previous
inlineSecretsTempMu.Unlock()
})
cleanupInlineSecretsDirs()
if _, err := os.Stat(secretsDir); !os.IsNotExist(err) {
t.Fatalf("inline secrets directory still exists: %v", err)
}
}
func TestCleanupInlineSecretsDirsIsConcurrentSafe(t *testing.T) {
root := t.TempDir()
dir := filepath.Join(root, "nuclei-secrets")
if err := os.Mkdir(dir, 0o700); err != nil {
t.Fatalf("create inline secrets directory: %v", err)
}
inlineSecretsTempMu.Lock()
previous := inlineSecretsTempDir
inlineSecretsTempDir = dir
inlineSecretsTempMu.Unlock()
t.Cleanup(func() {
inlineSecretsTempMu.Lock()
inlineSecretsTempDir = previous
inlineSecretsTempMu.Unlock()
})
start := make(chan struct{})
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
<-start
cleanupInlineSecretsDirs()
}()
}
close(start)
wg.Wait()
if _, err := os.Stat(dir); !os.IsNotExist(err) {
t.Fatalf("inline secrets directory still exists: %s: %v", dir, err)
}
}
func TestReadConfigFatalCleansInlineSecretsDirectory(t *testing.T) {
if os.Getenv(inlineSecretsFatalChildEnv) == "1" {
options = &types.Options{Logger: gologger.DefaultLogger}
os.Args = []string{
os.Args[0],
"-profile", os.Getenv("NUCLEI_TEST_PROFILE"),
"-secret-file", os.Getenv("NUCLEI_TEST_MISSING_SECRET"),
"-disable-update-check",
}
readConfig()
os.Exit(0)
}
root := t.TempDir()
profilePath := filepath.Join(root, "profile.yaml")
if err := os.WriteFile(profilePath, []byte("secrets:\n static: []\n"), 0o600); err != nil {
t.Fatalf("write profile: %v", err)
}
missingSecret := filepath.Join(root, "missing-secrets.yaml")
tempRoot := filepath.Join(root, "tmp")
if err := os.Mkdir(tempRoot, 0o700); err != nil {
t.Fatalf("create temporary root: %v", err)
}
cmd := exec.Command(os.Args[0], "-test.run=^TestReadConfigFatalCleansInlineSecretsDirectory$", "-test.count=1")
cmd.Env = isolatedNucleiStorageEnv(root, tempRoot,
inlineSecretsFatalChildEnv+"=1",
"NUCLEI_TEST_PROFILE="+profilePath,
"NUCLEI_TEST_MISSING_SECRET="+missingSecret,
)
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("child process did not exit on the missing secrets file")
}
if !bytes.Contains(output, []byte("Secrets file")) || !bytes.Contains(output, []byte("does not exist")) {
t.Fatalf("child did not reach the missing secrets file failure:\n%s", output)
}
matches, err := filepath.Glob(filepath.Join(tempRoot, "nuclei-secrets-*"))
if err != nil {
t.Fatalf("list inline secrets directories: %v", err)
}
if len(matches) != 0 {
t.Fatalf("inline secrets directories remain after fatal exit: %v", matches)
}
}
func isolatedNucleiStorageEnv(root, tempRoot string, extra ...string) []string {
env := append(os.Environ(),
"TMPDIR="+tempRoot,
"TMP="+tempRoot,
"TEMP="+tempRoot,
"XDG_CONFIG_HOME="+filepath.Join(root, "config"),
"XDG_STATE_HOME="+filepath.Join(root, "state"),
"XDG_CACHE_HOME="+filepath.Join(root, "cache"),
"XDG_DATA_HOME="+filepath.Join(root, "data"),
"NUCLEI_CONFIG_DIR="+filepath.Join(root, "config", "nuclei"),
)
return append(env, extra...)
}
+16
View File
@@ -0,0 +1,16 @@
id: leaktest-basic-http
info:
name: CLI Leak Test Basic HTTP
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/"
matchers:
- type: status
status:
- 204
+63 -56
View File
@@ -1,6 +1,6 @@
module github.com/projectdiscovery/nuclei/v3
go 1.25.7
go 1.26
require (
github.com/andygrunwald/go-jira v1.16.1
@@ -13,16 +13,16 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/miekg/dns v1.1.68
github.com/miekg/dns v1.1.72
github.com/olekukonko/tablewriter v1.0.8
github.com/pkg/errors v0.9.1
github.com/projectdiscovery/clistats v0.1.4
github.com/projectdiscovery/fastdialer v0.5.11
github.com/projectdiscovery/hmap v0.0.101
github.com/projectdiscovery/clistats v0.1.6
github.com/projectdiscovery/fastdialer v0.5.18
github.com/projectdiscovery/hmap v0.0.102
github.com/projectdiscovery/interactsh v1.3.1
github.com/projectdiscovery/rawhttp v0.1.90
github.com/projectdiscovery/retryabledns v1.0.115
github.com/projectdiscovery/retryablehttp-go v1.3.16
github.com/projectdiscovery/rawhttp v0.1.91
github.com/projectdiscovery/retryabledns v1.0.116
github.com/projectdiscovery/retryablehttp-go v1.3.24
github.com/projectdiscovery/yamldoc-go v1.0.6
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
@@ -32,9 +32,9 @@ require (
github.com/syndtr/goleveldb v1.0.0
github.com/weppos/publicsuffix-go v0.50.3 // indirect
go.uber.org/multierr v1.11.0
golang.org/x/net v0.55.0
golang.org/x/oauth2 v0.34.0
golang.org/x/text v0.37.0
golang.org/x/net v0.57.0
golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.40.0
)
require (
@@ -49,6 +49,7 @@ require (
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
github.com/adrg/xdg v0.5.3
github.com/alexsnet/go-vnc v0.1.0
github.com/alitto/pond v1.9.2
github.com/antchfx/xmlquery v1.4.4
@@ -65,12 +66,13 @@ require (
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
github.com/docker/go-units v0.5.0
github.com/fatih/structs v1.1.0
github.com/getkin/kin-openapi v0.132.0
github.com/go-git/go-git/v5 v5.19.1
github.com/fullstorydev/grpcurl v1.9.3
github.com/getkin/kin-openapi v0.144.0
github.com/go-git/go-git/v5 v5.19.2
github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-pdf/fpdf v0.9.0
github.com/go-pg/pg/v10 v10.15.0
github.com/go-sql-driver/mysql v1.9.3
github.com/go-sql-driver/mysql v1.10.0
github.com/goccy/go-json v0.10.5
github.com/google/go-github/v30 v30.1.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@@ -78,9 +80,10 @@ require (
github.com/h2non/filetype v1.1.3
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/jhump/protoreflect v1.17.0
github.com/kitabisa/go-ci v1.0.3
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
github.com/lib/pq v1.11.2
github.com/lib/pq v1.12.3
github.com/logrusorgru/aurora/v4 v4.0.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/maypok86/otter/v2 v2.2.1
@@ -89,41 +92,41 @@ require (
github.com/oiweiwei/go-msrpc v1.2.12
github.com/ory/dockertest/v3 v3.12.0
github.com/praetorian-inc/fingerprintx v1.1.15
github.com/projectdiscovery/dsl v0.8.20
github.com/projectdiscovery/dsl v0.8.22
github.com/projectdiscovery/fasttemplate v0.0.2
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/goflags v0.2.0
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
github.com/projectdiscovery/gologger v1.1.71
github.com/projectdiscovery/gologger v1.1.72
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
github.com/projectdiscovery/httpx v1.9.0
github.com/projectdiscovery/httpx v1.10.0
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
github.com/projectdiscovery/networkpolicy v0.1.41
github.com/projectdiscovery/networkpolicy v0.1.48
github.com/projectdiscovery/ratelimit v0.0.88
github.com/projectdiscovery/rdap v0.9.0
github.com/projectdiscovery/sarif v0.1.0
github.com/projectdiscovery/tlsx v1.2.2
github.com/projectdiscovery/tlsx v1.3.2
github.com/projectdiscovery/uncover v1.2.1
github.com/projectdiscovery/useragent v0.0.108
github.com/projectdiscovery/utils v0.11.1
github.com/projectdiscovery/wappalyzergo v0.2.87
github.com/projectdiscovery/useragent v0.0.109
github.com/projectdiscovery/utils v0.11.2
github.com/projectdiscovery/wappalyzergo v0.2.95
github.com/redis/go-redis/v9 v9.11.0
github.com/rs/zerolog v1.34.0
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/sijms/go-ora/v2 v2.9.0
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
github.com/yassinebenaid/godump v0.11.1
github.com/zmap/zgrab2 v0.1.8
gitlab.com/gitlab-org/api/client-go v1.9.1
go.mongodb.org/mongo-driver v1.17.9
golang.org/x/sync v0.20.0
golang.org/x/term v0.43.0
golang.org/x/sync v0.22.0
golang.org/x/term v0.45.0
google.golang.org/grpc v1.82.1
gopkg.in/yaml.v3 v3.0.1
moul.io/http2curl v1.0.0
)
@@ -131,7 +134,7 @@ require (
require (
aead.dev/minisign v0.3.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
@@ -143,7 +146,7 @@ require (
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/PuerkitoBio/goquery v1.12.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
@@ -177,6 +180,7 @@ require (
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/brianvoe/gofakeit/v7 v7.2.1 // indirect
github.com/bufbuild/protocompile v0.14.1 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
@@ -195,12 +199,12 @@ require (
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
@@ -213,13 +217,15 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gaissmai/bart v0.28.0 // indirect
github.com/gaissmai/bart v0.29.0 // indirect
github.com/geoffgarside/ber v1.2.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
@@ -227,11 +233,12 @@ require (
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -241,6 +248,7 @@ require (
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
@@ -252,7 +260,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
github.com/hdm/jarm-go v0.0.7 // indirect
github.com/hdm/jarm-go v0.0.8 // indirect
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/indece-official/go-ebcdic v1.2.0 // indirect
@@ -264,12 +272,11 @@ require (
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/kataras/jwt v0.1.10 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@@ -296,13 +303,12 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/oasdiff/yaml v0.1.1 // indirect
github.com/oasdiff/yaml3 v0.0.14 // indirect
github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
@@ -312,19 +318,19 @@ require (
github.com/opencontainers/runc v1.3.6 // indirect
github.com/openrdap/rdap v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/blackrock v0.0.1 // indirect
github.com/projectdiscovery/cdncheck v1.2.42 // indirect
github.com/projectdiscovery/blackrock v0.0.2 // indirect
github.com/projectdiscovery/cdncheck v1.2.50 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashabaranov/go-openai v1.37.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
@@ -334,7 +340,7 @@ require (
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/tidwall/btree v1.8.1 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -368,21 +374,22 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark v1.7.17 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.3.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
mellium.im/sasl v0.3.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
)
require (
@@ -404,13 +411,13 @@ require (
go.etcd.io/bbolt v1.4.3 // indirect
go.uber.org/zap v1.27.0 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.45.0
google.golang.org/protobuf v1.36.11 // indirect
golang.org/x/tools v0.47.0
google.golang.org/protobuf v1.36.11
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
)
+126 -104
View File
@@ -41,8 +41,8 @@ code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBl
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
@@ -97,8 +97,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687 h1:HHkHNwakgAFX3qlMm88c090vIYNFWiO5+4WyAr0xJuM=
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687/go.mod h1:l6FRaC2TliS/JMNWskO3J1tmrcKJyOaMFhWC6hHeWno=
github.com/RumbleDiscovery/rumble-tools v0.0.0-20201105153123-f2adbb3244d2/go.mod h1:jD2+mU+E2SZUuAOHZvZj4xP4frlOo+N/YrXDvASFhkE=
@@ -106,6 +106,8 @@ github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi78=
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -219,6 +221,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -282,6 +286,8 @@ github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJ
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a h1:Ohw57yVY2dBTt+gsC6aZdteyxwlxfbtgkFEMTEkwgSw=
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
@@ -337,7 +343,11 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -357,14 +367,16 @@ github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b/go.mod h1:l2Jrml4vo
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1ZkyVU3jk=
github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gaissmai/bart v0.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@@ -383,11 +395,13 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -407,10 +421,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
github.com/go-pg/pg/v10 v10.15.0 h1:6DQwbaxJz/e4wvgzbxBkBLiL/Uuk87MGgHhkURtzx24=
@@ -429,11 +445,9 @@ github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw=
@@ -492,6 +506,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -577,8 +593,9 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf h1:umfGUaWdFP2s6457fz1+xXYIWDxdGc7HdkLS9aJ1skk=
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf/go.mod h1:V99KdStnMHZsvVOwIvhfcUzYgYkRZeQWUtumtL+SKxA=
github.com/hdm/jarm-go v0.0.7 h1:Eq0geenHrBSYuKrdVhrBdMMzOmA+CAMLzN2WrF3eL6A=
github.com/hdm/jarm-go v0.0.7/go.mod h1:kinGoS0+Sdn1Rr54OtanET5E5n7AlD6T6CrJAKDjJSQ=
github.com/hdm/jarm-go v0.0.8 h1:8yDIGrmhEJs+QW1voR3JU0Zaz+AUqs3CJzMAQKzGFIc=
github.com/hdm/jarm-go v0.0.8/go.mod h1:memdt/YMMW3811nvANDJsrtc8Fy2e1685aDWdI7Gwto=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -613,10 +630,11 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jlaffaye/ftp v0.0.0-20190624084859-c1312a7102bf/go.mod h1:lli8NYPQOFy3O++YmYbqVgOcQ1JPCwdOy+5zSjKJ9qY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -647,8 +665,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kitabisa/go-ci v1.0.3 h1:JmIUIvcercRQc/9x/v02ydCCqU4MadSHaNaOF8T2pGA=
github.com/kitabisa/go-ci v1.0.3/go.mod h1:e3wBSzaJbcifXrr/Gw2ZBLn44MmeqP5WySwXyHlCK/U=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -672,8 +690,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa h1:KQKuQDgA3DZX6C396lt3WDYB9Um1gLITLbvficVbqXk=
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa/go.mod h1:HbwNE4XGwjgtUELkvQaAOjWrpianHYZdQVNqSdYW3UM=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
@@ -716,8 +734,8 @@ github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minio-go/v6 v6.0.46/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
@@ -745,8 +763,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
@@ -761,10 +777,10 @@ github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
github.com/oiweiwei/go-msrpc v1.2.12 h1:gaxnv1cyX3v9l+NNxyr4ONyvh/mnmh8Egel9r8zxNxE=
github.com/oiweiwei/go-msrpc v1.2.12/go.mod h1:T6/ENmAoD1nYCr3NW8PS8AjIX0tZEAL7yO0tsejtK18=
github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4=
@@ -798,8 +814,6 @@ github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCy
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
@@ -810,51 +824,49 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
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/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ=
github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
github.com/projectdiscovery/cdncheck v1.2.42 h1:Y1Q9MPq7uuv25+aGlgjA5nToOcsk+9gNEKjicyhIwQI=
github.com/projectdiscovery/cdncheck v1.2.42/go.mod h1:9oE9KKxCSHNvUf0UaMeqqUwWpC38FkNaTll0ScIBT3w=
github.com/projectdiscovery/clistats v0.1.4 h1:kDnXoNxIdOvQElOF7k2Mt6XosGa5GbMKPtRXdPHMVzU=
github.com/projectdiscovery/clistats v0.1.4/go.mod h1:hjJYNcUubk9T3cuFvA+JkLhZGjzYW50fkC48dqUAtbU=
github.com/projectdiscovery/dsl v0.8.20 h1:CxWcKuoHFpOSS1kzqnbJuK5No/6qoRG8IzNDMnZ6c/M=
github.com/projectdiscovery/dsl v0.8.20/go.mod h1:e1oHi7mxAxF+UhBhD5gOk90Ga6LQqvFea2voMO1E5D0=
github.com/projectdiscovery/fastdialer v0.5.11 h1:eI7jfwz0i73Ot1cowIBezQLxbg0i6INdAsFGJjfwPa0=
github.com/projectdiscovery/fastdialer v0.5.11/go.mod h1:W1ZkULr9mMR6i0oRFTztANnpVyEEzPUovK8sUM4eAw8=
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.2.50 h1:KZBYT4jKNvlmlDKJqGMJ7r6XtwAim82oEZLaI+NAWWk=
github.com/projectdiscovery/cdncheck v1.2.50/go.mod h1:9oE9KKxCSHNvUf0UaMeqqUwWpC38FkNaTll0ScIBT3w=
github.com/projectdiscovery/clistats v0.1.6 h1:e2Hc7cVWKJ4sCbyQ7th5zuBvgqsZcLwRq1qY0vmQXY4=
github.com/projectdiscovery/clistats v0.1.6/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
github.com/projectdiscovery/dsl v0.8.22 h1:ZfPunLaaYUJTBffE1ShuAAV9hkSH2thvlszK48FlZS0=
github.com/projectdiscovery/dsl v0.8.22/go.mod h1:fr8WMU381As89e4Z+SQ8tYQ1NudniWAS6NLPHrhe+Vs=
github.com/projectdiscovery/fastdialer v0.5.18 h1:bBGJdni/xruoUISBug86KwRpuewLg1lEUfQLTg3p0G0=
github.com/projectdiscovery/fastdialer v0.5.18/go.mod h1:fH4CDmC7Dk7Shm4v3Rsql7ql3OhlKeZfOdAlc/muP1I=
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/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/go-smb2 v0.0.0-20240129202741-052cc450c6cb h1:rutG906Drtbpz4DwU5mhGIeOhRcktDH4cGQitGUMAsg=
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb/go.mod h1:FLjF1DmZ+POoGEiIQdWuYVwS++C/GwpX8YaCsTSm1RY=
github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c=
github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4=
github.com/projectdiscovery/goflags v0.2.0 h1:zj16hnTmLmlnVYm02hfOrCY6oYTu/AQDBE99yZkS+s8=
github.com/projectdiscovery/goflags v0.2.0/go.mod h1:7nAP1r2Dqgn/rwmOE3EWbZWUCEJKNIhVSBGpuzJAIns=
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534 h1:hYd1zQA/dxO2ASyQ6Re73TcJkW1LjLQvt4+86Hxefz8=
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534/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.71 h1:IYU4mw9viKdSzMTIGVpYuw1Gtg7QIHIStqAQgeNXcBQ=
github.com/projectdiscovery/gologger v1.1.71/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
github.com/projectdiscovery/gologger v1.1.72 h1:PKk+aSx3jYOCBPi+FD+nTnhe+Vxx9u3K7Ega0GVlMCI=
github.com/projectdiscovery/gologger v1.1.72/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/hmap v0.0.101 h1:zXM6YtLmsn8Q0CUUw8QavhqWmiQYwaw+/U679Rr00pc=
github.com/projectdiscovery/hmap v0.0.101/go.mod h1:w6N9/a5H8kvyx53AhtPDUWe5Qq3D6NBDPA23glHpa/Q=
github.com/projectdiscovery/httpx v1.9.0 h1:5yn4ik/LqZ+v3MLgU7+CZJQyND9osW9NmZ3squylxsc=
github.com/projectdiscovery/httpx v1.9.0/go.mod h1:jGTRyUHddo2WyK4klWIwQXgGF1Lu39XVyzlue4H3pX8=
github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP1Erl8kaU=
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
github.com/projectdiscovery/httpx v1.10.0 h1:sZvfeJDeX4eJfB5Oh29My4gOHmLfvpCt2wCqhargJrE=
github.com/projectdiscovery/httpx v1.10.0/go.mod h1:gfdNnFznLLlAgAgSQlYjUeEVvYzKrNV0jO5Qyo5w+FI=
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
@@ -865,32 +877,32 @@ 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.41 h1:6daf4A8Vj1+iQ7nH2FR4+sOYd7q1WH1qfN61EyQU74c=
github.com/projectdiscovery/networkpolicy v0.1.41/go.mod h1:9ULLaMbdv9UnT0C5rmuK4nIwYs0o776xMnkPUb8TtaE=
github.com/projectdiscovery/networkpolicy v0.1.48 h1:s/d0nEyfkHYVBeZI/bgRiOCS66KDxv3+s7Hb+AvepHY=
github.com/projectdiscovery/networkpolicy v0.1.48/go.mod h1:bb2ZkDnOeVuR9Ok8BdCdoBEz8S9Y9EFvFFcn5FTUV2U=
github.com/projectdiscovery/ratelimit v0.0.88 h1:AcurW9aLRzlEyPe9kSjnOpr3XzLMWTpiWAlW/w73ALU=
github.com/projectdiscovery/ratelimit v0.0.88/go.mod h1:CU1s+68UUG2mctSl2wi32/DHLJA6TMg+4rxgP59LfVk=
github.com/projectdiscovery/rawhttp v0.1.90 h1:LOSZ6PUH08tnKmWsIwvwv1Z/4zkiYKYOSZ6n+8RFKtw=
github.com/projectdiscovery/rawhttp v0.1.90/go.mod h1:VZYAM25UI/wVB3URZ95ZaftgOnsbphxyAw/XnQRRz4Y=
github.com/projectdiscovery/rawhttp v0.1.91 h1:6EIhZxCkBn27kJAl47FieCFCe4H2ZdbhoBVj0J6ZCVY=
github.com/projectdiscovery/rawhttp v0.1.91/go.mod h1:VZYAM25UI/wVB3URZ95ZaftgOnsbphxyAw/XnQRRz4Y=
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.115 h1:RKV63FNIznFHUoawg/1hs53pVH3wqPtFhwstCuxVSoA=
github.com/projectdiscovery/retryabledns v1.0.115/go.mod h1:+fEMWoPigw+M0lGNKY7AZ+g8FIgj+4sONjsinMmeL3k=
github.com/projectdiscovery/retryablehttp-go v1.3.16 h1:M/xICwSaiSHhd5OIarU3+5JoU7VmbiSAAwYUCK7CTjw=
github.com/projectdiscovery/retryablehttp-go v1.3.16/go.mod h1:s0azLAqAbcVCjHI9t0ezPhamevYGM1eoOvFkn4QmpZ8=
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.24 h1:bODdeUEka/U5SS9wfnhoAjBRg1wq/AJdwb9CUNIOR88=
github.com/projectdiscovery/retryablehttp-go v1.3.24/go.mod h1:OL5zZEBd59GXEMaDjEHW/bfGlFsV441ADz64hWajgxc=
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.2.2 h1:Y96QBqeD2anpzEtBl4kqNbwzXh2TrzJuXfgiBLvK+SE=
github.com/projectdiscovery/tlsx v1.2.2/go.mod h1:ZJl9F1sSl0sdwE+lR0yuNHVX4Zx6tCSTqnNxnHCFZB4=
github.com/projectdiscovery/tlsx v1.3.2 h1:1Lh2ith79o8R+rOOSBZiMq8EiZ8j4D3T/8ENO+gAqXA=
github.com/projectdiscovery/tlsx v1.3.2/go.mod h1:2wgTGC/sourHvoR+RX8cAok0Sa8YK6QbEP3xo5SDzPI=
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.108 h1:fb+uLuFJvC+MHZjCtxQJxtvp1X6A8n98CUGPyFcg3NE=
github.com/projectdiscovery/useragent v0.0.108/go.mod h1:XdNRrlvtDmYfVL1Oybat4uMe+W6cLwsK9S18ond17CI=
github.com/projectdiscovery/utils v0.11.1 h1:PWj1KjIASxt8icxommH72C0TQqNOvGkcSODRkiq0SQw=
github.com/projectdiscovery/utils v0.11.1/go.mod h1:yktGrHGk2CTjNiccXovnvGrLHX9sV2bqz9nSnbA3V8M=
github.com/projectdiscovery/wappalyzergo v0.2.87 h1:KuUpeRSQ80L6tx9YaAPhfYWTF47bpEURYbAymr76zto=
github.com/projectdiscovery/wappalyzergo v0.2.87/go.mod h1:gMH0o5lBp65sKMwHx/tuUdOtW2RjodC6Ti+9QDsYMkY=
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.2 h1:gfvXFBEHN4DgX8HBRzsoO1s2wH/2R+cH17cLpC865x4=
github.com/projectdiscovery/utils v0.11.2/go.mod h1:HMxhxLigsAr+M9Oa8n9Z0ROZcs8wAJHGsgR7UGb+oUE=
github.com/projectdiscovery/wappalyzergo v0.2.95 h1:XHjbQVrrxusezFq7ciWmtznPKWhpr/srWp7Yk2noxqg=
github.com/projectdiscovery/wappalyzergo v0.2.95/go.mod h1:E2p8L90ysTTUkU5FUOgGoCQ7ucEUgqJ/tvYgiGGAlEM=
github.com/projectdiscovery/yamldoc-go v1.0.6 h1:GCEdIRlQjDux28xTXKszM7n3jlMf152d5nqVpVoetas=
github.com/projectdiscovery/yamldoc-go v1.0.6/go.mod h1:R5lWrNzP+7Oyn77NDVPnBsxx2/FyQZBBkIAaSaCQFxw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -937,6 +949,8 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
@@ -978,6 +992,8 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -997,8 +1013,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9 h1:GXIyLuIJ5Qk46lI8WJ83qHBZKUI3zhmMmuoY9HICUIQ=
@@ -1113,8 +1129,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.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
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-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -1156,16 +1172,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -1174,8 +1190,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
goftp.io/server/v2 v2.0.1 h1:H+9UbCX2N206ePDSVNCjBftOKOgil6kQ5RAQNx5hJwE=
@@ -1206,8 +1222,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1245,8 +1261,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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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=
@@ -1296,8 +1312,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1307,8 +1323,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1325,8 +1341,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1393,8 +1409,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1408,8 +1424,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1427,8 +1443,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1480,12 +1496,14 @@ 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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -1538,6 +1556,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1550,6 +1570,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1612,5 +1634,5 @@ pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
software.sslmate.com/src/go-pkcs12 v0.7.2 h1:Rh9FoMaI5k7Oo6EOS+2/BnoZ+JFIS+XHjM0VGkSPXLM=
software.sslmate.com/src/go-pkcs12 v0.7.2/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
+198
View File
@@ -0,0 +1,198 @@
package configuration
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"slices"
"github.com/adrg/xdg"
"github.com/projectdiscovery/goflags"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
// Selection contains the config and profile selected for one invocation.
type Selection struct {
Config string
Profile string
}
// Load applies automatic config files, one selected config file, and one
// selected profile in priority order. Explicit CLI flags retain precedence.
func Load(
cfg *catalogconfig.Config,
flagSet *goflags.FlagSet,
selected *Selection,
resolveProfile func(string) (string, error),
args ...string,
) ([]error, error) {
userConfigFile := cfg.GetFlagsConfigFilePath()
defaultUserConfigFile := filepath.Join(xdg.ConfigHome, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
automaticFiles := automaticConfigFiles(xdg.ConfigDirs, runtime.GOOS, userConfigFile)
return loadConfigFiles(userConfigFile, defaultUserConfigFile, automaticFiles, flagSet, selected, resolveProfile, args...)
}
func loadConfigFiles(
userConfigFile string,
defaultUserConfigFile string,
automaticFiles []string,
flagSet *goflags.FlagSet,
selected *Selection,
resolveProfile func(string) (string, error),
args ...string,
) ([]error, error) {
var warnings []error
if err := prepareUserConfigFile(userConfigFile, defaultUserConfigFile); err != nil {
warnings = append(warnings, err)
}
flagSet.SetConfigFilePaths(automaticFiles...)
if err := flagSet.Parse(args...); err != nil {
warnings = append(warnings, fmt.Errorf("automatic configuration was ignored: %w", err))
}
if err := mergeSelectedFiles(flagSet, selected, resolveProfile); err != nil {
return warnings, err
}
return warnings, nil
}
func mergeSelectedFiles(
flagSet *goflags.FlagSet,
selected *Selection,
resolveProfile func(string) (string, error),
) error {
configFile := selected.Config
if configFile != "" {
if err := flagSet.MergeConfigFile(configFile); err != nil {
return fmt.Errorf("read config file %q: %w", configFile, err)
}
// A selected config may choose a profile, but it cannot redirect its
// completed config stage.
selected.Config = configFile
}
if selected.Profile == "" {
return nil
}
profileFile, err := resolveProfile(selected.Profile)
if err != nil {
return err
}
if err := flagSet.MergeConfigFile(profileFile); err != nil {
return fmt.Errorf("read template profile %q: %w", profileFile, err)
}
// A profile cannot redirect either completed file-selection stage.
selected.Config = configFile
selected.Profile = profileFile
return nil
}
func automaticConfigFiles(configDirs []string, goos, userConfigFile string) []string {
highestFirst := make([]string, 0, len(configDirs)+2)
highestFirst = append(highestFirst, userConfigFile)
if unixSystemFile := unixSystemConfigFilePath(goos); unixSystemFile != "" {
highestFirst = append(highestFirst, unixSystemFile)
}
for _, configDir := range configDirs {
if filepath.IsAbs(configDir) {
highestFirst = append(highestFirst, filepath.Join(configDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName))
}
}
files := make([]string, 0, len(highestFirst))
seen := make(map[string]struct{}, len(highestFirst))
for _, file := range highestFirst {
if file == "" {
continue
}
cleanFile := filepath.Clean(file)
if _, exists := seen[cleanFile]; exists {
continue
}
seen[cleanFile] = struct{}{}
files = append(files, file)
}
slices.Reverse(files)
return files
}
func unixSystemConfigFilePath(goos string) string {
switch goos {
case "aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
return path.Join("/etc", catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
default:
return ""
}
}
func prepareUserConfigFile(configFile, defaultConfigFile string) error {
if filepath.Clean(configFile) == filepath.Clean(defaultConfigFile) {
return nil
}
if _, err := os.Stat(configFile); err == nil {
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("inspect custom config file %q: %w", configFile, err)
}
data, err := os.ReadFile(defaultConfigFile)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return fmt.Errorf("read default config file %q: %w", defaultConfigFile, err)
}
if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
return fmt.Errorf("create custom config directory %q: %w", filepath.Dir(configFile), err)
}
file, err := os.OpenFile(configFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if errors.Is(err, os.ErrExist) {
return nil
}
if err != nil {
return fmt.Errorf("create custom config file %q: %w", configFile, err)
}
written := false
defer func() {
_ = file.Close()
if !written {
_ = os.Remove(configFile)
}
}()
if _, err := file.Write(data); err != nil {
return fmt.Errorf("copy default config from %q to %q: %w", defaultConfigFile, configFile, err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("close custom config file %q: %w", configFile, err)
}
written = true
return nil
}
+443
View File
@@ -0,0 +1,443 @@
package configuration
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/projectdiscovery/goflags"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func TestConfigurationPrecedence(t *testing.T) {
dir := t.TempDir()
preferredSystemDir := filepath.Join(dir, "preferred-system")
fallbackSystemDir := filepath.Join(dir, "fallback-system")
preferredSystemPath := filepath.Join(preferredSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
fallbackSystemPath := filepath.Join(fallbackSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
unixSystemPath := filepath.Join(dir, "etc", catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
userPath := filepath.Join(dir, "user.yaml")
explicitPath := filepath.Join(dir, "explicit.yaml")
profilePath := filepath.Join(dir, "profile.yaml")
writeConfigFile(t, fallbackSystemPath, `
fallback-only: fallback
preferred-wins: fallback
local-wins: fallback
user-wins: fallback
explicit-wins: fallback
profile-wins: fallback
cli-wins: fallback
default-wins: fallback
config: ignored.yaml
profile: ignored.yaml
`)
writeConfigFile(t, preferredSystemPath, `
preferred-wins: preferred
local-wins: preferred
user-wins: preferred
explicit-wins: preferred
profile-wins: preferred
cli-wins: preferred
default-wins: preferred
`)
writeConfigFile(t, unixSystemPath, `
local-wins: local
user-wins: local
explicit-wins: local
profile-wins: local
cli-wins: local
default-wins: local
`)
writeConfigFile(t, userPath, fmt.Sprintf(`
user-wins: user
explicit-wins: user
profile-wins: user
cli-wins: user
default-wins: user
config: %q
profile: ignored-user.yaml
`, explicitPath))
writeConfigFile(t, explicitPath, fmt.Sprintf(`
explicit-wins: explicit
profile-wins: explicit
cli-wins: explicit
default-wins: explicit
profile: %q
`, profilePath))
writeConfigFile(t, profilePath, `
profile-wins: profile
cli-wins: profile
default-wins: profile
`)
var fallbackOnly, preferredWins, localWins, userWins, explicitWins, profileWins, cliWins, defaultWins string
var selected Selection
flagSet := goflags.NewFlagSet()
flagSet.StringVar(&fallbackOnly, "fallback-only", "built-in", "")
flagSet.StringVar(&preferredWins, "preferred-wins", "built-in", "")
flagSet.StringVar(&localWins, "local-wins", "built-in", "")
flagSet.StringVar(&userWins, "user-wins", "built-in", "")
flagSet.StringVar(&explicitWins, "explicit-wins", "built-in", "")
flagSet.StringVar(&profileWins, "profile-wins", "built-in", "")
flagSet.StringVar(&cliWins, "cli-wins", "built-in", "")
flagSet.StringVar(&defaultWins, "default-wins", "built-in", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
automaticFiles := []string{fallbackSystemPath, preferredSystemPath, unixSystemPath, userPath}
automaticErr, err := loadTestFlags(flagSet, automaticFiles, &selected, existingProfilePath, "-cli-wins=cli", "-default-wins=built-in")
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
assertEqual(t, "fallback-only", fallbackOnly, "fallback")
assertEqual(t, "preferred-wins", preferredWins, "preferred")
assertEqual(t, "local-wins", localWins, "local")
assertEqual(t, "user-wins", userWins, "user")
assertEqual(t, "explicit-wins", explicitWins, "explicit")
assertEqual(t, "profile-wins", profileWins, "profile")
assertEqual(t, "cli-wins", cliWins, "cli")
assertEqual(t, "default-wins", defaultWins, "built-in")
assertEqual(t, "selected config", selected.Config, explicitPath)
assertEqual(t, "selected profile", selected.Profile, profilePath)
}
func TestAutomaticConfigFiles(t *testing.T) {
dir := t.TempDir()
preferredSystemDir := filepath.Join(dir, "preferred")
fallbackSystemDir := filepath.Join(dir, "fallback")
userConfigDir := filepath.Join(dir, "user")
preferredSystemFile := filepath.Join(preferredSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
fallbackSystemFile := filepath.Join(fallbackSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
userConfigFile := filepath.Join(userConfigDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
unixSystemFile := unixSystemConfigFilePath("linux")
got := automaticConfigFiles([]string{preferredSystemDir, "relative-system", fallbackSystemDir}, "linux", userConfigFile)
want := []string{
fallbackSystemFile,
preferredSystemFile,
unixSystemFile,
userConfigFile,
}
if !slices.Equal(got, want) {
t.Fatalf("automatic config files = %v, want %v", got, want)
}
got = automaticConfigFiles([]string{preferredSystemDir, "/etc", userConfigDir}, "linux", userConfigFile)
want = []string{
preferredSystemFile,
unixSystemFile,
userConfigFile,
}
if !slices.Equal(got, want) {
t.Fatalf("automatic config files with duplicate system paths = %v, want %v", got, want)
}
got = automaticConfigFiles([]string{preferredSystemDir, "/etc"}, "linux", unixSystemFile)
want = []string{
preferredSystemFile,
unixSystemFile,
}
if !slices.Equal(got, want) {
t.Fatalf("automatic config files with Unix path used as user path = %v, want %v", got, want)
}
}
func TestHigherPriorityFilesReplaceCollections(t *testing.T) {
dir := t.TempDir()
systemPath := filepath.Join(dir, "system.yaml")
userPath := filepath.Join(dir, "user.yaml")
explicitPath := filepath.Join(dir, "explicit.yaml")
profilePath := filepath.Join(dir, "profile.yaml")
writeConfigFile(t, systemPath, "replace: [system-a, system-b]\nclear: [system]\nvar: [system=one]\n")
writeConfigFile(t, userPath, fmt.Sprintf("replace: [user]\nclear: [user]\nvar: [user=two]\nconfig: %q\n", explicitPath))
writeConfigFile(t, explicitPath, fmt.Sprintf("replace: [explicit-a, explicit-b]\nclear: [explicit]\nvar: [explicit=three]\nprofile: %q\n", profilePath))
writeConfigFile(t, profilePath, "replace: [profile]\nclear: []\nvar: [profile=four]\n")
var replace, clear goflags.StringSlice
var variables goflags.RuntimeMap
var selected Selection
flagSet := goflags.NewFlagSet()
flagSet.StringSliceVar(&replace, "replace", nil, "", goflags.StringSliceOptions)
flagSet.StringSliceVar(&clear, "clear", nil, "", goflags.StringSliceOptions)
flagSet.RuntimeMapVar(&variables, "var", nil, "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-config="+explicitPath)
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
if !slices.Equal(replace, []string{"profile"}) {
t.Fatalf("replace = %v, want [profile]", replace)
}
if len(clear) != 0 {
t.Fatalf("clear = %v, want an empty list", clear)
}
if got := variables.AsMap(); len(got) != 1 || got["profile"] != "four" {
t.Fatalf("var = %v, want only profile=four", got)
}
}
func TestParsingCreatesPrimaryUserConfig(t *testing.T) {
tests := []struct {
name string
createSystem bool
wantValue string
forbidInUser string
}{
{name: "missing system", wantValue: "built-in"},
{name: "system values are not materialized", createSystem: true, wantValue: "system", forbidInUser: "system"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
systemPath := filepath.Join(dir, "system.yaml")
userPath := filepath.Join(dir, "user", "yaml")
if test.createSystem {
writeConfigFile(t, systemPath, "value: system\n")
}
value := "built-in"
var selected Selection
var noop bool
flagSet := goflags.NewFlagSet()
flagSet.StringVar(&value, "value", "built-in", "value description")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
flagSet.BoolVar(&noop, "noop", false, "")
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-noop=false")
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
assertEqual(t, "value", value, test.wantValue)
data, err := os.ReadFile(userPath)
if err != nil {
t.Fatalf("read generated user config: %v", err)
}
content := string(data)
if !strings.Contains(content, "#value: built-in") {
t.Fatalf("generated user config is not a commented built-in config:\n%s", content)
}
if test.forbidInUser != "" && strings.Contains(content, test.forbidInUser) {
t.Fatalf("generated user config materialized %q:\n%s", test.forbidInUser, content)
}
})
}
}
func TestSelectedConfigLoadsAfterMalformedAutomaticConfig(t *testing.T) {
dir := t.TempDir()
systemPath := filepath.Join(dir, "system.yaml")
userPath := filepath.Join(dir, "user.yaml")
explicitPath := filepath.Join(dir, "explicit.yaml")
writeConfigFile(t, systemPath, "value: system\nautomatic-only: system\n")
writeConfigFile(t, userPath, "value: [\n")
writeConfigFile(t, explicitPath, "value: explicit\n")
value := "built-in"
automaticOnly := "built-in"
var selected Selection
flagSet := goflags.NewFlagSet()
flagSet.StringVar(&value, "value", "built-in", "")
flagSet.StringVar(&automaticOnly, "automatic-only", "built-in", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-config="+explicitPath)
if err != nil {
t.Fatalf("valid explicit config failed after malformed automatic config: %v", err)
}
if automaticErr == nil {
t.Fatal("expected malformed automatic configuration error")
}
assertEqual(t, "value", value, "explicit")
assertEqual(t, "automatic-only", automaticOnly, "built-in")
}
func TestDeferredUpdateConfiguration(t *testing.T) {
dir := t.TempDir()
userPath := filepath.Join(dir, "user.yaml")
updateDir := filepath.Join(dir, "templates")
writeConfigFile(t, userPath, fmt.Sprintf(`
disable-update-check: true
update: true
update-templates: true
update-template-dir: %q
`, updateDir))
var disableUpdateCheck, updateBinary, updateTemplates, noop bool
var selected Selection
var selectedUpdateDir string
flagSet := goflags.NewFlagSet()
flagSet.BoolVar(&disableUpdateCheck, "disable-update-check", false, "")
flagSet.BoolVar(&updateBinary, "update", false, "")
flagSet.BoolVar(&updateTemplates, "update-templates", false, "")
flagSet.StringVar(&selectedUpdateDir, "update-template-dir", "", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
flagSet.BoolVar(&noop, "noop", false, "")
automaticErr, err := loadTestFlags(flagSet, []string{filepath.Join(dir, "missing-system.yaml"), userPath}, &selected, existingProfilePath, "-noop=false")
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
if !disableUpdateCheck || !updateBinary || !updateTemplates {
t.Fatalf("update values were not loaded: disable=%t update=%t templates=%t", disableUpdateCheck, updateBinary, updateTemplates)
}
assertEqual(t, "update template directory", selectedUpdateDir, updateDir)
testConfig := &catalogconfig.Config{}
if !testConfig.CanCheckForUpdates() {
t.Fatal("configuration loading applied disable-update-check too early")
}
if disableUpdateCheck {
testConfig.DisableUpdateCheck()
}
if testConfig.CanCheckForUpdates() {
t.Fatal("deferred disable-update-check was not applied")
}
}
func TestConfigFilesDoNotExecuteCallbacks(t *testing.T) {
dir := t.TempDir()
systemPath := filepath.Join(dir, "system.yaml")
userPath := filepath.Join(dir, "user.yaml")
explicitPath := filepath.Join(dir, "explicit.yaml")
profilePath := filepath.Join(dir, "profile.yaml")
callbackYAML := "reset: true\nversion: true\ntemplates-version: true\n"
writeConfigFile(t, systemPath, callbackYAML)
writeConfigFile(t, userPath, fmt.Sprintf("%sconfig: %q\n", callbackYAML, explicitPath))
writeConfigFile(t, explicitPath, fmt.Sprintf("%sprofile: %q\n", callbackYAML, profilePath))
writeConfigFile(t, profilePath, callbackYAML)
callbackCount := 0
var selected Selection
var noop bool
flagSet := goflags.NewFlagSet()
flagSet.CallbackVar(func() { callbackCount++ }, "reset", "")
flagSet.CallbackVar(func() { callbackCount++ }, "version", "")
flagSet.CallbackVar(func() { callbackCount++ }, "templates-version", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
flagSet.BoolVar(&noop, "noop", false, "")
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-noop=false")
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
if callbackCount != 0 {
t.Fatalf("configuration executed %d callbacks", callbackCount)
}
flagSet = goflags.NewFlagSet()
flagSet.CallbackVar(func() { callbackCount++ }, "reset", "")
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
automaticErr, err = loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-reset")
if err != nil {
t.Fatalf("load configuration with CLI callback failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration with CLI callback failed: %v", automaticErr)
}
if callbackCount != 1 {
t.Fatalf("CLI callback count = %d, want 1", callbackCount)
}
}
func TestSelectedFilesCannotRedirectTheirOwnStage(t *testing.T) {
dir := t.TempDir()
userPath := filepath.Join(dir, "user.yaml")
configPath := filepath.Join(dir, "config.yaml")
otherConfigPath := filepath.Join(dir, "other-config.yaml")
profilePath := filepath.Join(dir, "profile.yaml")
otherProfilePath := filepath.Join(dir, "other-profile.yaml")
writeConfigFile(t, userPath, fmt.Sprintf("config: %q\n", configPath))
writeConfigFile(t, configPath, fmt.Sprintf("config: %q\nprofile: %q\n", otherConfigPath, profilePath))
writeConfigFile(t, otherConfigPath, "other-config-loaded: true\n")
writeConfigFile(t, profilePath, fmt.Sprintf("config: %q\nprofile: %q\n", otherConfigPath, otherProfilePath))
writeConfigFile(t, otherProfilePath, "other-profile-loaded: true\n")
var selected Selection
var otherConfigLoaded, otherProfileLoaded bool
flagSet := goflags.NewFlagSet()
flagSet.StringVar(&selected.Config, "config", "", "")
flagSet.StringVar(&selected.Profile, "profile", "", "")
flagSet.BoolVar(&otherConfigLoaded, "other-config-loaded", false, "")
flagSet.BoolVar(&otherProfileLoaded, "other-profile-loaded", false, "")
automaticErr, err := loadTestFlags(flagSet, []string{userPath}, &selected, existingProfilePath, "--")
if err != nil {
t.Fatalf("load configuration failed: %v", err)
}
if automaticErr != nil {
t.Fatalf("automatic configuration failed: %v", automaticErr)
}
assertEqual(t, "selected config", selected.Config, configPath)
assertEqual(t, "selected profile", selected.Profile, profilePath)
if otherConfigLoaded || otherProfileLoaded {
t.Fatalf("loading followed a redirected stage: config=%t profile=%t", otherConfigLoaded, otherProfileLoaded)
}
}
func loadTestFlags(
flagSet *goflags.FlagSet,
automaticFiles []string,
selected *Selection,
resolveProfile func(string) (string, error),
args ...string,
) (error, error) {
userConfigFile := automaticFiles[len(automaticFiles)-1]
warnings, err := loadConfigFiles(userConfigFile, userConfigFile, automaticFiles, flagSet, selected, resolveProfile, args...)
return errors.Join(warnings...), err
}
func existingProfilePath(path string) (string, error) {
if _, err := os.Stat(path); err != nil {
return "", err
}
return path, nil
}
func writeConfigFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("create config directory: %v", err)
}
if err := os.WriteFile(path, []byte(strings.TrimPrefix(content, "\n")), 0o600); err != nil {
t.Fatalf("write config file %q: %v", path, err)
}
}
func assertEqual[T comparable](t *testing.T, name string, got, want T) {
t.Helper()
if got != want {
t.Fatalf("%s = %v, want %v", name, got, want)
}
}
+67
View File
@@ -0,0 +1,67 @@
package configuration
import (
"os"
"path/filepath"
"testing"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func TestUnixSystemConfigFilePath(t *testing.T) {
for _, goos := range []string{"aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris"} {
if got, want := unixSystemConfigFilePath(goos), "/etc/nuclei/config.yaml"; got != want {
t.Fatalf("unix system config for %s = %q, want %q", goos, got, want)
}
}
for _, goos := range []string{"windows", "plan9", "js"} {
if got := unixSystemConfigFilePath(goos); got != "" {
t.Fatalf("system config for %s = %q, want empty", goos, got)
}
}
}
func TestPrepareUserConfigFile(t *testing.T) {
dir := t.TempDir()
defaultConfigFile := filepath.Join(dir, "default", catalogconfig.CLIConfigFileName)
customConfigFile := filepath.Join(dir, "custom", catalogconfig.CLIConfigFileName)
if err := os.MkdirAll(filepath.Dir(defaultConfigFile), 0o700); err != nil {
t.Fatalf("create default config directory: %v", err)
}
if err := os.WriteFile(defaultConfigFile, []byte("value: inherited\n"), 0o600); err != nil {
t.Fatalf("write default config: %v", err)
}
if err := prepareUserConfigFile(customConfigFile, defaultConfigFile); err != nil {
t.Fatalf("prepare user config file: %v", err)
}
data, err := os.ReadFile(customConfigFile)
if err != nil {
t.Fatalf("read inherited config: %v", err)
}
if got, want := string(data), "value: inherited\n"; got != want {
t.Fatalf("inherited config = %q, want %q", got, want)
}
if err := os.WriteFile(defaultConfigFile, []byte("value: changed\n"), 0o600); err != nil {
t.Fatalf("update default config: %v", err)
}
if err := prepareUserConfigFile(customConfigFile, defaultConfigFile); err != nil {
t.Fatalf("prepare existing user config: %v", err)
}
data, err = os.ReadFile(customConfigFile)
if err != nil {
t.Fatalf("read existing user config: %v", err)
}
if got, want := string(data), "value: inherited\n"; got != want {
t.Fatalf("existing user config = %q, want %q", got, want)
}
missingConfigFile := filepath.Join(dir, "missing-custom", catalogconfig.CLIConfigFileName)
if err := prepareUserConfigFile(missingConfigFile, filepath.Join(dir, "missing-default.yaml")); err != nil {
t.Fatalf("prepare config without default source: %v", err)
}
if _, err := os.Stat(missingConfigFile); !os.IsNotExist(err) {
t.Fatalf("missing default source created user config: %v", err)
}
}
+187
View File
@@ -0,0 +1,187 @@
package configuration
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
fileutil "github.com/projectdiscovery/utils/file"
)
// TemplatesDirectory returns the final template directory used while loading
// configuration and resolving profiles.
func TemplatesDirectory(options *types.Options, config *catalogconfig.Config) string {
if templatesDir, overridden := TemplatesDirectoryOverride(options); overridden {
return templatesDir
}
return config.GetTemplateDir()
}
// TemplatesDirectoryOverride returns an explicit flag/config or environment
// template directory.
func TemplatesDirectoryOverride(options *types.Options) (string, bool) {
templatesDir := options.NewTemplatesDirectory
if templatesDir == "" {
templatesDir = os.Getenv(catalogconfig.NucleiTemplatesDirEnv)
}
if templatesDir == "" {
return "", false
}
if absPath, err := filepath.Abs(templatesDir); err == nil {
return absPath, true
}
return templatesDir, true
}
// ResolveProfilePath resolves a profile path or profile ID against the active
// template root.
func ResolveProfilePath(profilePath, templatesDir string) (string, error) {
defaultProfilesPath := filepath.Join(templatesDir, "profiles")
if filepath.Ext(profilePath) == "" {
path, err := findProfilePathByID(profilePath, defaultProfilesPath)
if err != nil {
return "", err
}
if path == "" {
return "", fmt.Errorf("%q is not a profile ID or profile path", profilePath)
}
profilePath = path
}
if !filepath.IsAbs(profilePath) {
if filepath.Dir(profilePath) == "profiles" {
defaultProfilesPath = templatesDir
}
currentDir, err := os.Getwd()
if err == nil && fileutil.FileExists(filepath.Join(currentDir, profilePath)) {
profilePath = filepath.Join(currentDir, profilePath)
} else {
profilePath = filepath.Join(defaultProfilesPath, profilePath)
}
}
if !fileutil.FileExists(profilePath) {
return "", fmt.Errorf("given template profile file %q does not exist", profilePath)
}
return profilePath, nil
}
func findProfilePathByID(profileID, profilesDir string) (string, error) {
var profilePath string
err := filepath.WalkDir(profilesDir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
ext := filepath.Ext(path)
if ext != extensions.YAML && ext != extensions.YML {
return nil
}
if strings.TrimSuffix(filepath.Base(path), ext) == profileID {
profilePath = path
return fs.SkipAll
}
return nil
})
if err != nil {
return "", fmt.Errorf("search profile ID %q in %q: %w", profileID, profilesDir, err)
}
return profilePath, nil
}
// ApplyProfile processes profile-only inline targets and secrets after its
// flag values have been merged. It returns the private directory that owns a
// generated inline-secrets file.
func ApplyProfile(profilePath string, options *types.Options) (string, error) {
if profilePath == "" {
return "", nil
}
options.Targets = appendInlineTargets(options.Targets, options.InlineTargetsList)
if strings.Contains(options.TargetsFilePath, "\n") {
options.Targets = appendInlineTargets(options.Targets, options.TargetsFilePath)
options.TargetsFilePath = ""
}
return processInlineSecretsFromProfile(profilePath, options)
}
func appendInlineTargets(targets []string, input string) []string {
for target := range strings.SplitSeq(strings.TrimSpace(input), "\n") {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
targets = append(targets, target)
}
}
return targets
}
type profileSecrets struct {
Secrets any `yaml:"secrets"`
}
func processInlineSecretsFromProfile(profilePath string, options *types.Options) (string, error) {
data, err := os.ReadFile(profilePath)
if err != nil {
return "", fmt.Errorf("read profile file %q: %w", profilePath, err)
}
var profile profileSecrets
if err := yaml.Unmarshal(data, &profile); err != nil {
return "", fmt.Errorf("parse profile YAML %q: %w", profilePath, err)
}
if profile.Secrets == nil {
return "", nil
}
secretsData, err := yaml.Marshal(profile.Secrets)
if err != nil {
return "", fmt.Errorf("marshal inline profile secrets: %w", err)
}
tempDir, err := os.MkdirTemp("", "nuclei-secrets-*")
if err != nil {
return "", fmt.Errorf("create inline secrets directory: %w", err)
}
removeDir := true
defer func() {
if removeDir {
_ = os.RemoveAll(tempDir)
}
}()
tempPath := filepath.Join(tempDir, "inline-secrets.yaml")
if err := os.WriteFile(tempPath, secretsData, 0o600); err != nil {
return "", fmt.Errorf("write inline secrets file %q: %w", tempPath, err)
}
removeDir = false
options.SecretsFile = append(options.SecretsFile, tempPath)
return tempDir, nil
}
@@ -1,254 +1,356 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestProcessInlineSecretsFromProfile(t *testing.T) {
t.Run("profile with inline secrets", func(t *testing.T) {
profileContent := `
secrets:
static:
- type: header
domains:
- api.example.com
headers:
- key: x-api-key
value: test-key-123
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
_ = tmpFile.Close()
opts := &types.Options{}
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
}
defer func() {
_ = os.Remove(tempPath)
}()
if len(opts.SecretsFile) != 1 {
t.Fatalf("expected 1 secrets file, got %d", len(opts.SecretsFile))
}
secretsPath := opts.SecretsFile[0]
if !strings.Contains(secretsPath, "inline-secrets-") {
t.Errorf("secrets file path should contain 'inline-secrets-', got %s", secretsPath)
}
data, err := os.ReadFile(secretsPath)
if err != nil {
t.Fatalf("could not read generated secrets file: %v", err)
}
content := string(data)
if !strings.Contains(content, "x-api-key") {
t.Errorf("secrets file should contain header key, got:\n%s", content)
}
if !strings.Contains(content, "test-key-123") {
t.Errorf("secrets file should contain header value, got:\n%s", content)
}
if !strings.Contains(content, "api.example.com") {
t.Errorf("secrets file should contain domain, got:\n%s", content)
}
})
t.Run("profile without secrets", func(t *testing.T) {
profileContent := `
severity:
- critical
- high
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("could not close temp file: %v", err)
}
opts := &types.Options{}
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile should not fail: %v", err)
}
if tempPath != "" {
t.Errorf("expected empty temp path for profile without secrets, got %s", tempPath)
}
if len(opts.SecretsFile) != 0 {
t.Errorf("expected 0 secrets files for profile without secrets, got %d", len(opts.SecretsFile))
}
})
t.Run("nonexistent profile", func(t *testing.T) {
opts := &types.Options{}
_, err := processInlineSecretsFromProfile(filepath.Join(t.TempDir(), "nonexistent.yaml"), opts)
if err == nil {
t.Error("expected error for nonexistent file")
}
})
}
func TestInlineTargetsParsing(t *testing.T) {
t.Run("multiline list key treated as inline targets", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "example.com\ntest.com\nscanme.sh\n",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
opts.TargetsFilePath = ""
}
if len(opts.Targets) != 3 {
t.Fatalf("expected 3 targets, got %d: %v", len(opts.Targets), opts.Targets)
}
if opts.Targets[0] != "example.com" {
t.Errorf("expected first target 'example.com', got '%s'", opts.Targets[0])
}
if opts.Targets[1] != "test.com" {
t.Errorf("expected second target 'test.com', got '%s'", opts.Targets[1])
}
if opts.Targets[2] != "scanme.sh" {
t.Errorf("expected third target 'scanme.sh', got '%s'", opts.Targets[2])
}
if opts.TargetsFilePath != "" {
t.Errorf("TargetsFilePath should be cleared, got '%s'", opts.TargetsFilePath)
}
})
t.Run("single line list key remains as file path", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "/path/to/targets.txt",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
t.Error("single-line path should not be treated as inline targets")
}
if opts.TargetsFilePath != "/path/to/targets.txt" {
t.Errorf("file path should remain unchanged, got '%s'", opts.TargetsFilePath)
}
})
t.Run("inline targets with comments and blank lines", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "example.com\n# this is a comment\n\ntest.com\n",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
opts.TargetsFilePath = ""
}
if len(opts.Targets) != 2 {
t.Fatalf("expected 2 targets (comments/blanks filtered), got %d: %v", len(opts.Targets), opts.Targets)
}
})
t.Run("targets-inline key", func(t *testing.T) {
opts := &types.Options{
InlineTargetsList: "host1.com\nhost2.com\nhost3.com",
}
if opts.InlineTargetsList != "" {
inlineTargets := strings.Split(strings.TrimSpace(opts.InlineTargetsList), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
}
if len(opts.Targets) != 3 {
t.Fatalf("expected 3 targets from targets-inline, got %d", len(opts.Targets))
}
})
}
func TestInlineSecretsFileFormat(t *testing.T) {
// Verify the generated secrets file has the correct YAML structure
// that matches what authx.GetAuthDataFromYAML expects
profileContent := `
secrets:
static:
- type: header
domains:
- api.example.com
headers:
- key: Authorization
value: Bearer test-token
dynamic:
- template: oauth-flow.yaml
variables:
- key: username
value: testuser
type: cookie
domains:
- auth.example.com
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-secrets-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("could not close temp file: %v", err)
}
opts := &types.Options{}
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
}
defer func() {
_ = os.Remove(tempPath)
}()
data, err := os.ReadFile(opts.SecretsFile[0])
if err != nil {
t.Fatalf("could not read secrets file: %v", err)
}
content := string(data)
// The secrets section should contain static and dynamic at root level,
// matching the Authx struct yaml tags
if !strings.Contains(content, "static:") {
t.Errorf("secrets file should have 'static:' key for Authx compatibility, got:\n%s", content)
}
if !strings.Contains(content, "dynamic:") {
t.Errorf("secrets file should have 'dynamic:' key for Authx compatibility, got:\n%s", content)
}
}
package configuration
import (
"os"
"path/filepath"
"strings"
"testing"
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
func TestProcessInlineSecretsFromProfile(t *testing.T) {
t.Run("profile with inline secrets", func(t *testing.T) {
profileContent := `
secrets:
static:
- type: header
domains:
- api.example.com
headers:
- key: x-api-key
value: test-key-123
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
_ = tmpFile.Close()
opts := &types.Options{}
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
}
defer func() {
_ = os.RemoveAll(tempDir)
}()
if len(opts.SecretsFile) != 1 {
t.Fatalf("expected 1 secrets file, got %d", len(opts.SecretsFile))
}
secretsPath := opts.SecretsFile[0]
if filepath.Base(secretsPath) != "inline-secrets.yaml" {
t.Errorf("secrets file name = %q, want inline-secrets.yaml", filepath.Base(secretsPath))
}
data, err := os.ReadFile(secretsPath)
if err != nil {
t.Fatalf("could not read generated secrets file: %v", err)
}
content := string(data)
if !strings.Contains(content, "x-api-key") {
t.Errorf("secrets file should contain header key, got:\n%s", content)
}
if !strings.Contains(content, "test-key-123") {
t.Errorf("secrets file should contain header value, got:\n%s", content)
}
if !strings.Contains(content, "api.example.com") {
t.Errorf("secrets file should contain domain, got:\n%s", content)
}
})
t.Run("profile without secrets", func(t *testing.T) {
profileContent := `
severity:
- critical
- high
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("could not close temp file: %v", err)
}
opts := &types.Options{}
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile should not fail: %v", err)
}
if tempDir != "" {
t.Errorf("expected no temporary directory for profile without secrets, got %s", tempDir)
}
if len(opts.SecretsFile) != 0 {
t.Errorf("expected 0 secrets files for profile without secrets, got %d", len(opts.SecretsFile))
}
})
t.Run("nonexistent profile", func(t *testing.T) {
opts := &types.Options{}
_, err := processInlineSecretsFromProfile(filepath.Join(t.TempDir(), "nonexistent.yaml"), opts)
if err == nil {
t.Error("expected error for nonexistent file")
}
})
t.Run("does not follow a shared temporary directory symlink", func(t *testing.T) {
tempRoot := t.TempDir()
t.Setenv("TMPDIR", tempRoot)
t.Setenv("TMP", tempRoot)
t.Setenv("TEMP", tempRoot)
victimDir := filepath.Join(tempRoot, "victim")
if err := os.Mkdir(victimDir, 0o755); err != nil {
t.Fatalf("create victim directory: %v", err)
}
before, err := os.Stat(victimDir)
if err != nil {
t.Fatalf("stat victim directory before processing: %v", err)
}
sharedPath := filepath.Join(tempRoot, "nuclei-secrets")
if err := os.Symlink(victimDir, sharedPath); err != nil {
t.Skipf("create directory symlink: %v", err)
}
profilePath := filepath.Join(t.TempDir(), "profile.yaml")
writeConfigFile(t, profilePath, "secrets:\n static: []\n")
opts := &types.Options{}
tempDir, err := processInlineSecretsFromProfile(profilePath, opts)
if err != nil {
t.Fatalf("process inline secrets: %v", err)
}
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
if tempDir == sharedPath {
t.Fatalf("inline secrets used shared path %q", sharedPath)
}
if got := filepath.Dir(opts.SecretsFile[0]); got != tempDir {
t.Fatalf("inline secrets file directory = %q, want %q", got, tempDir)
}
after, err := os.Stat(victimDir)
if err != nil {
t.Fatalf("stat victim directory after processing: %v", err)
}
if before.Mode().Perm() != after.Mode().Perm() {
t.Fatalf("victim directory mode changed from %o to %o", before.Mode().Perm(), after.Mode().Perm())
}
})
}
func TestInlineTargetsParsing(t *testing.T) {
t.Run("multiline list key treated as inline targets", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "example.com\ntest.com\nscanme.sh\n",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
opts.TargetsFilePath = ""
}
if len(opts.Targets) != 3 {
t.Fatalf("expected 3 targets, got %d: %v", len(opts.Targets), opts.Targets)
}
if opts.Targets[0] != "example.com" {
t.Errorf("expected first target 'example.com', got '%s'", opts.Targets[0])
}
if opts.Targets[1] != "test.com" {
t.Errorf("expected second target 'test.com', got '%s'", opts.Targets[1])
}
if opts.Targets[2] != "scanme.sh" {
t.Errorf("expected third target 'scanme.sh', got '%s'", opts.Targets[2])
}
if opts.TargetsFilePath != "" {
t.Errorf("TargetsFilePath should be cleared, got '%s'", opts.TargetsFilePath)
}
})
t.Run("single line list key remains as file path", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "/path/to/targets.txt",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
t.Error("single-line path should not be treated as inline targets")
}
if opts.TargetsFilePath != "/path/to/targets.txt" {
t.Errorf("file path should remain unchanged, got '%s'", opts.TargetsFilePath)
}
})
t.Run("inline targets with comments and blank lines", func(t *testing.T) {
opts := &types.Options{
TargetsFilePath: "example.com\n# this is a comment\n\ntest.com\n",
}
if strings.Contains(opts.TargetsFilePath, "\n") {
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
opts.TargetsFilePath = ""
}
if len(opts.Targets) != 2 {
t.Fatalf("expected 2 targets (comments/blanks filtered), got %d: %v", len(opts.Targets), opts.Targets)
}
})
t.Run("targets-inline key", func(t *testing.T) {
opts := &types.Options{
InlineTargetsList: "host1.com\nhost2.com\nhost3.com",
}
if opts.InlineTargetsList != "" {
inlineTargets := strings.Split(strings.TrimSpace(opts.InlineTargetsList), "\n")
for _, target := range inlineTargets {
target = strings.TrimSpace(target)
if target != "" && !strings.HasPrefix(target, "#") {
opts.Targets = append(opts.Targets, target)
}
}
}
if len(opts.Targets) != 3 {
t.Fatalf("expected 3 targets from targets-inline, got %d", len(opts.Targets))
}
})
}
func TestInlineSecretsFileFormat(t *testing.T) {
// Verify the generated secrets file has the correct YAML structure
// that matches what authx.GetAuthDataFromYAML expects
profileContent := `
secrets:
static:
- type: header
domains:
- api.example.com
headers:
- key: Authorization
value: Bearer test-token
dynamic:
- template: oauth-flow.yaml
variables:
- key: username
value: testuser
type: cookie
domains:
- auth.example.com
`
tmpFile, err := os.CreateTemp(t.TempDir(), "test-secrets-*.yaml")
if err != nil {
t.Fatalf("could not create temp file: %v", err)
}
if _, err := tmpFile.WriteString(profileContent); err != nil {
t.Fatalf("could not write profile: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("could not close temp file: %v", err)
}
opts := &types.Options{}
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
if err != nil {
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
}
defer func() {
_ = os.RemoveAll(tempDir)
}()
data, err := os.ReadFile(opts.SecretsFile[0])
if err != nil {
t.Fatalf("could not read secrets file: %v", err)
}
content := string(data)
// The secrets section should contain static and dynamic at root level,
// matching the Authx struct yaml tags
if !strings.Contains(content, "static:") {
t.Errorf("secrets file should have 'static:' key for Authx compatibility, got:\n%s", content)
}
if !strings.Contains(content, "dynamic:") {
t.Errorf("secrets file should have 'dynamic:' key for Authx compatibility, got:\n%s", content)
}
}
func TestApplyProfileParsesInlineTargets(t *testing.T) {
profilePath := filepath.Join(t.TempDir(), "profile.yaml")
writeConfigFile(t, profilePath, "severity: [high]\n")
options := &types.Options{
InlineTargetsList: "inline.example\n# comment\n",
TargetsFilePath: "list.example\n\nsecond.example\n",
}
tempSecretsDir, err := ApplyProfile(profilePath, options)
if err != nil {
t.Fatalf("apply profile: %v", err)
}
if tempSecretsDir != "" {
t.Fatalf("temporary secrets directory = %q, want empty", tempSecretsDir)
}
want := []string{"inline.example", "list.example", "second.example"}
if strings.Join(options.Targets, ",") != strings.Join(want, ",") {
t.Fatalf("targets = %v, want %v", options.Targets, want)
}
if options.TargetsFilePath != "" {
t.Fatalf("targets file path = %q, want empty", options.TargetsFilePath)
}
}
func TestResolveProfilePathByID(t *testing.T) {
templatesDir := t.TempDir()
profilePath := filepath.Join(templatesDir, "profiles", "nested", "recommended.yaml")
writeConfigFile(t, profilePath, "severity: [high]\n")
got, err := ResolveProfilePath("recommended", templatesDir)
if err != nil {
t.Fatalf("resolve profile ID: %v", err)
}
if got != profilePath {
t.Fatalf("profile path = %q, want %q", got, profilePath)
}
}
func TestTemplatesDirectoryOverridePrecedence(t *testing.T) {
dir := t.TempDir()
envDir := filepath.Join(dir, "environment")
flagDir := filepath.Join(dir, "flag")
t.Setenv(catalogconfig.NucleiTemplatesDirEnv, envDir)
got, overridden := TemplatesDirectoryOverride(&types.Options{NewTemplatesDirectory: flagDir})
if !overridden {
t.Fatal("flag template directory was not reported as an override")
}
want, err := filepath.Abs(flagDir)
if err != nil {
t.Fatalf("resolve expected path: %v", err)
}
if got != want {
t.Fatalf("template directory = %q, want flag path %q", got, want)
}
}
+52
View File
@@ -0,0 +1,52 @@
package runner
import (
"fmt"
"io"
"strings"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
)
type directoryRecord struct {
label string
path string
}
func directoryRecords() []directoryRecord {
return []directoryRecord{
{label: "Nuclei Config Directory", path: config.DefaultConfig.GetConfigDir()},
{label: "Nuclei State Directory", path: config.DefaultConfig.GetStateDir()},
{label: "Nuclei Cache Directory", path: config.DefaultConfig.GetCacheDir()},
{label: "PDCP Directory", path: pdcpauth.PDCPDir},
}
}
// AppendDirectoryInfo writes nuclei config/state/cache/PDCP directory paths.
func AppendDirectoryInfo(w io.Writer) {
for _, record := range directoryRecords() {
_, _ = fmt.Fprintf(w, "%s: %s\n", record.label, record.path)
}
}
// DirectoryInfo returns nuclei config/state/cache/PDCP directory paths as a string.
func DirectoryInfo() string {
var b strings.Builder
AppendDirectoryInfo(&b)
return b.String()
}
// LogDirectoryInfo logs nuclei config/state/cache/PDCP directory paths at info level.
func LogDirectoryInfo(logger *gologger.Logger) {
if logger == nil {
return
}
for _, record := range directoryRecords() {
logger.Info().Msgf("%s: %s", record.label, record.path)
}
}
+54
View File
@@ -0,0 +1,54 @@
package runner
import (
"fmt"
"strings"
"testing"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/formatter"
"github.com/projectdiscovery/gologger/levels"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
)
type directoryLogWriter struct {
records []string
}
func (w *directoryLogWriter) Write(data []byte, _ levels.Level) {
w.records = append(w.records, string(data))
}
func TestDirectoryInfoContainsKnownLabels(t *testing.T) {
info := DirectoryInfo()
cfg := config.DefaultConfig
require.Equal(t, strings.Join([]string{
fmt.Sprintf("Nuclei Config Directory: %s", cfg.GetConfigDir()),
fmt.Sprintf("Nuclei State Directory: %s", cfg.GetStateDir()),
fmt.Sprintf("Nuclei Cache Directory: %s", cfg.GetCacheDir()),
fmt.Sprintf("PDCP Directory: %s", pdcpauth.PDCPDir),
"",
}, "\n"), info)
require.Equal(t, 4, strings.Count(info, "\n"))
}
func TestLogDirectoryInfoContainsKnownLabels(t *testing.T) {
writer := &directoryLogWriter{}
logger := &gologger.Logger{}
logger.SetFormatter(formatter.NewCLI(true))
logger.SetWriter(writer)
logger.SetMaxLevel(levels.LevelInfo)
LogDirectoryInfo(logger)
require.Equal(t, []string{
fmt.Sprintf("[INF] Nuclei Config Directory: %s", config.DefaultConfig.GetConfigDir()),
fmt.Sprintf("[INF] Nuclei State Directory: %s", config.DefaultConfig.GetStateDir()),
fmt.Sprintf("[INF] Nuclei Cache Directory: %s", config.DefaultConfig.GetCacheDir()),
fmt.Sprintf("[INF] PDCP Directory: %s", pdcpauth.PDCPDir),
}, writer.records)
}
+27 -1
View File
@@ -20,19 +20,34 @@ func DoHealthCheck(options *types.Options) string {
fmt.Fprintf(&test, "Architecture: %s\n", runtime.GOARCH)
fmt.Fprintf(&test, "Go Version: %s\n", runtime.Version())
fmt.Fprintf(&test, "Compiler: %s\n", runtime.Compiler)
AppendDirectoryInfo(&test)
var testResult string
cfg := config.DefaultConfig
for _, filename := range []string{cfg.GetFlagsConfigFilePath(), cfg.GetIgnoreFilePath(), cfg.GetChecksumFilePath()} {
if err := cfg.InitializationError(); err != nil {
fmt.Fprintf(&test, "Configuration initialization => Ko (%s)\n", err)
} else {
fmt.Fprintln(&test, "Configuration initialization => Ok")
}
for _, filename := range []string{
cfg.GetFlagsConfigFilePath(),
cfg.GetTemplatesStateFilePath(),
cfg.GetActiveIgnoreFilePath(),
cfg.GetChecksumFilePath(),
} {
ok, err := fileutil.IsReadable(filename)
if ok {
testResult = "Ok"
} else {
testResult = "Ko"
}
if err != nil {
testResult += fmt.Sprintf(" (%s)", err)
}
fmt.Fprintf(&test, "File \"%s\" Read => %s\n", filename, testResult)
ok, err = fileutil.IsWriteable(filename)
if ok {
@@ -40,37 +55,48 @@ func DoHealthCheck(options *types.Options) string {
} else {
testResult = "Ko"
}
if err != nil {
testResult += fmt.Sprintf(" (%s)", err)
}
fmt.Fprintf(&test, "File \"%s\" Write => %s\n", filename, testResult)
}
c4, err := net.Dial("tcp4", "scanme.sh:80")
if err == nil && c4 != nil {
_ = c4.Close()
}
testResult = "Ok"
if err != nil {
testResult = fmt.Sprintf("Ko (%s)", err)
}
fmt.Fprintf(&test, "IPv4 connectivity to scanme.sh:80 => %s\n", testResult)
c6, err := net.Dial("tcp6", "scanme.sh:80")
if err == nil && c6 != nil {
_ = c6.Close()
}
testResult = "Ok"
if err != nil {
testResult = fmt.Sprintf("Ko (%s)", err)
}
fmt.Fprintf(&test, "IPv6 connectivity to scanme.sh:80 => %s\n", testResult)
u4, err := net.Dial("udp4", "scanme.sh:53")
if err == nil && u4 != nil {
_ = u4.Close()
}
testResult = "Ok"
if err != nil {
testResult = fmt.Sprintf("Ko (%s)", err)
}
fmt.Fprintf(&test, "IPv4 UDP connectivity to scanme.sh:53 => %s\n", testResult)
return test.String()
+1 -1
View File
@@ -49,7 +49,7 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
strsl := goflags.StringSlice{}
for _, v := range strings.Split(tt.input, ",") {
for v := range strings.SplitSeq(tt.input, ",") {
//nolint
strsl.Set(v)
}
+2 -3
View File
@@ -543,9 +543,8 @@ func splitPorts(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
var out []string
for p := range strings.SplitSeq(s, ",") {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
+70 -21
View File
@@ -117,16 +117,25 @@ func New(options *types.Options) (*Runner, error) {
Logger: options.Logger,
}
if err := config.DefaultConfig.InitializationError(); err != nil && !options.HealthCheck {
return nil, fmt.Errorf("initialize nuclei configuration: %w", err)
}
if options.HealthCheck {
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
os.Exit(0)
}
if options.Verbose || options.VerboseVerbose {
LogDirectoryInfo(runner.Logger)
}
// Version check by default
if config.DefaultConfig.CanCheckForUpdates() {
if err := installer.NucleiVersionCheck(); err != nil {
latestIgnoreHash, versionCheckErr := installer.NucleiVersionCheck()
if versionCheckErr != nil {
if options.Verbose || options.Debug {
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", err)
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", versionCheckErr)
}
}
@@ -143,15 +152,15 @@ func New(options *types.Options) (*Runner, error) {
DisablePublicTemplates: options.PublicTemplateDisableDownload,
}
if err := tm.FreshInstallIfNotExists(); err != nil {
runner.Logger.Warning().Msgf("failed to install nuclei templates: %s\n", err)
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
}
if err := tm.UpdateIfOutdated(); err != nil {
runner.Logger.Warning().Msgf("failed to update nuclei templates: %s\n", err)
runner.Logger.Warning().Msgf("Failed to update nuclei templates: %s\n", err)
}
if config.DefaultConfig.NeedsIgnoreFileUpdate() {
if config.DefaultConfig.IgnoreFileNeedsUpdate(latestIgnoreHash) {
if err := installer.UpdateIgnoreFile(); err != nil {
runner.Logger.Warning().Msgf("failed to update nuclei ignore file: %s\n", err)
runner.Logger.Warning().Msgf("Failed to update .nuclei-ignore file: %s\n", err)
}
}
@@ -161,6 +170,7 @@ func New(options *types.Options) (*Runner, error) {
if !config.DefaultConfig.NeedsTemplateUpdate() {
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
}
// manually trigger update of custom templates
if ctm != nil {
ctm.Update(context.TODO())
@@ -177,21 +187,25 @@ func New(options *types.Options) (*Runner, error) {
if options.Validate {
parser.ShouldValidate = true
}
// TODO: refactor to pass options reference globally without cycles
parser.NoStrictSyntax = options.NoStrictSyntax
runner.parser = parser
}
yaml.StrictSyntax = !options.NoStrictSyntax
yaml.AllowLocalFileAccess = options.AllowLocalFileAccess
if options.Headless {
if engine.MustDisableSandbox() {
runner.Logger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox\n")
}
browser, err := engine.New(options)
if err != nil {
return nil, err
}
runner.browser = browser
}
@@ -200,6 +214,7 @@ func New(options *types.Options) (*Runner, error) {
var httpclient *retryablehttp.Client
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
var err error
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
if err != nil {
return nil, err
@@ -209,10 +224,12 @@ func New(options *types.Options) (*Runner, error) {
if err := reporting.CreateConfigIfNotExists(); err != nil {
return nil, err
}
reportingOptions, err := createReportingOptions(options)
if err != nil {
return nil, err
}
if reportingOptions != nil && httpclient != nil {
reportingOptions.HttpClient = httpclient
}
@@ -222,6 +239,7 @@ func New(options *types.Options) (*Runner, error) {
if err != nil {
return nil, errors.Wrap(err, "could not create issue reporting client")
}
runner.issuesClient = client
}
@@ -284,10 +302,12 @@ func New(options *types.Options) (*Runner, error) {
if err != nil {
return nil, errors.Wrap(err, "could not create output file")
}
if hpDetector != nil {
outputWriter.SetHoneypotDetector(hpDetector)
runner.honeypotDetector = hpDetector
}
// setup a proxy writer to automatically upload results to PDCP
runner.output = runner.setupPDCPUpload(outputWriter)
if options.HTTPStats {
@@ -298,9 +318,11 @@ func New(options *types.Options) (*Runner, error) {
if options.JSONL && options.EnableProgressBar {
options.StatsJSON = true
}
if options.StatsJSON {
options.EnableProgressBar = true
}
// Creates the progress tracking object
var progressErr error
statsInterval := options.StatsInterval
@@ -326,12 +348,15 @@ func New(options *types.Options) (*Runner, error) {
if err != nil {
return nil, err
}
err = json.Unmarshal(file, &resumeCfg)
if err != nil {
return nil, err
}
resumeCfg.Compile()
}
runner.resumeCfg = resumeCfg
if options.DASTReport || options.DASTServer {
@@ -340,11 +365,13 @@ func New(options *types.Options) (*Runner, error) {
if err != nil {
return nil, errors.Wrap(err, "could not create fuzz stats db")
}
if !options.DASTServer {
dastServer, err := server.NewStatsServer(runner.fuzzStats)
if err != nil {
return nil, errors.Wrap(err, "could not create dast server")
}
runner.dastServer = dastServer
}
}
@@ -354,6 +381,7 @@ func New(options *types.Options) (*Runner, error) {
if request.Error == "none" || request.Error == "" {
return
}
runner.fuzzStats.RecordErrorEvent(fuzzStats.ErrorEvent{
TemplateID: request.Template,
URL: request.Input,
@@ -391,6 +419,7 @@ func New(options *types.Options) (*Runner, error) {
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
}
interactshClient, err := interactsh.New(opts)
if err != nil {
runner.Logger.Error().Msgf("Could not create interactsh client: %s", err)
@@ -403,9 +432,11 @@ func New(options *types.Options) (*Runner, error) {
options.RateLimit = options.RateLimitMinute
options.RateLimitDuration = time.Minute
}
if options.RateLimit > 0 && options.RateLimitDuration == 0 {
options.RateLimitDuration = time.Second
}
// If per-host rate limiting is enabled, make global rate limiter unlimited
if options.PerHostRateLimit {
runner.rateLimiter = utils.GetRateLimiter(context.Background(), 0, 0)
@@ -415,6 +446,7 @@ func New(options *types.Options) (*Runner, error) {
// Initialization successful, disable cleanup on error
cleanupOnError = false
return runner, nil
}
@@ -423,6 +455,7 @@ func (r *Runner) runStandardEnumeration(executerOpts *protocols.ExecutorOptions,
if r.options.AutomaticScan {
return r.executeSmartWorkflowInput(executerOpts, store, engine)
}
return r.executeTemplatesInput(store, engine)
}
@@ -675,8 +708,8 @@ func (r *Runner) RunEnumeration() error {
// If using input-file flags, only load http fuzzing based templates.
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
if !strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST {
// if input type is not list (implicitly enable fuzzing)
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
r.options.DAST = true
}
store, err := loader.New(loaderConfig)
@@ -953,8 +986,10 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
r.Logger.Warning().Msg("No DAST templates found")
}
stats.ForceDisplayWarning(templates.SkippedUnverifiedCodeTemplateStats)
stats.ForceDisplayWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
} else {
stats.DisplayAsWarning(templates.SkippedUnverifiedCodeTemplateStats)
stats.DisplayAsWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
}
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
@@ -964,15 +999,8 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
cfg := config.DefaultConfig
updateutils.Aurora = r.colorizer
versionInfo := func(version, latestVersion, versionType string) string {
if !cfg.CanCheckForUpdates() {
return fmt.Sprintf("Current %s version: %v (%s) - remove '-duc' flag to enable update checks", versionType, version, r.colorizer.BrightYellow("unknown"))
}
return fmt.Sprintf("Current %s version: %v %v", versionType, version, updateutils.GetVersionDescription(version, latestVersion))
}
gologger.Info().Msg(versionInfo(config.Version, cfg.LatestNucleiVersion, "nuclei"))
gologger.Info().Msg(versionInfo(cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion, "nuclei-templates"))
gologger.Info().Msg(versionInfo(cfg, r.colorizer, config.Version, cfg.LatestNucleiVersion, "nuclei"))
gologger.Info().Msg(templateVersionInfo(cfg, r.colorizer))
if !HideAutoSaveMsg {
if r.pdcpUploadErrMsg != "" {
r.Logger.Warning().Msgf("%s", r.pdcpUploadErrMsg)
@@ -1006,19 +1034,40 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
}
}
func versionInfo(cfg *config.Config, colorizer *aurora.Aurora, version, latestVersion, versionType string) string {
if !cfg.CanCheckForUpdates() {
return fmt.Sprintf("Current %s version: %v (%s) - remove '-duc' flag to enable update checks", versionType, version, colorizer.BrightYellow("unknown"))
}
return fmt.Sprintf("Current %s version: %v %v", versionType, version, updateutils.GetVersionDescription(version, latestVersion))
}
func templateVersionInfo(cfg *config.Config, colorizer *aurora.Aurora) string {
return versionInfo(cfg, colorizer, cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion, "nuclei-templates")
}
// SaveResumeConfig to file
func (r *Runner) SaveResumeConfig(path string) error {
dir := filepath.Dir(path)
if !fileutil.FolderExists(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create resume directory %q: %w", dir, err)
}
}
resumeCfgClone := r.resumeCfg.Clone()
resumeCfgClone.ResumeFrom = resumeCfgClone.Current
data, _ := json.MarshalIndent(resumeCfgClone, "", "\t")
return os.WriteFile(path, data, permissionutil.ConfigFilePermission)
data, err := json.MarshalIndent(resumeCfgClone, "", "\t")
if err != nil {
return fmt.Errorf("encode resume configuration: %w", err)
}
if err := os.WriteFile(path, data, permissionutil.ConfigFilePermission); err != nil {
return fmt.Errorf("write resume configuration %q: %w", path, err)
}
return nil
}
// upload existing scan results to cloud with progress
+45
View File
@@ -0,0 +1,45 @@
package runner
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func TestTemplateVersionInfoIgnoresLegacyDiscoverySource(t *testing.T) {
dir := t.TempDir()
root := filepath.Join(dir, "system", config.BinaryName, config.NucleiTemplatesDirName)
cfg := &config.Config{}
cfg.SetStateDir(filepath.Join(dir, "state"))
state, err := json.Marshal(map[string]string{
"nuclei-templates-directory": root,
"nuclei-templates-directory-source": "xdg-system",
"nuclei-templates-version": "v1.2.3",
"nuclei-templates-latest-version": "v99.0.0",
})
if err != nil {
t.Fatalf("marshal templates state: %v", err)
}
if err := os.MkdirAll(filepath.Dir(cfg.GetTemplatesStateFilePath()), 0o700); err != nil {
t.Fatalf("create state directory: %v", err)
}
if err := os.WriteFile(cfg.GetTemplatesStateFilePath(), state, 0o600); err != nil {
t.Fatalf("write templates state: %v", err)
}
if err := cfg.ReadTemplatesConfig(); err != nil {
t.Fatalf("read templates state: %v", err)
}
info := templateVersionInfo(cfg, nil)
if !strings.Contains(info, "v1.2.3") || !strings.Contains(info, "outdated") {
t.Fatalf("template version info = %q, want normal version comparison", info)
}
if strings.Contains(info, "package-managed") {
t.Fatalf("template version info retained discovery ownership: %q", info)
}
}
+2 -2
View File
@@ -117,8 +117,8 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
// If using input-file flags, only load http fuzzing based templates.
loaderConfig := loader.NewConfig(opts.Options, opts.Catalog, executorOpts)
if !strings.EqualFold(opts.Options.InputFileMode, "list") || opts.Options.DAST || opts.Options.DASTServer {
// if input type is not list (implicitly enable fuzzing)
if (!strings.EqualFold(opts.Options.InputFileMode, "list") || opts.Options.DAST || opts.Options.DASTServer) && !opts.Options.OfflineHTTP {
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
opts.Options.DAST = true
}
store, err := loader.New(loaderConfig)
+23 -10
View File
@@ -59,7 +59,12 @@ func runTestMain(m *testing.M) int {
ci: isCI(),
runner: testutils.NewRunner(
testutils.WithWorkingDir(repoRoot),
testutils.WithBaseEnv("NUCLEI_CONFIG_DIR="+configDir),
testutils.WithBaseEnv(
"NUCLEI_CONFIG_DIR="+configDir,
"NUCLEI_TEMPLATES_DIR="+filepath.Join(configDir, "templates"),
"XDG_STATE_HOME="+filepath.Join(configDir, "state"),
"XDG_CACHE_HOME="+filepath.Join(configDir, "cache"),
),
),
}
defer func() {
@@ -447,23 +452,31 @@ func candidateExecutableNames(name string) []string {
}
func prepareFunctionalEnvironment(binaryPath, repoRoot, configDir string) error {
configRoot := filepath.Join(configDir, "nuclei")
if err := os.MkdirAll(configRoot, 0755); err != nil {
return fmt.Errorf("failed to create functional nuclei config root: %w", err)
}
ignoreFile := filepath.Join(configRoot, ".nuclei-ignore")
if err := os.WriteFile(ignoreFile, nil, 0644); err != nil {
return fmt.Errorf("failed to create functional ignore file: %w", err)
}
templatesDir := filepath.Join(configDir, "templates")
for _, args := range [][]string{{"-update-templates"}, {"-validate"}} {
cmd := exec.Command(binaryPath, args...)
cmd.Dir = repoRoot
cmd.Env = append(os.Environ(), "NUCLEI_CONFIG_DIR="+configDir)
cmd.Env = append(os.Environ(),
"NUCLEI_CONFIG_DIR="+configDir,
"NUCLEI_TEMPLATES_DIR="+templatesDir,
"XDG_STATE_HOME="+filepath.Join(configDir, "state"),
"XDG_CACHE_HOME="+filepath.Join(configDir, "cache"),
)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("functional setup command %v failed: %w\n%s", args, err, strings.TrimSpace(string(output)))
}
}
ignoreFile, err := os.ReadFile(filepath.Join(templatesDir, ".nuclei-ignore"))
if err != nil {
return fmt.Errorf("failed to read functional ignore file: %w", err)
}
if err := os.WriteFile(filepath.Join(configDir, ".nuclei-ignore"), ignoreFile, 0o600); err != nil {
return fmt.Errorf("failed to write legacy functional ignore file: %w", err)
}
return nil
}
+4 -1
View File
@@ -24,7 +24,10 @@
{{binary}} -tags cve -exclude-templates http/cves/2020/
{{binary}} -tags cve -exclude-templates http/cves/2020/CVE-2020-9757.yaml
{{binary}} -tags cve -exclude-templates http/cves/2020/CVE-2020-9757.yaml -exclude-templates http/cves/2021/
{{binary}} -tags cve -include-tags dos,fuzz
# Skipped until the next nuclei release ships #7536: release still force-includes
# via -include-tags (bypassing -tags), so release/current load counts diverge by design.
# Covered by pkg/catalog/index filter unit tests in the meantime.
# {{binary}} -tags cve -include-tags dos,fuzz
{{binary}} -tags cve -ntv 8.8.8,8.8.9
{{binary}} -tags cve -severity high
{{binary}} -tags cve,dos,fuzz
+32 -17
View File
@@ -17,8 +17,8 @@ import (
)
var isCodeDisabled = func() bool { return osutils.IsWindows() && os.Getenv("CI") == "true" }
var signedCodeTemplates sync.Once
var signedCodeTemplatesErr error
var signedIntegrationTemplates sync.Once
var signedIntegrationTemplatesErr error
func hasAnyExecutable(names ...string) bool {
for _, name := range names {
@@ -51,23 +51,20 @@ const (
var testcertpath = ""
func ensureSignedCodeTemplates() error {
if isCodeDisabled() {
return nil
}
signedCodeTemplates.Do(func() {
func ensureSignedIntegrationTemplates() error {
signedIntegrationTemplates.Do(func() {
previousWD, err := os.Getwd()
if err != nil {
signedCodeTemplatesErr = err
signedIntegrationTemplatesErr = err
return
}
if err := os.Chdir(suite.fixturesDir); err != nil {
signedCodeTemplatesErr = err
signedIntegrationTemplatesErr = err
return
}
defer func() {
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedCodeTemplatesErr == nil {
signedCodeTemplatesErr = chdirErr
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedIntegrationTemplatesErr == nil {
signedIntegrationTemplatesErr = chdirErr
}
}()
@@ -79,13 +76,16 @@ func ensureSignedCodeTemplates() error {
tsigner, err := signer.NewTemplateSignerFromFiles(certPath, keyPath)
if err != nil {
signedCodeTemplatesErr = err
signedIntegrationTemplatesErr = err
return
}
templatesToSign := []string{
"workflow/code-template-1.yaml",
"workflow/code-template-2.yaml",
var templatesToSign []string
if !isCodeDisabled() {
templatesToSign = append(templatesToSign,
"workflow/code-template-1.yaml",
"workflow/code-template-2.yaml",
)
}
for _, v := range codeTestCases {
if v.DisableOn != nil && v.DisableOn() {
@@ -102,14 +102,20 @@ func ensureSignedCodeTemplates() error {
}
templatesToSign = append(templatesToSign, v.Path)
}
for _, v := range jsTestcases {
if v.DisableOn != nil && v.DisableOn() {
continue
}
templatesToSign = append(templatesToSign, v.Path)
}
for _, templatePath := range templatesToSign {
if err := templates.SignTemplate(tsigner, fixturePath(templatePath)); err != nil {
signedCodeTemplatesErr = err
signedIntegrationTemplatesErr = err
return
}
}
})
return signedCodeTemplatesErr
return signedIntegrationTemplatesErr
}
func getEnvValues() []string {
@@ -118,6 +124,15 @@ func getEnvValues() []string {
}
}
func runSignedNucleiTemplateAndGetResults(template, url string, debug bool, extra ...string) ([]string, error) {
if err := ensureSignedIntegrationTemplates(); err != nil {
return nil, err
}
args := []string{"-t", template, "-target", url}
args = append(args, extra...)
return testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), args...)
}
type codeSnippet struct{}
// Execute executes a test case and returns an error if occurred
@@ -119,7 +119,9 @@ func TestTemplatesDirEnv(t *testing.T) {
func TestCustomConfigDir(t *testing.T) {
t.Run("ConfigDirIsolated", func(t *testing.T) {
customTempDir := t.TempDir()
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, []string{"NUCLEI_CONFIG_DIR=" + customTempDir}, "-t", "protocols/http/get.yaml", "-u", newTemplateDirTarget(t))
stateHome := t.TempDir()
templatesDir := t.TempDir()
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, []string{"NUCLEI_CONFIG_DIR=" + customTempDir, "XDG_STATE_HOME=" + stateHome, "NUCLEI_TEMPLATES_DIR=" + templatesDir}, "-t", "protocols/http/get.yaml", "-u", newTemplateDirTarget(t))
if err != nil {
t.Fatalf("custom config dir request failed: %v", err)
}
@@ -132,11 +134,14 @@ func TestCustomConfigDir(t *testing.T) {
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
for _, requiredFile := range []string{".templates-config.json", "config.yaml", "reporting-config.yaml"} {
for _, requiredFile := range []string{"config.yaml", "reporting-config.yaml"} {
if !slices.Contains(fileNames, requiredFile) {
t.Fatalf("missing required config file %q in custom config dir: %v", requiredFile, fileNames)
}
}
if _, err := os.Stat(filepath.Join(stateHome, "nuclei", "templates.json")); err != nil {
t.Fatalf("missing templates state file: %v", err)
}
}
})
}
+10
View File
@@ -24,6 +24,16 @@ func TestDNS(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/dns/duration.yaml", "one.one.one.one", suite.debug)
if err != nil {
t.Fatalf("dns duration request failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("AAAA", func(t *testing.T) {
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/dns/aaaa.yaml", "one.one.one.one", suite.debug)
if err != nil {
@@ -0,0 +1,9 @@
//go:build integration
// +build integration
package integration_test
import "time"
// Keep duration assertions above the timer granularity of fast local sockets on Windows.
const integrationDurationObservationDelay = 10 * time.Millisecond
+53
View File
@@ -5,9 +5,13 @@ package integration_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
@@ -36,6 +40,55 @@ var fuzzingTestCases = []integrationCase{
{Path: "fuzz/fuzz-body-params-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
{Path: "fuzz/fuzz-body-xml-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
{Path: "fuzz/fuzz-body-generic-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 4}},
{Path: "fuzz/fuzz-raw-http-input.yaml", TestCase: &rawHttpInputFuzz{}},
}
// rawHttpInputFuzz seeds the base request from a raw HTTP request file, the
// shape users have on hand when there is no spec or captured traffic to feed in.
type rawHttpInputFuzz struct{}
func (h *rawHttpInputFuzz) Execute(filePath string) error {
router := httprouter.New()
router.POST("/api/login", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html")
var payload map[string]string
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// stand in for a backend that interpolates the value into a query
if strings.Contains(payload["user"], "'") {
_, _ = fmt.Fprint(w, "unrecognized token:")
return
}
_, _ = fmt.Fprintf(w, "welcome %s", payload["user"])
})
ts := httptest.NewServer(router)
defer ts.Close()
target, err := url.Parse(ts.URL)
if err != nil {
return err
}
rawRequest := fmt.Sprintf("POST /api/login HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\n\r\n{\"user\":\"admin\"}", target.Host)
targetPath := filepath.Join(os.TempDir(), "nuclei-raw-http-input.http")
if err := os.WriteFile(targetPath, []byte(rawRequest), 0644); err != nil {
return err
}
defer func() {
_ = os.Remove(targetPath)
}()
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", targetPath, "-im", "http", "-dast")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type genericFuzzTestCase struct {
@@ -16,6 +16,7 @@ import (
var headlessTestcases = []integrationCase{
{Path: "protocols/headless/headless-basic.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-duration.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-waitevent.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-dsl.yaml", TestCase: &headlessBasic{}},
{Path: "protocols/headless/headless-self-contained.yaml", TestCase: &headlessSelfContained{}},
+52 -5
View File
@@ -43,6 +43,7 @@ var httpTestcases = []integrationCase{
{Path: "protocols/http/post-json-body.yaml", TestCase: &httpPostJSONBody{}},
{Path: "protocols/http/post-multipart-body.yaml", TestCase: &httpPostMultipartBody{}},
{Path: "protocols/http/raw-cookie-reuse.yaml", TestCase: &httpRawCookieReuse{}},
{Path: "protocols/http/disable-cookie-reuse.yaml", TestCase: &httpDisableCookieReuse{}},
{Path: "protocols/http/raw-dynamic-extractor.yaml", TestCase: &httpRawDynamicExtractor{}},
{Path: "protocols/http/raw-get-query.yaml", TestCase: &httpRawGetQuery{}},
{Path: "protocols/http/raw-get.yaml", TestCase: &httpRawGet{}},
@@ -75,6 +76,7 @@ var httpTestcases = []integrationCase{
{Path: "protocols/http/variable-dsl-function.yaml", TestCase: &httpVariableDSLFunction{}},
{Path: "protocols/http/get-override-sni.yaml", TestCase: &httpSniAnnotation{}},
{Path: "protocols/http/get-sni.yaml", TestCase: &customCLISNI{}},
{Path: "protocols/http/tls-metadata.yaml", TestCase: &httpTLSMetadata{}},
{Path: "protocols/http/redirect-match-url.yaml", TestCase: &httpRedirectMatchURL{}},
{Path: "protocols/http/get-sni-unsafe.yaml", TestCase: &customCLISNIUnsafe{}},
{Path: "protocols/http/annotation-timeout.yaml", TestCase: &annotationTimeout{}},
@@ -829,7 +831,7 @@ func (h *httpRawUnsafePath) Execute(filepath string) error {
}
actual := []string{}
for _, v := range strings.Split(results, "\n") {
for v := range strings.SplitSeq(results, "\n") {
if strings.Contains(v, "GET") {
parts := strings.Fields(v)
if len(parts) == 3 {
@@ -877,7 +879,7 @@ func (h *httpPaths) Execute(filepath string) error {
}
actual := []string{}
for _, v := range strings.Split(results, "\n") {
for v := range strings.SplitSeq(results, "\n") {
if strings.Contains(v, "GET") {
parts := strings.Fields(v)
if len(parts) == 3 {
@@ -901,6 +903,29 @@ func (h *httpPaths) Execute(filepath string) error {
type httpRawCookieReuse struct{}
type httpDisableCookieReuse struct{}
func (h *httpDisableCookieReuse) Execute(filePath string) error {
router := httprouter.New()
router.GET("/login", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
http.SetCookie(w, &http.Cookie{Name: "nuclei", Value: "test"})
_, _ = fmt.Fprint(w, "logged in")
})
router.GET("/anonymous", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if _, err := r.Cookie("nuclei"); errors.Is(err, http.ErrNoCookie) {
_, _ = fmt.Fprint(w, "anonymous")
}
})
ts := httptest.NewServer(router)
defer ts.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
// Execute executes a test case and returns an error if occurred
func (h *httpRawCookieReuse) Execute(filePath string) error {
router := httprouter.New()
@@ -1394,7 +1419,7 @@ func (h *httpVariableDSLFunction) Execute(filePath string) error {
}
actual := []string{}
for _, v := range strings.Split(results, "\n") {
for v := range strings.SplitSeq(results, "\n") {
if strings.Contains(v, "GET") {
parts := strings.Fields(v)
if len(parts) == 3 {
@@ -1431,6 +1456,28 @@ func (h *customCLISNI) Execute(filePath string) error {
return expectResultsCount(results, 1)
}
type httpTLSMetadata struct{}
// Execute verifies TLS handshake metadata from the same HTTP connection is matchable.
func (h *httpTLSMetadata) Execute(filePath string) error {
router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if r.TLS != nil && r.TLS.ServerName == "test" {
_, _ = w.Write([]byte("test-ok"))
return
}
_, _ = w.Write([]byte("test-ko"))
})
ts := httptest.NewTLSServer(router)
defer ts.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-sni", "test")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type httpSniAnnotation struct{}
// Execute executes a test case and returns an error if occurred
@@ -1763,7 +1810,7 @@ func (h *httpRawPathSingleSlash) Execute(filepath string) error {
}
var actual string
for _, v := range strings.Split(results, "\n") {
for v := range strings.SplitSeq(results, "\n") {
if strings.Contains(v, "GET") {
parts := strings.Fields(v)
if len(parts) == 3 {
@@ -1788,7 +1835,7 @@ func (h *httpRawUnsafePathSingleSlash) Execute(filepath string) error {
}
var actual string
for _, v := range strings.Split(results, "\n") {
for v := range strings.SplitSeq(results, "\n") {
if strings.Contains(v, "GET") {
parts := strings.Fields(v)
if len(parts) == 3 {
@@ -18,6 +18,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/internal/fuzzplayground"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
type integrationHarness struct {
@@ -63,12 +64,26 @@ func TestMain(m *testing.M) {
_ = os.RemoveAll(tempDir)
os.Exit(1)
}
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to determine user home directory: %v\n", err)
_ = os.RemoveAll(tempDir)
os.Exit(1)
}
templatesDir := filepath.Join(homeDir, config.NucleiTemplatesDirName)
config.DefaultConfig.SetTemplatesDir(templatesDir)
previousRunner := testutils.DefaultRunner()
runner := testutils.NewRunner(
testutils.WithBinaryPath(binaryPath),
testutils.WithWorkingDir(workingFixturesDir),
testutils.WithExtraArgs(integrationExtraArgs...),
testutils.WithBaseEnv(
"NUCLEI_TEMPLATES_DIR="+templatesDir,
"XDG_CONFIG_HOME="+filepath.Join(tempDir, "config"),
"XDG_STATE_HOME="+filepath.Join(tempDir, "state"),
"XDG_CACHE_HOME="+filepath.Join(tempDir, "cache"),
),
)
testutils.SetDefaultRunner(runner)
@@ -0,0 +1,61 @@
//go:build integration
// +build integration
package integration_test
import (
"fmt"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
)
// startGRPCHealthServer starts an in-process gRPC server exposing the standard
// health service (status SERVING) and server reflection on a random loopback
// port. It returns the "host:port" address and a stop func.
func startGRPCHealthServer() (string, func(), error) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", nil, fmt.Errorf("grpc health listen: %w", err)
}
srv := grpc.NewServer()
hs := health.NewServer()
hs.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthpb.RegisterHealthServer(srv, hs)
reflection.Register(srv)
go func() { _ = srv.Serve(lis) }()
return lis.Addr().String(), srv.Stop, nil
}
// javascriptGRPCHealth exercises the nuclei/grpc library end to end against a
// real gRPC server via server reflection.
type javascriptGRPCHealth struct{}
func (j *javascriptGRPCHealth) Execute(filePath string) error {
address, stop, err := startGRPCHealthServer()
if err != nil {
return err
}
defer stop()
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
// javascriptGRPCDenied asserts the nuclei/grpc library refuses to dial a host
// on the exclude list (network-policy enforcement) before connecting.
type javascriptGRPCDenied struct{}
func (j *javascriptGRPCDenied) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
@@ -0,0 +1,86 @@
//go:build integration
// +build integration
package integration_test
import (
"io"
"net/http"
"net/http/httptest"
"strings"
)
// startJSHTTPServer starts a local HTTP server exercising the generic paths
// used by the nuclei/http javascript integration templates.
func startJSHTTPServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test", "ok")
_, _ = io.WriteString(w, "hello")
})
mux.HandleFunc("/echo-header", func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, r.Header.Get("X-Token"))
})
mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
_, _ = w.Write(b)
})
mux.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "sid", Value: "abc"})
_, _ = io.WriteString(w, "set")
})
mux.HandleFunc("/echo-cookie", func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("sid")
if err != nil || c.Value != "abc" {
_, _ = io.WriteString(w, "missing")
return
}
_, _ = io.WriteString(w, "sid-ok")
})
mux.HandleFunc("/redir", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/land", http.StatusFound)
})
mux.HandleFunc("/land", func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, "landed")
})
return httptest.NewServer(mux)
}
type javascriptHTTPGet struct{}
func (j *javascriptHTTPGet) Execute(filePath string) error {
ts := startJSHTTPServer()
defer ts.Close()
// Target as host:port so javascript Host/Port args populate cleanly.
hostPort := strings.TrimPrefix(ts.URL, "http://")
results, err := runSignedNucleiTemplateAndGetResults(filePath, hostPort, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptHTTPClientFlow struct{}
func (j *javascriptHTTPClientFlow) Execute(filePath string) error {
ts := startJSHTTPServer()
defer ts.Close()
hostPort := strings.TrimPrefix(ts.URL, "http://")
results, err := runSignedNucleiTemplateAndGetResults(filePath, hostPort, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
type javascriptHTTPDenied struct{}
func (j *javascriptHTTPDenied) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
@@ -19,8 +19,6 @@ import (
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
)
// javascriptASRepRoast exercises templates/ad/asrep-roast.yaml end-to-end
@@ -46,7 +44,7 @@ func (j *javascriptASRepRoast) Execute(filePath string) error {
}
defer kdc.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
if err != nil {
return err
}
@@ -0,0 +1,66 @@
//go:build integration
// +build integration
package integration_test
import (
"encoding/binary"
"io"
"net"
"time"
)
type javascriptMSSQLFingerprint struct{}
func (j *javascriptMSSQLFingerprint) Execute(filePath string) error {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return err
}
defer func() { _ = ln.Close() }()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handleMSSQLPreloginConn(conn)
}
}()
results, err := runSignedNucleiTemplateAndGetResults(filePath, ln.Addr().String(), debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
func handleMSSQLPreloginConn(conn net.Conn) {
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
buf := make([]byte, 256)
if _, err := io.ReadAtLeast(conn, buf, 8); err != nil {
return
}
_, _ = conn.Write(sampleMSSQLPreloginResponse())
}
func sampleMSSQLPreloginResponse() []byte {
body := []byte{
0x00, 0x00, 0x1f, 0x00, 0x06,
0x01, 0x00, 0x25, 0x00, 0x01,
0x02, 0x00, 0x26, 0x00, 0x01,
0x03, 0x00, 0x27, 0x00, 0x00,
0x04, 0x00, 0x27, 0x00, 0x01,
0x05, 0x00, 0x28, 0x00, 0x00,
0xff,
0x0f, 0x00, 0x07, 0xd0, 0x00, 0x00,
0x02,
0x00,
0x01,
}
header := []byte{0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}
binary.BigEndian.PutUint16(header[2:4], uint16(8+len(body)))
return append(header, body...)
}
+78 -9
View File
@@ -31,13 +31,21 @@ func javascriptGoExecSambaDisabled() bool {
var jsTestcases = []integrationCase{
{Path: "protocols/javascript/redis-pass-brute.yaml", TestCase: &javascriptRedisPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/redis-lua-script.yaml", TestCase: &javascriptRedisLuaScript{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/mysql-connect.yaml", TestCase: &javascriptMySQLConnect{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/mysql-fingerprint.yaml", TestCase: &javascriptMySQLFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/mssql-fingerprint.yaml", TestCase: &javascriptMSSQLFingerprint{}},
{Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}},
{Path: "protocols/javascript/no-port-args.yaml", TestCase: &javascriptNoPortArgs{}},
{Path: "protocols/javascript/telnet-auth-test.yaml", TestCase: &javascriptTelnetAuthTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
@@ -94,7 +102,7 @@ func newJavascriptDockerSpec(port string, options *dockertest.RunOptions, readyT
type javascriptNetHttps struct{}
func (j *javascriptNetHttps) Execute(filePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
@@ -111,6 +119,16 @@ func (j *javascriptRedisPassBrute) Execute(filePath string) error {
}, javascriptServiceReadyTimeout, 0, nil))
}
type javascriptRedisLuaScript struct{}
func (j *javascriptRedisLuaScript) Execute(filePath string) error {
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("6379/tcp", &dockertest.RunOptions{
Repository: "redis",
Tag: "latest",
Cmd: []string{"redis-server", "--requirepass", "iamadmin"},
}, javascriptServiceReadyTimeout, 0, nil))
}
type javascriptSSHServerFingerprint struct{}
func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
@@ -163,11 +181,62 @@ func (j *javascriptMySQLConnect) Execute(filePath string) error {
}, javascriptDatabaseReadyTimeout, 0, mysqlReadyCheck("root", "secret")))
}
type javascriptMySQLFingerprint struct{}
// Execute fingerprints multiple MySQL/MariaDB server versions and asserts the
// extended handshake fields (protocol, version, salt, capabilities, auth plugin).
func (j *javascriptMySQLFingerprint) Execute(filePath string) error {
cases := []struct {
name string
repository string
tag string
env []string
}{
{
name: "mysql-5.7",
repository: "mysql",
tag: "5.7",
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
},
{
name: "mysql-8.0",
repository: "mysql",
tag: "8.0",
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
},
{
name: "mysql-8.4",
repository: "mysql",
tag: "8.4",
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
},
{
name: "mariadb-11.4",
repository: "mariadb",
tag: "11.4",
env: []string{"MARIADB_ROOT_PASSWORD=secret"},
},
}
var errs []error
for _, tc := range cases {
err := runJavascriptDockerCase(filePath, newJavascriptDockerSpec("3306/tcp", &dockertest.RunOptions{
Repository: tc.repository,
Tag: tc.tag,
Env: tc.env,
}, javascriptDatabaseReadyTimeout, 0, mysqlReadyCheck("root", "secret")))
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w", tc.name, err))
}
}
return multierr.Combine(errs...)
}
type javascriptMultiPortsSSH struct{}
func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
// use scanme.sh as target to ensure we match on the 2nd default port 22
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
@@ -177,7 +246,7 @@ func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
type javascriptNoPortArgs struct{}
func (j *javascriptNoPortArgs) Execute(filePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
if err != nil {
return err
}
@@ -192,7 +261,7 @@ func (j *javascriptWMICommand) Execute(filePath string) error {
// the exclude list and short-circuits before any dial, so the JSON result
// must contain "ok":false plus "network policy" in the error while not
// leaking the password.
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
@@ -204,7 +273,7 @@ type javascriptGoExecRedaction struct{}
func (j *javascriptGoExecRedaction) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
@@ -216,7 +285,7 @@ type javascriptGoExecModules struct{}
func (j *javascriptGoExecModules) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
@@ -275,7 +344,7 @@ func (j *javascriptGoExecSambaNTLM) Execute(filePath string) error {
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
if err == nil {
if countErr := expectResultsCount(results, 1); countErr == nil {
return nil
@@ -383,7 +452,7 @@ func (j *networkMultiStep) Execute(filePath string) error {
})
defer server.Close()
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, server.URL, debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, server.URL, debug)
if err != nil {
return err
}
@@ -439,7 +508,7 @@ func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expecte
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, targetAddress, debug)
results, err := runSignedNucleiTemplateAndGetResults(filePath, targetAddress, debug)
if err == nil {
if countErr := expectResultsCount(results, expectedNumbers...); countErr == nil {
return nil
@@ -79,7 +79,7 @@ type javascriptNoAccess struct{}
// Execute executes a test case and returns an error if occurred
func (h *javascriptNoAccess) Execute(filePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
results, err := runSignedNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
if err != nil {
return err
}
@@ -116,6 +116,46 @@ func TestNetwork(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
errState := &capturedError{}
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 5, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "FIRST" {
time.Sleep(integrationDurationObservationDelay)
_, _ = conn.Write([]byte("PING"))
}
data, err = reader.ConnReadNWithTimeout(conn, 6, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "SECOND" {
time.Sleep(integrationDurationObservationDelay)
_, _ = conn.Write([]byte("PONG"))
}
_, _ = conn.Write([]byte("NUCLEI"))
})
defer server.Close()
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/network/duration.yaml", server.URL, suite.debug)
if err != nil {
t.Fatalf("duration network request failed: %v", err)
}
if err := errState.Err(); err != nil {
t.Fatal(err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("SelfContained", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
+8 -6
View File
@@ -106,7 +106,7 @@ func integrationFamilies() []integrationFamily {
{Name: "headless", Cases: headlessTestcases},
{Name: "whois", Cases: whoisTestCases},
{Name: "library", Cases: libraryTestcases},
{Name: "template-path", Cases: templatesPathTestCases},
{Name: "template-path", Cases: templatesPathTestCases()},
{Name: "offline-http", Cases: offlineHttpTestcases},
{Name: "fuzz", Cases: fuzzingTestCases},
{Name: "generic", Cases: genericTestcases},
@@ -159,9 +159,9 @@ func executeIntegrationCase(testCase integrationCase) error {
if testCase.DisableOn != nil && testCase.DisableOn() {
return nil
}
if needsSignedCodeTemplates(testCase.Path) {
if err := ensureSignedCodeTemplates(); err != nil {
return fmt.Errorf("failed to sign code templates: %w", err)
if needsSignedIntegrationTemplates(testCase.Path) {
if err := ensureSignedIntegrationTemplates(); err != nil {
return fmt.Errorf("failed to sign integration templates: %w", err)
}
}
@@ -191,8 +191,10 @@ func parallelism() int {
return parallelism
}
func needsSignedCodeTemplates(path string) bool {
return strings.HasPrefix(path, "protocols/code/") || strings.Contains(path, "workflow/code-")
func needsSignedIntegrationTemplates(path string) bool {
return strings.HasPrefix(path, "protocols/code/") ||
strings.Contains(path, "workflow/code-") ||
strings.HasPrefix(path, "protocols/javascript/")
}
func testNameForPath(path string) string {
+11
View File
@@ -23,6 +23,17 @@ func TestSSL(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
server := newTLSServer(t, &tls.Config{})
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/ssl/duration.yaml", server.URL, suite.debug)
if err != nil {
t.Fatalf("duration ssl request failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("BasicZTLS", func(t *testing.T) {
server := newTLSServer(t, &tls.Config{})
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/ssl/basic-ztls.yaml", server.URL, suite.debug, "-ztls")
@@ -5,25 +5,24 @@ package integration_test
import (
"fmt"
"path/filepath"
"strings"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func getTemplatePath() string {
return config.DefaultConfig.TemplatesDirectory
}
var templatesPathTestCases = []integrationCase{
//template folder path issue
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
//cwd
{Path: "./dns/detect-dangling-cname.yaml", TestCase: &cwdTemplateTest{}},
//relative path
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
//absolute path
{Path: fmt.Sprintf("%v/dns/dns-saas-service-detection.yaml", getTemplatePath()), TestCase: &absolutePathTemplateTest{}},
func templatesPathTestCases() []integrationCase {
return []integrationCase{
//template folder path issue
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
//cwd
{Path: "./dns/detect-dangling-cname.yaml", TestCase: &cwdTemplateTest{}},
//relative path
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
//absolute path
{Path: filepath.Join(config.DefaultConfig.TemplatesDirectory, "dns", "dns-saas-service-detection.yaml"), TestCase: &absolutePathTemplateTest{}},
}
}
type cwdTemplateTest struct{}
@@ -0,0 +1,34 @@
id: raw-http-input-body-fuzz
info:
name: fuzzing json body seeded from a raw http request file
author: pdteam
severity: info
description: |
This template fuzzes the json body of a base request seeded from a raw HTTP
request file (-im http), covering the shortest path from a single request to
a DAST scan.
http:
- pre-condition:
- type: dsl
dsl:
- method == "POST"
- contains(content_type, "application/json")
condition: and
payloads:
injection:
- "'"
fuzzing:
- part: body
type: postfix
mode: single
fuzz:
- '{{injection}}'
matchers:
- type: word
words:
- "unrecognized token:"
@@ -0,0 +1,23 @@
id: dns-duration
info:
name: DNS Duration
author: pdteam
severity: info
flow: dns("duration_dns")
dns:
- id: duration_dns
name: "{{FQDN}}"
type: A
class: inet
recursion: true
retries: 3
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_dns_duration > 0 && duration_dns_duration < 60"
- "duration == duration_dns_duration"
@@ -0,0 +1,35 @@
id: headless-duration
info:
name: Headless Duration
author: pdteam
severity: info
tags: headless
flow: headless("duration_headless")
headless:
- id: duration_headless
steps:
- action: navigate
args:
url: "{{BaseURL}}/"
- action: waitload
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_1 > 0 && duration_1 < 60"
- "duration_2 > 0 && duration_2 < 60"
- "duration == duration_2"
- "duration_headless_duration > 0 && duration_headless_duration < 60"
- "duration_headless_duration_1 > 0 && duration_headless_duration_1 < 60"
- "duration_headless_duration_2 > 0 && duration_headless_duration_2 < 60"
- "duration_headless_duration == duration"
- "duration_headless_duration_1 == duration_1"
- "duration_headless_duration_2 == duration_2"
- type: word
words:
- "<html>"
matchers-condition: and
@@ -0,0 +1,27 @@
id: disable-cookie-reuse
info:
name: Disable Cookie Reuse
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/login"
matchers:
- type: word
words:
- "logged in"
internal: true
- method: GET
path:
- "{{BaseURL}}/anonymous"
disable-cookie: true
matchers:
- type: word
words:
- "anonymous"
@@ -0,0 +1,22 @@
id: http-tls-metadata
info:
name: HTTP TLS metadata from same connection
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
matchers-condition: and
matchers:
- type: word
words:
- "test-ok"
- type: dsl
dsl:
- sni == "test"
- tls_version != ""
- cipher != ""
- len(to_string(domains)) > 0 || subject_cn != "" || len(to_string(subject_an)) > 0
@@ -0,0 +1,37 @@
id: grpc-network-policy-denied
info:
name: gRPC Network Policy Denial
author: pdteam
severity: info
description: |
Hardening check for the nuclei/grpc javascript library: dialing a host on
the exclude list (RFC 5737 TEST-NET-3) must be rejected by the network
policy before any connection is made. The error is captured and asserted.
javascript:
- code: |
const grpc = require('nuclei/grpc');
let out = "";
try {
const opts = new grpc.Options();
opts.Plaintext = true;
const client = new grpc.Client("203.0.113.10:50051", opts);
client.Invoke('grpc.health.v1.Health/Check', '{}');
} catch (e) {
out = "" + e;
}
out;
args:
Host: "{{Host}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "network policy"
- "203.0.113.10"
condition: and
@@ -0,0 +1,31 @@
id: grpc-health-check
info:
name: gRPC Health Check
author: pdteam
severity: info
description: |
Exercises the nuclei/grpc javascript library end to end: dial through the
network policy, resolve the schema via server reflection and invoke the
standard health check, expecting a SERVING response.
javascript:
- code: |
const grpc = require('nuclei/grpc');
const opts = new grpc.Options();
opts.Plaintext = true;
const client = new grpc.Client(address, opts);
const resp = client.Invoke('grpc.health.v1.Health/Check', '{"service":""}');
client.Close();
resp;
args:
address: "{{Host}}:{{Port}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "SERVING"
@@ -0,0 +1,30 @@
id: js-http-client-flow
info:
name: nuclei/http client flow
author: pdteam
severity: info
description: |
Exercises nuclei/http Client for Post, cookie jar reuse, and redirect
following against a local HTTP server.
javascript:
- code: |
const http = require('nuclei/http');
const client = new http.Client();
const post = client.Post(BaseURL + '/echo', 'payload');
client.Get(BaseURL + '/set-cookie');
const cookie = client.Get(BaseURL + '/echo-cookie');
const redir = client.Get(BaseURL + '/redir');
post.Body + '|' + cookie.Body + '|' + redir.StatusCode + '|' + redir.Body;
args:
BaseURL: "http://{{Host}}:{{Port}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "payload|sid-ok|200|landed"
@@ -0,0 +1,33 @@
id: js-http-network-policy-denied
info:
name: nuclei/http network policy denial
author: pdteam
severity: info
description: |
Hardening check for nuclei/http: requesting a host on the exclude list
(RFC 5737 TEST-NET-3) must be rejected by network policy before dialing.
javascript:
- code: |
const http = require('nuclei/http');
let out = "";
try {
http.Get('http://203.0.113.10/');
} catch (e) {
out = "" + e;
}
out;
args:
Host: "{{Host}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "network policy"
- "203.0.113.10"
condition: and
@@ -0,0 +1,29 @@
id: js-http-get
info:
name: nuclei/http GET
author: pdteam
severity: info
description: |
Exercises nuclei/http package Get and Client.Get/SetHeader against a local
HTTP server (status, body, response headers, default request header).
javascript:
- code: |
const http = require('nuclei/http');
const a = http.Get(BaseURL + '/hello');
const client = new http.Client();
client.SetHeader('X-Token', 'nuclei');
const b = client.Get(BaseURL + '/echo-header');
a.StatusCode + '|' + a.Body + '|' + a.GetHeader('X-Test') + '|' + b.Body;
args:
BaseURL: "http://{{Host}}:{{Port}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "200|hello|ok|nuclei"
@@ -0,0 +1,42 @@
id: mssql-fingerprint
info:
name: MSSQL Fingerprint Test
author: pdteam
severity: info
javascript:
- pre-condition: |
isPortOpen(Host, Port)
code: |
const mssql = require('nuclei/mssql');
const client = new mssql.MSSQLClient();
const isMssql = client.IsMssql(Host, Port);
const info = client.FingerprintMssql(Host, Port);
const ok = isMssql && info && info.Version && info.Version.length > 0 && info.MajorVersion > 0 && info.EncryptionMode && info.EncryptionMode.length > 0 && info.Protocol === "mssql";
Export({
ok: ok,
isMssql: isMssql,
version: info.Version,
majorVersion: info.MajorVersion,
minorVersion: info.MinorVersion,
buildNumber: info.BuildNumber,
encryptionMode: info.EncryptionMode,
protocol: info.Protocol,
transport: info.Transport,
});
args:
Host: "{{Host}}"
Port: "1433"
matchers:
- type: dsl
dsl:
- "success == true"
- "ok == true"
- "isMssql == true"
- "len(version) > 0"
- "majorVersion > 0"
- 'protocol == "mssql"'
- 'transport == "tcp"'
- "len(encryptionMode) > 0"
@@ -0,0 +1,45 @@
id: mysql-fingerprint
info:
name: MySQL Fingerprint Test
author: pdteam
severity: info
javascript:
- pre-condition: |
isPortOpen(Host, Port)
code: |
const mysql = require('nuclei/mysql');
const client = new mysql.MySQLClient();
const isMySQL = client.IsMySQL(Host, Port);
const info = client.FingerprintMySQL(Host, Port);
const ok = isMySQL && info && info.ProtocolVersion === 10 && info.Version && info.Version.length > 0 && info.Salt && info.Salt.length > 0 && info.CapabilityFlags > 0 && info.Debug && info.Debug.PacketType === 'handshake';
Export({
ok: ok,
isMySQL: isMySQL,
packetType: info.Debug.PacketType,
protocolVersion: info.ProtocolVersion,
version: info.Version,
threadId: info.ThreadID,
authPluginName: info.AuthPluginName,
capabilityFlags: info.CapabilityFlags,
capabilities: info.Capabilities,
status: info.Status,
salt: info.Salt,
});
args:
Host: "{{Host}}"
Port: "3306"
matchers:
- type: dsl
dsl:
- "success == true"
- "ok == true"
- "isMySQL == true"
- 'packetType == "handshake"'
- "protocolVersion == 10"
- "len(version) > 0"
- "capabilityFlags > 0"
- "len(salt) > 0"
- "threadId > 0"
@@ -0,0 +1,29 @@
id: redis-lua-script
info:
name: Redis RunLuaScript - Detect
author: DhiyaneshDK
severity: info
javascript:
- code: |
const redis = require('nuclei/redis');
// Set a key via KEYS with a string and a numeric arg via ARGV: validates
// that keys are passed through and non-string args are not dropped
redis.RunLuaScript(Host, Port, Password, 'return redis.call("set", KEYS[1], ARGV[1] .. ":" .. ARGV[2])', ['testkey'], ['testvalue', 123]);
// Read the value back via KEYS and return it as the response so the
// matcher can assert the combined string was stored correctly
redis.RunLuaScript(Host, Port, Password, 'return redis.call("get", KEYS[1])', ['testkey'], []);
args:
Host: "{{Host}}"
Port: "6379"
# must match the --requirepass value of the dockerized redis instance
# started by internal/tests/integration/javascript_test.go
Password: "iamadmin"
matchers:
- type: dsl
dsl:
- 'success == true && contains(response, "testvalue:123")'
@@ -0,0 +1,48 @@
id: network-duration
info:
name: Network Duration
author: pdteam
severity: info
flow: tcp("duration_network")
network:
- id: duration_network
inputs:
- data: "FIRST"
read: 4
name: first
- data: "SECOND"
read: 4
name: second
host:
- "{{Hostname}}"
read-size: 6
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_1 > 0 && duration_1 < 60"
- "duration_2 > 0 && duration_2 < 60"
- "duration == duration_2"
- "duration_network_duration > 0 && duration_network_duration < 60"
- "duration_network_duration_1 > 0 && duration_network_duration_1 < 60"
- "duration_network_duration_2 > 0 && duration_network_duration_2 < 60"
- "duration_network_duration == duration"
- "duration_network_duration_1 == duration_1"
- "duration_network_duration_2 == duration_2"
- type: word
part: first
words:
- "PING"
- type: word
part: second
words:
- "PONG"
- type: word
part: data
words:
- "NUCLEI"
matchers-condition: and
@@ -0,0 +1,20 @@
id: ssl-duration
info:
name: SSL Duration
author: pdteam
severity: info
flow: ssl("duration_ssl")
ssl:
- id: duration_ssl
address: "{{Host}}:{{Port}}"
matchers:
- type: dsl
condition: and
dsl:
- "probe_status == true"
- "duration > 0 && duration < 60"
- "duration_ssl_duration > 0 && duration_ssl_duration < 60"
- "duration == duration_ssl_duration"
@@ -0,0 +1,36 @@
id: websocket-duration
info:
name: WebSocket Duration
author: pdteam
severity: info
flow: websocket("duration_ws")
websocket:
- id: duration_ws
address: "{{Scheme}}://{{Hostname}}"
inputs:
- data: hello
- data: status
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_1 > 0 && duration_1 < 60"
- "duration_2 > 0 && duration_2 < 60"
- "duration == duration_2"
- "duration_ws_duration > 0 && duration_ws_duration < 60"
- "duration_ws_duration_1 > 0 && duration_ws_duration_1 < 60"
- "duration_ws_duration_2 > 0 && duration_ws_duration_2 < 60"
- "duration_ws_duration == duration"
- "duration_ws_duration_1 == duration_1"
- "duration_ws_duration_2 == duration_2"
- type: word
part: response
words:
- world
- ready
condition: and
matchers-condition: and
@@ -0,0 +1,19 @@
id: whois-duration
info:
name: WHOIS Duration
author: pdteam
severity: info
flow: whois("duration_whois")
whois:
- id: duration_whois
query: "{{Host}}"
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_whois_duration > 0 && duration_whois_duration < 60"
- "duration == duration_whois_duration"
@@ -7,6 +7,7 @@ import (
"net"
"strings"
"testing"
"time"
"github.com/gobwas/ws/wsutil"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
@@ -38,6 +39,37 @@ func TestWebSocket(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
connHandler := func(conn net.Conn) {
for {
msg, op, err := wsutil.ReadClientData(conn)
if err != nil {
return
}
switch string(msg) {
case "hello":
time.Sleep(integrationDurationObservationDelay)
_ = wsutil.WriteServerMessage(conn, op, []byte("world"))
case "status":
time.Sleep(integrationDurationObservationDelay)
_ = wsutil.WriteServerMessage(conn, op, []byte("ready"))
default:
return
}
}
}
server := testutils.NewWebsocketServer("", connHandler, func(origin string) bool { return true })
defer server.Close()
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/websocket/duration.yaml", strings.ReplaceAll(server.URL, "http", "ws"), suite.debug)
if err != nil {
t.Fatalf("duration websocket request failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("CSWSH", func(t *testing.T) {
server := testutils.NewWebsocketServer("", func(conn net.Conn) {}, func(origin string) bool { return true })
defer server.Close()
+1
View File
@@ -9,6 +9,7 @@ import (
var whoisTestCases = []integrationCase{
{Path: "protocols/whois/basic.yaml", TestCase: &whoisBasic{}},
{Path: "protocols/whois/duration.yaml", TestCase: &whoisBasic{}},
}
type whoisBasic struct{}
-1
View File
@@ -40,7 +40,6 @@ func Cleanup(options *types.Options) {
// DefaultOptions is the default options structure for nuclei during mocking.
var DefaultOptions = &types.Options{
Metrics: false,
Debug: false,
DebugRequests: false,
DebugResponse: false,
+15
View File
@@ -339,6 +339,21 @@ func WithScanStrategy(strategy string) NucleiSDKOptions {
// OutputWriter
type OutputWriter output.Writer
// WithResultCallback sets a result callback invoked for each finding.
//
// When used with ThreadSafeNucleiEngine.ExecuteNucleiWithOpts / ExecuteNucleiWithOptsCtx,
// the callback is scoped to that execution only (combined with any GlobalResultCallback).
// When used at engine construction time, it behaves like the default result callback list.
func WithResultCallback(callback func(event *output.ResultEvent)) NucleiSDKOptions {
return func(e *NucleiEngine) error {
if callback == nil {
return nil
}
e.resultCallbacks = append(e.resultCallbacks, callback)
return nil
}
}
// UseOutputWriter allows setting custom output writer
// by default a mock writer is used with user defined callback
// if outputWriter is used callback will be ignored
+46 -12
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/logrusorgru/aurora/v4"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
@@ -22,14 +23,17 @@ import (
// in ThreadSafeNucleiEngine
type unsafeOptions struct {
executerOpts *protocols.ExecutorOptions
engine *core.Engine
}
// createEphemeralObjects creates ephemeral nuclei objects/instances/types
func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types.Options) (*unsafeOptions, error) {
// createEphemeralObjects creates ephemeral nuclei objects/instances/types.
// outputWriter overrides the base engine writer when non-nil (used for per-call callbacks).
func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types.Options, outputWriter output.Writer) (*unsafeOptions, error) {
if outputWriter == nil {
outputWriter = base.customWriter
}
u := &unsafeOptions{}
u.executerOpts = &protocols.ExecutorOptions{
Output: base.customWriter,
Output: outputWriter,
Options: opts,
Progress: base.customProgress,
Catalog: base.catalog,
@@ -40,6 +44,11 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
ResumeCfg: types.NewResumeCfg(),
Parser: base.parser,
Browser: base.browserInstance,
// Thread-safe executes can run concurrently with different Output writers.
// The compiled-template cache shallow-copies requests and mutates shared
// ExecutorOptions.Output via UpdateOptions/ApplyNewEngineOptions, which
// would otherwise route all findings to whichever call last won the race.
DoNotCache: true,
}
if opts.ShouldUseHostError() && base.hostErrCache != nil {
u.executerOpts.HostErrorsCache = base.hostErrCache
@@ -52,11 +61,33 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
opts.RateLimitDuration = time.Second
}
u.executerOpts.RateLimiter = utils.GetRateLimiter(ctx, opts.RateLimit, opts.RateLimitDuration)
u.engine = core.New(opts)
u.engine.SetExecuterOptions(u.executerOpts)
return u, nil
}
// resolveEphemeralOutput combines the base/global writer with any per-call result callbacks.
// Global callbacks keep working; per-call callbacks are isolated to this execution.
func resolveEphemeralOutput(base, call *NucleiEngine) output.Writer {
out := base.customWriter
if call == nil || len(call.resultCallbacks) == 0 {
return out
}
callbacks := append([]func(event *output.ResultEvent){}, call.resultCallbacks...)
callWriter := testutils.NewMockOutputWriter(call.opts.OmitTemplate)
callWriter.WriteCallback = func(event *output.ResultEvent) {
for _, cb := range callbacks {
if cb != nil {
cb(event)
}
}
}
if out == nil {
return callWriter
}
return output.NewMultiWriter(out, callWriter)
}
// closeEphemeralObjects closes all resources used by ephemeral nuclei objects/instances/types
func closeEphemeralObjects(u *unsafeOptions) {
if u.executerOpts.RateLimiter != nil {
@@ -113,14 +144,15 @@ func (e *ThreadSafeNucleiEngine) GlobalLoadAllTemplates() error {
}
// GlobalResultCallback sets a callback function which will be called for each result
// across all ExecuteNucleiWithOpts invocations. For a callback scoped to a single
// execution, pass WithResultCallback in that call's opts instead.
func (e *ThreadSafeNucleiEngine) GlobalResultCallback(callback func(event *output.ResultEvent)) {
e.eng.resultCallbacks = []func(*output.ResultEvent){callback}
}
// ExecuteNucleiWithOptsCtx executes templates on targets and calls callback on each result(only if results are found)
// This method can be called concurrently and it will use some global resources but can be run parallelly
// by invoking this method with different options and targets
// Note: Not all options are thread-safe. this method will throw error if you try to use non-thread-safe options
// ExecuteNucleiWithOptsCtx executes templates on targets and can be called concurrently.
// Pass WithResultCallback in opts for a per-execution result callback (combined with any
// GlobalResultCallback). Not all options are thread-safe.
func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, targets []string, opts ...NucleiSDKOptions) error {
baseOpts := e.eng.opts.Copy()
tmpEngine := &NucleiEngine{opts: baseOpts, mode: threadSafe}
@@ -131,7 +163,7 @@ func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, t
}
// create ephemeral nuclei objects/instances/types using base nuclei engine
unsafeOpts, err := createEphemeralObjects(ctx, e.eng, tmpEngine.opts)
unsafeOpts, err := createEphemeralObjects(ctx, e.eng, tmpEngine.opts, resolveEphemeralOutput(e.eng, tmpEngine))
if err != nil {
return err
}
@@ -145,7 +177,9 @@ func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, t
}
unsafeOpts.executerOpts.WorkflowLoader = workflowLoader
store, err := loader.New(loader.NewConfig(tmpEngine.opts, e.eng.catalog, unsafeOpts.executerOpts))
loaderConfig := loader.NewConfig(tmpEngine.opts, e.eng.catalog, unsafeOpts.executerOpts)
loaderConfig.MetadataIndex = e.eng.getMetadataIndex()
store, err := loader.New(loaderConfig)
if err != nil {
return errkit.Wrapf(err, "Could not create loader client: %s", err)
}
+73
View File
@@ -0,0 +1,73 @@
package nuclei
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
)
func BenchmarkExecuteNucleiWithOptsCtx(b *testing.B) {
templatePath := filepath.Join(b.TempDir(), "benchmark.yaml")
err := os.WriteFile(templatePath, []byte(`id: benchmark-execute-nuclei-with-opts-ctx
info:
name: Benchmark ExecuteNucleiWithOptsCtx
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
matchers:
- type: status
status:
- 418
`), 0o600)
if err != nil {
b.Fatalf("could not write benchmark template: %s", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
b.Cleanup(server.Close)
metadataIndex, err := index.NewIndex(b.TempDir())
if err != nil {
b.Fatalf("could not create benchmark metadata index: %s", err)
}
engine, err := NewThreadSafeNucleiEngineCtx(
b.Context(),
DisableUpdateCheck(),
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
)
if err != nil {
b.Fatalf("could not create thread-safe nuclei engine: %s", err)
}
b.Cleanup(engine.Close)
// Keep benchmark metadata isolated from the user's persistent cache.
engine.eng.metadataIndexOnce.Do(func() {
engine.eng.metadataIndex = metadataIndex
})
targets := []string{server.URL}
if err := engine.ExecuteNucleiWithOptsCtx(b.Context(), targets); err != nil {
b.Fatalf("could not warm up nuclei execution: %s", err)
}
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if err := engine.ExecuteNucleiWithOptsCtx(b.Context(), targets); err != nil {
b.Fatalf("could not execute nuclei: %s", err)
}
}
}
+70
View File
@@ -0,0 +1,70 @@
package nuclei
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestCallerParserUsesEngineLocalCompiledCache(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "template.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(`id: parser-lifecycle
info:
name: Parser lifecycle
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
`), 0o600))
callerParser := templates.NewParser()
callerParser.Cache().StoreWithoutRaw("sentinel", &templates.Template{}, nil)
for range 3 {
compiledParser := func() *templates.Parser {
opts := types.DefaultOptions()
opts.Parser = callerParser
opts.Templates = []string{templatePath}
engine, err := NewNucleiEngineCtx(context.Background(), DisableUpdateCheck(), WithOptions(opts))
require.NoError(t, err)
engineParser := engine.GetParser()
require.Same(t, callerParser, engineParser)
require.False(t, engine.GetExecuterOptions().DoNotCache)
compiledParser, ok := engine.GetExecuterOptions().Parser.(*templates.Parser)
require.True(t, ok)
require.NotSame(t, callerParser, compiledParser)
require.Same(t, callerParser.Cache(), compiledParser.Cache())
require.NoError(t, engine.LoadAllTemplates())
require.Len(t, engine.GetTemplates(), 1)
require.Equal(t, 0, callerParser.CompiledCount())
require.Equal(t, 1, compiledParser.CompiledCount())
require.Equal(t, 2, callerParser.ParsedCount())
require.NoError(t, engine.LoadAllTemplates())
require.Len(t, engine.GetTemplates(), 1)
require.Equal(t, 1, compiledParser.CompiledCount())
engine.Close()
return compiledParser
}()
require.Equal(t, 0, callerParser.CompiledCount())
require.Equal(t, 0, compiledParser.CompiledCount())
require.Equal(t, 2, callerParser.ParsedCount())
cached, err := callerParser.Cache().Get("sentinel")
require.NoError(t, err)
require.NotNil(t, cached)
}
}
+168
View File
@@ -0,0 +1,168 @@
package nuclei_test
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/stretchr/testify/require"
)
func TestThreadSafeWithResultCallbackIsolation(t *testing.T) {
templatePath := writeSDKMatchTemplate(t)
srvA := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("token-A-unique"))
}))
t.Cleanup(srvA.Close)
srvB := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("token-B-unique"))
}))
t.Cleanup(srvB.Close)
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background())
require.NoError(t, err)
t.Cleanup(ne.Close)
var (
mu sync.Mutex
callbackA []string
callbackB []string
globalURLs []string
globalCount int
errA, errB error
)
ne.GlobalResultCallback(func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
globalCount++
globalURLs = append(globalURLs, eventMatchedURL(event))
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
errA = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srvA.URL},
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IDs: []string{"sdk-callback-match"}}),
nuclei.WithResultCallback(func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
callbackA = append(callbackA, eventMatchedURL(event))
}),
)
}()
go func() {
defer wg.Done()
errB = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srvB.URL},
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IDs: []string{"sdk-callback-match"}}),
nuclei.WithResultCallback(func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
callbackB = append(callbackB, eventMatchedURL(event))
}),
)
}()
wg.Wait()
require.NoError(t, errA)
require.NoError(t, errB)
mu.Lock()
defer mu.Unlock()
require.NotEmpty(t, callbackA, "per-call callback A should receive results")
require.NotEmpty(t, callbackB, "per-call callback B should receive results")
for _, matched := range callbackA {
require.Contains(t, matched, srvA.URL, "callback A must only see server A")
require.NotContains(t, matched, srvB.URL, "callback A must not see server B")
}
for _, matched := range callbackB {
require.Contains(t, matched, srvB.URL, "callback B must only see server B")
require.NotContains(t, matched, srvA.URL, "callback B must not see server A")
}
// Global callback still sees both executions (no regression).
require.GreaterOrEqual(t, globalCount, 2)
require.NotEmpty(t, globalURLs)
}
func TestThreadSafeGlobalCallbackWithoutPerCallStillWorks(t *testing.T) {
templatePath := writeSDKMatchTemplate(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("token-A-unique"))
}))
t.Cleanup(srv.Close)
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background())
require.NoError(t, err)
t.Cleanup(ne.Close)
var got int
ne.GlobalResultCallback(func(event *output.ResultEvent) {
got++
})
err = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srv.URL},
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IDs: []string{"sdk-callback-match"}}),
)
require.NoError(t, err)
require.GreaterOrEqual(t, got, 1, "global callback should still receive results without WithResultCallback")
}
func TestWithResultCallbackNilIsNoop(t *testing.T) {
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(), nuclei.WithResultCallback(nil))
require.NoError(t, err)
ne.Close()
}
func writeSDKMatchTemplate(t *testing.T) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "sdk-callback-match.yaml")
content := `id: sdk-callback-match
info:
name: SDK callback match
author: nuclei-sdk-test
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/"
matchers:
- type: word
words:
- "token-"
`
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
return path
}
// eventMatchedURL prefers Matched/URL over Host (Host omits the port).
func eventMatchedURL(event *output.ResultEvent) string {
if event == nil {
return ""
}
if event.Matched != "" {
return event.Matched
}
if event.URL != "" {
return event.URL
}
return event.Host
}
+38 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
metadataindex "github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
@@ -80,9 +81,13 @@ type NucleiEngine struct {
browserInstance *engine.Browser
httpClient *retryablehttp.Client
parser *templates.Parser
compiledParser *templates.Parser
ownsParser bool // true when the engine created the parser and may purge it on Close
authprovider authprovider.AuthProvider
metadataIndex *metadataindex.Index
metadataIndexOnce sync.Once
// unexported meta options
opts *types.Options
interactshOpts *interactsh.Options
@@ -100,6 +105,24 @@ type NucleiEngine struct {
tmpDir string
}
func (e *NucleiEngine) getMetadataIndex() *metadataindex.Index {
e.metadataIndexOnce.Do(func() {
metadataIndex, err := metadataindex.NewDefaultIndex()
if err != nil {
e.Logger.Warning().Msgf("Could not create metadata cache: %v", err)
return
}
e.metadataIndex = metadataIndex
if err := e.metadataIndex.Load(); err != nil {
e.Logger.Warning().Msgf("Could not load metadata cache: %v", err)
}
})
return e.metadataIndex
}
// LoadAllTemplates loads all nuclei template based on given options
func (e *NucleiEngine) LoadAllTemplates() error {
workflowLoader, err := workflow.NewLoader(e.executerOpts)
@@ -108,7 +131,11 @@ func (e *NucleiEngine) LoadAllTemplates() error {
}
e.executerOpts.WorkflowLoader = workflowLoader
e.store, err = loader.New(loader.NewConfig(e.opts, e.catalog, e.executerOpts))
loaderConfig := loader.NewConfig(e.opts, e.catalog, e.executerOpts)
if e.mode == threadSafe {
loaderConfig.MetadataIndex = e.getMetadataIndex()
}
e.store, err = loader.New(loaderConfig)
if err != nil {
return errkit.Wrapf(err, "Could not create loader client: %s", err)
}
@@ -239,16 +266,22 @@ func (e *NucleiEngine) closeInternal() {
if e.httpxClient != nil {
_ = e.httpxClient.Close()
}
if e.metadataIndex != nil {
if err := e.metadataIndex.Save(); err != nil {
e.Logger.Warning().Msgf("Could not save metadata cache: %v", err)
}
}
if e.tmpDir != "" {
_ = os.RemoveAll(e.tmpDir)
}
if e.opts != nil {
generators.ClearOptionsPayloadMap(e.opts)
}
// Purge the template caches (compiled templates are heap-heavy) so a
// long-running embedder does not retain them for the process lifetime.
// Only do this when the engine created the parser; a caller-supplied
// parser is an opt-in shared cache the caller owns.
// Purge engine-owned template caches. A caller-supplied parser remains
// untouched, while its engine-local compiled cache is always released.
if e.compiledParser != nil && e.compiledParser != e.parser {
e.compiledParser.CompiledCache().Purge()
}
if e.ownsParser && e.parser != nil {
e.parser.Purge()
}
+28 -9
View File
@@ -144,6 +144,9 @@ func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
// init
func (e *NucleiEngine) init(ctx context.Context) error {
if err := config.DefaultConfig.InitializationError(); err != nil {
return errors.Wrap(err, "initialize nuclei configuration")
}
// Update logger ref (if it was changed by [WithLogger])
// (Logger is already initialized)
if e.opts.Logger != e.Logger {
@@ -172,6 +175,14 @@ func (e *NucleiEngine) init(ctx context.Context) error {
e.parser = templates.NewParser()
e.ownsParser = true
}
e.compiledParser = e.parser
if !e.ownsParser {
// Compiled templates retain engine-local ExecutorOptions. Keep their
// cache private while the caller-owned parser provides the parsed cache.
e.compiledParser = templates.NewParserWithParsedCache(e.parser.Cache())
e.compiledParser.ShouldValidate = e.parser.ShouldValidate
e.compiledParser.NoStrictSyntax = e.parser.NoStrictSyntax
}
if protocolstate.ShouldInit(e.opts.ExecutionId) {
_ = protocolinit.Init(e.opts)
@@ -250,7 +261,8 @@ func (e *NucleiEngine) init(ctx context.Context) error {
Colorizer: aurora.New(aurora.WithColors(true)),
ResumeCfg: types.NewResumeCfg(),
Browser: e.browserInstance,
Parser: e.parser,
Parser: e.compiledParser,
DoNotCache: e.opts.DoNotCacheTemplates,
InputHelper: input.NewHelper(),
TemporaryDirectory: e.tmpDir,
Logger: e.opts.Logger,
@@ -306,8 +318,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
if cachedParser, ok := e.opts.Parser.(*templates.Parser); ok {
e.parser = cachedParser
e.opts.Parser = cachedParser
e.executerOpts.Parser = cachedParser
e.executerOpts.Options.Parser = cachedParser
}
}
@@ -332,14 +342,23 @@ func (e *NucleiEngine) init(ctx context.Context) error {
e.httpxClient = nucleiUtils.GetInputLivenessChecker(client)
}
// Only Happens once regardless how many times this function is called
// This will update ignore file to filter out templates with weak matchers to avoid false positives
// and also upgrade templates to latest version if available
installer.NucleiSDKVersionCheck()
if DefaultConfig.CanCheckForUpdates() {
return e.processUpdateCheckResults()
// Only Happens once regardless how many times this function is called
// This will update ignore file to filter out templates with weak matchers to avoid false positives
// and also upgrade templates to latest version if available
latestIgnoreHash, _ := installer.NucleiSDKVersionCheck()
if err := e.processUpdateCheckResults(); err != nil {
return err
}
if DefaultConfig.IgnoreFileNeedsUpdate(latestIgnoreHash) {
if err := installer.UpdateIgnoreFile(); err != nil {
e.opts.Logger.Warning().Msgf("failed to update nuclei ignore file: %s\n", err)
}
}
}
return nil
}
+21 -18
View File
@@ -18,11 +18,16 @@ var knownLeaks = []goleak.Option{
// net/http transport maintains idle keep-alive connections whose goroutines
// exit on idle timeout or explicit close - not real leaks.
goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
// expirable LRU cache creates a background goroutine for TTL expiration that persists
// see: https://github.com/hashicorp/golang-lru/blob/770151e9c8cdfae1797826b7b74c33d6f103fbd8/expirable/expirable_lru.go#L79
goleak.IgnoreAnyContainingPkg("github.com/hashicorp/golang-lru/v2/expirable"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
// httpcache leveldb + shared rate limiter + memguardian outlive a single SDK engine.
// Sleep alone is not reliable under -race or when NewNucleiEngineCtx fails before Close.
goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),
goleak.IgnoreAnyFunction("github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate.StartActiveMemGuardian.func1"),
}
func TestSimpleNuclei(t *testing.T) {
@@ -38,12 +43,12 @@ func TestSimpleNuclei(t *testing.T) {
nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}), // filter dns templates
nuclei.EnableStatsWithOpts(nuclei.StatsOptions{JSON: true}),
)
require.Nil(t, err)
require.NoError(t, err)
defer ne.Close()
ne.LoadTargets([]string{"scanme.sh"}, false) // probe non http/https target is set to false here
// when callback is nil it nuclei will print JSON output to stdout
err = ne.ExecuteWithCallback(nil)
require.Nil(t, err)
defer ne.Close()
require.NoError(t, err)
}
// this is shared test so needs to be run as separate process
@@ -76,14 +81,14 @@ func TestSimpleNucleiRemote(t *testing.T) {
},
),
)
require.Nil(t, err)
require.NoError(t, err)
defer ne.Close()
ne.LoadTargets([]string{"scanme.sh"}, false) // probe non http/https target is set to false here
err = ne.LoadAllTemplates()
require.Nil(t, err, "could not load templates")
require.NoError(t, err, "could not load templates")
// when callback is nil it nuclei will print JSON output to stdout
err = ne.ExecuteWithCallback(nil)
require.Nil(t, err)
defer ne.Close()
require.NoError(t, err)
}
// this is shared test so needs to be run as separate process
if env.GetEnvOrDefault("TestSimpleNucleiRemote", false) {
@@ -108,22 +113,20 @@ func TestThreadSafeNuclei(t *testing.T) {
}()
// create nuclei engine with options
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.TODO())
require.Nil(t, err)
require.NoError(t, err)
defer ne.Close()
// scan 1 = run dns templates on scanme.sh
t.Run("scanme.sh", func(t *testing.T) {
err = ne.ExecuteNucleiWithOpts([]string{"scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
require.Nil(t, err)
require.NoError(t, err)
})
// scan 2 = run dns templates on honey.scanme.sh
t.Run("honey.scanme.sh", func(t *testing.T) {
err = ne.ExecuteNucleiWithOpts([]string{"honey.scanme.sh"}, nuclei.WithTemplateFilters(nuclei.TemplateFilters{ProtocolTypes: "dns"}))
require.Nil(t, err)
require.NoError(t, err)
})
// wait for all scans to finish
defer ne.Close()
}
if env.GetEnvOrDefault("TestThreadSafeNuclei", false) {
@@ -153,11 +156,11 @@ func TestWithVarsNuclei(t *testing.T) {
nuclei.WithVars([]string{"token=foobar"}),
nuclei.WithVerbosity(nuclei.VerbosityOptions{Debug: true}),
)
require.Nil(t, err)
require.NoError(t, err)
defer ne.Close()
ne.LoadTargets([]string{"scanme.sh"}, true) // probe http/https target is set to true here
err = ne.ExecuteWithCallback(nil)
require.Nil(t, err)
defer ne.Close()
require.NoError(t, err)
}
// this is shared test so needs to be run as separate process
if env.GetEnvOrDefault("TestWithVarsNuclei", false) {
+31 -1
View File
@@ -122,7 +122,12 @@
"CAA",
"TLSA",
"ANY",
"SRV"
"SRV",
"RRSIG",
"NSEC",
"DNSKEY",
"NSEC3",
"NSEC3PARAM"
],
"title": "type of DNS request to make",
"description": "Type is the type of DNS request to make"
@@ -468,6 +473,31 @@
"type": "boolean",
"title": "do not perform recursion",
"description": "Specifies whether to not do recursive checks if folders are provided"
},
"smb-user": {
"type": "string",
"title": "SMB username",
"description": "Username for SMB file targets"
},
"smb-password": {
"type": "string",
"title": "SMB password",
"description": "Password for SMB file targets"
},
"smb-domain": {
"type": "string",
"title": "SMB domain",
"description": "Domain for SMB authentication"
},
"smb-hash": {
"type": "string",
"title": "SMB NT hash",
"description": "NT hash for SMB pass-the-hash"
},
"smb-port": {
"type": "integer",
"title": "SMB port",
"description": "TCP port for SMB (default 445)"
}
},
"additionalProperties": false,
+9 -3
View File
@@ -21,9 +21,15 @@ var (
)
const (
TemplateConfigFileName = ".templates-config.json"
// TODO(dwisiswant0): do not use this in the future.
legacyTemplatesConfigFileName = ".templates-config.json"
// TemplateConfigFileName is the legacy templates-state filename.
//
// Deprecated: use TemplatesStateFileName for current templates state.
TemplateConfigFileName = legacyTemplatesConfigFileName
TemplatesStateFileName = "templates.json"
NucleiTemplatesDirName = "nuclei-templates"
OfficialNucleiTemplatesRepoName = "nuclei-templates"
OfficialNucleiTemplatesRepoName = NucleiTemplatesDirName
NucleiIgnoreFileName = ".nuclei-ignore"
NucleiTemplatesIndexFileName = ".templates-index" // contains index of official nuclei templates
NucleiTemplatesCheckSumFileName = ".checksum"
@@ -31,7 +37,7 @@ const (
CLIConfigFileName = "config.yaml"
ReportingConfigFilename = "reporting-config.yaml"
// Version is the current version of nuclei
Version = `v3.10.0`
Version = `v3.11.1`
// Directory Names of custom templates
CustomS3TemplatesDirName = "s3"
CustomGitHubTemplatesDirName = "github"
+64 -4
View File
@@ -1,6 +1,8 @@
package config
import (
"crypto/md5"
"fmt"
"os"
"github.com/projectdiscovery/gologger"
@@ -13,11 +15,12 @@ type IgnoreFile struct {
Files []string `yaml:"files"`
}
// ReadIgnoreFile reads the nuclei ignore file returning blocked tags and paths
// ReadIgnoreFile reads the .nuclei-ignore file returning blocked tags and paths.
func ReadIgnoreFile() IgnoreFile {
file, err := os.Open(DefaultConfig.GetIgnoreFilePath())
path := DefaultConfig.GetActiveIgnoreFilePath()
file, err := os.Open(path)
if err != nil {
gologger.Error().Msgf("Could not read nuclei-ignore file: %s\n", err)
gologger.Error().Msgf("Could not read .nuclei-ignore file %q: %s\n", path, err)
return IgnoreFile{}
}
defer func() {
@@ -26,8 +29,65 @@ func ReadIgnoreFile() IgnoreFile {
ignore := IgnoreFile{}
if err := yaml.NewDecoder(file).Decode(&ignore); err != nil {
gologger.Error().Msgf("Could not parse nuclei-ignore file: %s\n", err)
gologger.Error().Msgf("Could not parse .nuclei-ignore file %q: %s\n", path, err)
return IgnoreFile{}
}
return ignore
}
// WriteActiveIgnoreFile atomically replaces the active root ignore file.
func (c *Config) WriteActiveIgnoreFile(data []byte) error {
if err := atomicWriteFile(c.GetActiveIgnoreFilePath(), data, 0o600); err != nil {
return fmt.Errorf("write active .nuclei-ignore file: %w", err)
}
return nil
}
// IgnoreFileNeedsUpdate reports whether the active ignore file is
// missing or differs from the latest known hash.
func (c *Config) IgnoreFileNeedsUpdate(latestHash string) bool {
ignoreFilePath := c.GetActiveIgnoreFilePath()
contents, err := os.ReadFile(ignoreFilePath)
if err != nil {
return true
}
if latestHash == "" {
return false
}
return ignoreFileHash(contents) != latestHash
}
// NeedsIgnoreFileUpdate reports whether the active ignore file differs
// from the deprecated in-memory remote hash.
//
// Deprecated: pass the transient remote hash to IgnoreFileNeedsUpdate.
func (c *Config) NeedsIgnoreFileUpdate() bool {
c.m.Lock()
latestHash := c.LatestNucleiIgnoreHash
c.m.Unlock()
return c.IgnoreFileNeedsUpdate(latestHash)
}
// UpdateNucleiIgnoreHash refreshes the deprecated in-memory compatibility
// field from the active ignore file.
//
// Deprecated: local ignore-file hashes are derived from the active file.
func (c *Config) UpdateNucleiIgnoreHash() error {
ignoreFilePath := c.GetActiveIgnoreFilePath()
contents, err := os.ReadFile(ignoreFilePath)
if err != nil {
return fmt.Errorf("read active .nuclei-ignore file %q: %w", ignoreFilePath, err)
}
c.NucleiIgnoreHash = ignoreFileHash(contents)
return nil
}
func ignoreFileHash(contents []byte) string {
return fmt.Sprintf("%x", md5.Sum(contents))
}
+54
View File
@@ -0,0 +1,54 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestReadIgnoreFileAllowsMissingOrInvalidActiveFile(t *testing.T) {
root := t.TempDir()
cfg := DefaultConfig
oldRoot := cfg.TemplatesDirectory
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
cfg.SetTemplatesDir(root)
if ignore := ReadIgnoreFile(); len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
t.Fatalf("missing ignore file = %+v, want empty", ignore)
}
if err := os.WriteFile(filepath.Join(root, NucleiIgnoreFileName), []byte("tags: ["), 0o600); err != nil {
t.Fatalf("write invalid ignore file: %v", err)
}
if ignore := ReadIgnoreFile(); len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
t.Fatalf("invalid ignore file = %+v, want empty", ignore)
}
if err := os.WriteFile(filepath.Join(root, NucleiIgnoreFileName), []byte("tags: [weak]\nfiles: [blocked.yaml]\n"), 0o600); err != nil {
t.Fatalf("write ignore file: %v", err)
}
ignore := ReadIgnoreFile()
if len(ignore.Tags) != 1 || ignore.Tags[0] != "weak" {
t.Fatalf("ignore tags = %v, want [weak]", ignore.Tags)
}
if len(ignore.Files) != 1 || ignore.Files[0] != "blocked.yaml" {
t.Fatalf("ignore files = %v, want [blocked.yaml]", ignore.Files)
}
}
func TestSetTemplatesDirDoesNotCopyLegacyIgnoreFile(t *testing.T) {
dir := t.TempDir()
configDir := filepath.Join(dir, "config")
legacyPath := filepath.Join(configDir, NucleiIgnoreFileName)
writeTestFile(t, legacyPath, []byte("tags: [legacy]\n"), 0o600)
root := filepath.Join(dir, "active")
if err := os.MkdirAll(root, 0o700); err != nil {
t.Fatalf("create active root: %v", err)
}
cfg := &Config{configDir: configDir}
cfg.SetTemplatesDir(root)
if _, err := os.Stat(cfg.GetActiveIgnoreFilePath()); !os.IsNotExist(err) {
t.Fatalf("legacy ignore file was copied into active root: %v", err)
}
}
+166 -140
View File
@@ -2,21 +2,20 @@ package config
import (
"bytes"
"crypto/md5"
"fmt"
"errors"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"github.com/adrg/xdg"
"github.com/projectdiscovery/gologger"
filepathutil "github.com/projectdiscovery/nuclei/v3/pkg/utils/filepath"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
"github.com/projectdiscovery/utils/env"
"github.com/projectdiscovery/utils/errkit"
fileutil "github.com/projectdiscovery/utils/file"
folderutil "github.com/projectdiscovery/utils/folder"
)
// DefaultConfig is the default nuclei configuration
@@ -26,32 +25,44 @@ var DefaultConfig *Config
type Config struct {
TemplatesDirectory string `json:"nuclei-templates-directory,omitempty"`
// customtemplates exists in templates directory with the name of custom-templates provider
// below custom paths are absolute paths to respective custom-templates directories
// Custom template paths are absolute provider-specific directories under the
// active template root.
CustomS3TemplatesDirectory string `json:"custom-s3-templates-directory"`
CustomGitHubTemplatesDirectory string `json:"custom-github-templates-directory"`
CustomGitLabTemplatesDirectory string `json:"custom-gitlab-templates-directory"`
CustomAzureTemplatesDirectory string `json:"custom-azure-templates-directory"`
TemplateVersion string `json:"nuclei-templates-version,omitempty"`
NucleiIgnoreHash string `json:"nuclei-ignore-hash,omitempty"`
TemplateVersion string `json:"nuclei-templates-version,omitempty"`
// NucleiIgnoreHash is retained for SDK compatibility. Nuclei derives the
// current hash from the active ignore file and does not persist this field.
//
// Deprecated: local ignore-file hashes are derived from the active file.
NucleiIgnoreHash string `json:"-"`
LogAllEvents bool `json:"-"` // when enabled logs all events (more than verbose)
HideTemplateSigWarning bool `json:"-"` // when enabled disables template signature warning
// LatestXXX are not meant to be used directly and is used as
// local cache of nuclei version check endpoint
// these fields are only update during nuclei version check
// TODO: move these fields to a separate unexported struct as they are not meant to be used directly
LatestNucleiVersion string `json:"nuclei-latest-version"`
LatestNucleiTemplatesVersion string `json:"nuclei-templates-latest-version"`
LatestNucleiIgnoreHash string `json:"nuclei-latest-ignore-hash,omitempty"`
Logger *gologger.Logger `json:"-"` // logger
// These fields cache version-check results.
LatestNucleiVersion string `json:"nuclei-latest-version"`
LatestNucleiTemplatesVersion string `json:"nuclei-templates-latest-version"`
// LatestNucleiIgnoreHash is retained for SDK compatibility. Nuclei passes
// remote ignore hashes directly from version checks to the active-file
// comparison and does not persist this field.
//
// Deprecated: remote ignore hashes are transient version-check results.
LatestNucleiIgnoreHash string `json:"-"`
Logger *gologger.Logger `json:"-"` // logger
// internal / unexported fields
disableUpdates bool `json:"-"` // disable updates both version check and template updates
homeDir string `json:"-"` // User Home Directory
configDir string `json:"-"` // Nuclei Global Config Directory
debugArgs []string `json:"-"` // debug args
disableUpdates bool `json:"-"` // disable updates both version check and template updates
configDir string `json:"-"` // Nuclei Global Config Directory
stateDir string `json:"-"` // Restart-persistent operational state
cacheDir string `json:"-"` // Regenerable cached data
dataHome string `json:"-"` // User template data
dataDirs []string `json:"-"` // Template data, in preference order
debugArgs []string `json:"-"` // debug args
initializationErr error `json:"-"`
m sync.Mutex
}
@@ -75,37 +86,46 @@ func (c *Config) IsCustomTemplate(templatePath string) bool {
return true
}
}
if c.TemplatesDirectory == "" {
return false
}
return !filepathutil.IsPathWithinDirectory(templatePath, c.TemplatesDirectory)
}
// WriteVersionCheckData writes version check data to config file
func (c *Config) WriteVersionCheckData(ignorehash, nucleiVersion, templatesVersion string) error {
func (c *Config) WriteVersionCheckData(ignoreHash, nucleiVersion, templatesVersion string) error {
updated := false
if ignorehash != "" && c.LatestNucleiIgnoreHash != ignorehash {
c.LatestNucleiIgnoreHash = ignorehash
updated = true
if ignoreHash != "" && c.LatestNucleiIgnoreHash != ignoreHash {
// Retain the historical in-memory SDK behavior without persisting remote
// ignore metadata.
c.LatestNucleiIgnoreHash = ignoreHash
}
if nucleiVersion != "" && c.LatestNucleiVersion != nucleiVersion {
c.LatestNucleiVersion = nucleiVersion
updated = true
}
if templatesVersion != "" && c.LatestNucleiTemplatesVersion != templatesVersion {
c.LatestNucleiTemplatesVersion = templatesVersion
updated = true
}
// write config to disk if any of the fields are updated
if updated {
return c.WriteTemplatesConfig()
}
return nil
}
// GetTemplateDir returns the nuclei templates directory absolute path
func (c *Config) GetTemplateDir() string {
val, _ := filepath.Abs(c.TemplatesDirectory)
return val
}
@@ -113,6 +133,7 @@ func (c *Config) GetTemplateDir() string {
func (c *Config) DisableUpdateCheck() {
c.m.Lock()
defer c.m.Unlock()
c.disableUpdates = true
}
@@ -120,6 +141,7 @@ func (c *Config) DisableUpdateCheck() {
func (c *Config) CanCheckForUpdates() bool {
c.m.Lock()
defer c.m.Unlock()
return !c.disableUpdates
}
@@ -127,30 +149,9 @@ func (c *Config) CanCheckForUpdates() bool {
func (c *Config) NeedsTemplateUpdate() bool {
c.m.Lock()
defer c.m.Unlock()
return !c.disableUpdates && (c.TemplateVersion == "" || IsOutdatedVersion(c.TemplateVersion, c.LatestNucleiTemplatesVersion) || !fileutil.FolderExists(c.TemplatesDirectory))
}
// NeedsIgnoreFileUpdate returns true if Ignore file hash is different (aka ignore file is outdated)
func (c *Config) NeedsIgnoreFileUpdate() bool {
c.m.Lock()
defer c.m.Unlock()
return c.NucleiIgnoreHash == "" || c.NucleiIgnoreHash != c.LatestNucleiIgnoreHash
}
// UpdateNucleiIgnoreHash updates the nuclei ignore hash in config
func (c *Config) UpdateNucleiIgnoreHash() error {
// calculate hash of ignore file and update config
ignoreFilePath := c.GetIgnoreFilePath()
if fileutil.FileExists(ignoreFilePath) {
bin, err := os.ReadFile(ignoreFilePath)
if err != nil {
return errkit.Newf("could not read nuclei ignore file: %v", err)
}
c.NucleiIgnoreHash = fmt.Sprintf("%x", md5.Sum(bin))
// write config to disk
return c.WriteTemplatesConfig()
}
return errkit.New("ignore file not found: could not update nuclei ignore hash")
return !c.disableUpdates &&
(c.TemplateVersion == "" || IsOutdatedVersion(c.TemplateVersion, c.LatestNucleiTemplatesVersion) || !fileutil.FolderExists(c.TemplatesDirectory))
}
// GetConfigDir returns the nuclei configuration directory
@@ -158,6 +159,21 @@ func (c *Config) GetConfigDir() string {
return c.configDir
}
// GetStateDir returns the directory for restart-persistent operational state.
func (c *Config) GetStateDir() string {
if c.stateDir != "" {
return c.stateDir
}
return defaultStateDir()
}
// SetStateDir changes the templates state directory. It is primarily useful to
// SDK callers that isolate all Nuclei storage.
func (c *Config) SetStateDir(dir string) {
c.stateDir = dir
}
// GetKeysDir returns the nuclei signer keys directory
func (c *Config) GetKeysDir() string {
return filepath.Join(c.configDir, "keys")
@@ -174,10 +190,19 @@ func (c *Config) GetReportingConfigFilePath() string {
}
// GetIgnoreFilePath returns the nuclei ignore file path
//
// Deprecated: use GetActiveIgnoreFilePath. The config-directory ignore file is
// no longer active.
func (c *Config) GetIgnoreFilePath() string {
return filepath.Join(c.configDir, NucleiIgnoreFileName)
}
// GetActiveIgnoreFilePath returns the ignore file in the active public template
// root.
func (c *Config) GetActiveIgnoreFilePath() string {
return filepath.Join(c.TemplatesDirectory, NucleiIgnoreFileName)
}
func (c *Config) GetTemplateIndexFilePath() string {
return filepath.Join(c.TemplatesDirectory, NucleiTemplatesIndexFileName)
}
@@ -196,58 +221,75 @@ func (c *Config) GetFlagsConfigFilePath() string {
// if .new-additions file is not present empty slice is returned
func (c *Config) GetNewAdditions() []string {
arr := []string{}
newAdditionsPath := filepath.Join(c.TemplatesDirectory, NewTemplateAdditionsFileName)
if !fileutil.FileExists(newAdditionsPath) {
return arr
}
bin, err := os.ReadFile(newAdditionsPath)
if err != nil {
return arr
}
for v := range strings.FieldsSeq(string(bin)) {
if IsTemplateWithRoot(v, c.TemplatesDirectory) {
arr = append(arr, v)
}
}
return arr
}
// GetCacheDir returns the nuclei cache directory
// with new version of nuclei cache directory is changed
// instead of saving resume files in nuclei config directory
// they are saved in nuclei cache directory
// GetCacheDir returns the directory for regenerable cached data.
func (c *Config) GetCacheDir() string {
return folderutil.AppCacheDirOrDefault(".nuclei-cache", BinaryName)
if c.cacheDir != "" {
return c.cacheDir
}
return defaultCacheDir()
}
// SetConfigDir sets the nuclei configuration directory
// and appropriate changes are made to the config
func (c *Config) SetConfigDir(dir string) {
c.configDir = dir
// Preserve the historical SDK behavior that SetConfigDir also isolates
// templates state. NUCLEI_CONFIG_DIR is resolved separately and does not move
// state.
c.stateDir = dir
if err := c.createConfigDirIfNotExists(); err != nil {
c.Logger.Fatal().Msgf("Could not create nuclei config directory at %s: %s", c.configDir, err)
}
// if folder already exists read config or create new
if err := c.ReadTemplatesConfig(); err != nil {
if !errors.Is(err, os.ErrNotExist) {
c.Logger.Fatal().Msgf("Could not read templates state: %s", err)
}
// create new config
applyDefaultConfig()
if defaultsErr := c.applyDefaultConfig(); defaultsErr != nil {
c.Logger.Fatal().Msgf("Could not select default templates directory: %s", defaultsErr)
}
if err2 := c.WriteTemplatesConfig(); err2 != nil {
c.Logger.Fatal().Msgf("Could not create nuclei config file at %s: %s", c.getTemplatesConfigFilePath(), err2)
}
}
// while other config files are optional, ignore file is mandatory
// since it is used to ignore templates with weak matchers
c.copyIgnoreFile()
}
// SetTemplatesDir sets the new nuclei templates directory
func (c *Config) SetTemplatesDir(dirPath string) {
c.setTemplatesDir(dirPath)
}
func (c *Config) setTemplatesDir(dirPath string) {
if dirPath != "" && !filepath.IsAbs(dirPath) {
cwd, _ := os.Getwd()
dirPath = filepath.Join(cwd, dirPath)
}
c.TemplatesDirectory = dirPath
// Update the custom templates directory
c.CustomGitHubTemplatesDirectory = filepath.Join(dirPath, CustomGitHubTemplatesDirName)
@@ -259,64 +301,25 @@ func (c *Config) SetTemplatesDir(dirPath string) {
// SetTemplatesVersion sets the new nuclei templates version
func (c *Config) SetTemplatesVersion(version string) error {
c.TemplateVersion = version
// write config to disk
if err := c.WriteTemplatesConfig(); err != nil {
return errkit.Newf("could not write nuclei config file at %s: %v", c.getTemplatesConfigFilePath(), err)
}
return nil
}
// ReadTemplatesConfig reads the nuclei templates config file
func (c *Config) ReadTemplatesConfig() error {
if !fileutil.FileExists(c.getTemplatesConfigFilePath()) {
return errkit.Newf("nuclei config file at %s does not exist", c.getTemplatesConfigFilePath())
}
var cfg *Config
bin, err := os.ReadFile(c.getTemplatesConfigFilePath())
if err != nil {
return errkit.Newf("could not read nuclei config file at %s: %v", c.getTemplatesConfigFilePath(), err)
}
if err := json.Unmarshal(bin, &cfg); err != nil {
return errkit.Newf("could not unmarshal nuclei config file at %s: %v", c.getTemplatesConfigFilePath(), err)
}
// apply config
c.TemplatesDirectory = cfg.TemplatesDirectory
c.TemplateVersion = cfg.TemplateVersion
c.NucleiIgnoreHash = cfg.NucleiIgnoreHash
c.LatestNucleiIgnoreHash = cfg.LatestNucleiIgnoreHash
c.LatestNucleiTemplatesVersion = cfg.LatestNucleiTemplatesVersion
return nil
}
// WriteTemplatesConfig writes the nuclei templates config file
func (c *Config) WriteTemplatesConfig() error {
// check if config folder exists if not create one
if err := c.createConfigDirIfNotExists(); err != nil {
return err
}
bin, err := json.Marshal(c)
if err != nil {
return errkit.Newf("failed to marshal nuclei config: %v", err)
}
if err = os.WriteFile(c.getTemplatesConfigFilePath(), bin, 0600); err != nil {
return errkit.Newf("failed to write nuclei config file at %s: %v", c.getTemplatesConfigFilePath(), err)
}
return nil
}
// WriteTemplatesIndex writes the nuclei templates index file
func (c *Config) WriteTemplatesIndex(index map[string]string) error {
indexFile := c.GetTemplateIndexFilePath()
var buff bytes.Buffer
for k, v := range index {
_, _ = buff.WriteString(k + "," + v + "\n")
}
return os.WriteFile(indexFile, buff.Bytes(), 0600)
}
// getTemplatesConfigFilePath returns configDir/.templates-config.json file path
func (c *Config) getTemplatesConfigFilePath() string {
return filepath.Join(c.configDir, TemplateConfigFileName)
return os.WriteFile(indexFile, buff.Bytes(), 0600)
}
// createConfigDirIfNotExists creates the nuclei config directory if not exists
@@ -326,23 +329,8 @@ func (c *Config) createConfigDirIfNotExists() error {
return errkit.Newf("could not create nuclei config directory at %s: %v", c.configDir, err)
}
}
return nil
}
// copyIgnoreFile copies the nuclei ignore file default config directory
// to the current config directory
func (c *Config) copyIgnoreFile() {
if err := c.createConfigDirIfNotExists(); err != nil {
c.Logger.Error().Msgf("Could not create nuclei config directory at %s: %s", c.configDir, err)
return
}
ignoreFilePath := c.GetIgnoreFilePath()
if !fileutil.FileExists(ignoreFilePath) {
// copy ignore file from default config directory
if err := fileutil.CopyFile(filepath.Join(folderutil.AppConfigDirOrDefault(FallbackConfigFolderName, BinaryName), NucleiIgnoreFileName), ignoreFilePath); err != nil {
c.Logger.Error().Msgf("Could not copy nuclei ignore file at %s: %s", ignoreFilePath, err)
}
}
return nil
}
// IsDebugArgEnabled checks if debug arg is enabled
@@ -355,22 +343,20 @@ func (c *Config) IsDebugArgEnabled(arg string) bool {
// parseDebugArgs from string
func (c *Config) parseDebugArgs(data string) {
// use space as separator instead of commas
tmp := strings.Fields(data)
for _, v := range tmp {
for v := range strings.FieldsSeq(data) {
key := v
value := ""
// if it is key value pair then split it
if strings.Contains(v, "=") {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
continue
}
key, value = strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
if k, val, ok := strings.Cut(v, "="); ok {
key, value = strings.TrimSpace(k), strings.TrimSpace(val)
}
if value == "false" || value == "0" {
// if false or disabled then skip
continue
}
switch key {
case DebugArgHostErrorStats:
c.debugArgs = append(c.debugArgs, DebugArgHostErrorStats)
@@ -381,7 +367,7 @@ func (c *Config) parseDebugArgs(data string) {
}
func init() {
ConfigDir := folderutil.AppConfigDirOrDefault(FallbackConfigFolderName, BinaryName)
ConfigDir := defaultConfigDir()
if cfgDir := os.Getenv(NucleiConfigDirEnv); cfgDir != "" {
ConfigDir = cfgDir
@@ -393,9 +379,13 @@ func init() {
gologger.Error().Msgf("failed to create config directory at %v got: %s", ConfigDir, err)
}
}
DefaultConfig = &Config{
homeDir: folderutil.HomeDirOrDefault(""),
configDir: ConfigDir,
stateDir: defaultStateDir(),
cacheDir: defaultCacheDir(),
dataHome: xdg.DataHome,
dataDirs: append([]string(nil), xdg.DataDirs...),
Logger: gologger.DefaultLogger,
}
@@ -411,25 +401,61 @@ func init() {
// try to read config from file
if err := DefaultConfig.ReadTemplatesConfig(); err != nil {
gologger.Verbose().Msgf("config file not found, creating new config file at %s", DefaultConfig.getTemplatesConfigFilePath())
applyDefaultConfig()
// write config to file
if err := DefaultConfig.WriteTemplatesConfig(); err != nil {
gologger.Error().Msgf("failed to write config file at %s got: %s", DefaultConfig.getTemplatesConfigFilePath(), err)
if !errors.Is(err, os.ErrNotExist) {
DefaultConfig.initializationErr = err
gologger.Error().Msgf("Failed to read templates state: %s", err)
} else {
gologger.Verbose().Msgf("Templates state not found, creating %s", DefaultConfig.getTemplatesConfigFilePath())
}
if defaultsErr := DefaultConfig.applyDefaultConfig(); defaultsErr != nil {
DefaultConfig.initializationErr = errors.Join(DefaultConfig.initializationErr, defaultsErr)
gologger.Error().Msgf("Failed to select default templates directory: %s", defaultsErr)
} else if DefaultConfig.initializationErr == nil {
if writeErr := DefaultConfig.WriteTemplatesConfig(); writeErr != nil {
DefaultConfig.initializationErr = writeErr
gologger.Error().Msgf("Failed to write templates state at %s: %s", DefaultConfig.getTemplatesConfigFilePath(), writeErr)
}
}
}
// Loads/updates paths of custom templates
// Note: custom templates paths should not be updated in config file
// and even if it is changed we don't follow it since it is not expected behavior
// If custom templates are in default locations only then they are loaded while running nuclei
DefaultConfig.SetTemplatesDir(DefaultConfig.TemplatesDirectory)
DefaultConfig.parseDebugArgs(env.GetEnvOrDefault("NUCLEI_ARGS", ""))
// expose the templates directory to the yaml preprocessor so include
// directives can be confined to it without yaml importing this package
// (which would create an import cycle).
yaml.TemplateBaseDirProvider = func() string {
return DefaultConfig.GetTemplateDir()
}
}
// Add Default Config adds default when .templates-config.json file is not present
func applyDefaultConfig() {
DefaultConfig.TemplatesDirectory = filepath.Join(DefaultConfig.homeDir, NucleiTemplatesDirName)
// updates all necessary paths
DefaultConfig.SetTemplatesDir(DefaultConfig.TemplatesDirectory)
// InitializationError returns a templates state or path error detected during
// package initialization.
func (c *Config) InitializationError() error {
return c.initializationErr
}
func (c *Config) applyDefaultConfig() error {
dataHome := c.dataHome
if dataHome == "" {
dataHome = xdg.DataHome
}
dataDirs := c.dataDirs
if dataDirs == nil {
dataDirs = xdg.DataDirs
}
root, err := selectXDGTemplateRoot(dataHome, dataDirs)
if err != nil {
return err
}
rootChanged := filepath.Clean(c.TemplatesDirectory) != filepath.Clean(root)
c.setTemplatesDir(root)
if rootChanged {
c.TemplateVersion = ""
}
return nil
}
+83
View File
@@ -1,6 +1,10 @@
package config
import (
"crypto/md5"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
)
@@ -46,3 +50,82 @@ func TestIsCustomTemplateUsesPathBoundaries(t *testing.T) {
})
}
}
func TestTemplateRootLifecycleIgnoresLegacyDiscoverySource(t *testing.T) {
dir := t.TempDir()
root := filepath.Join(dir, "system", BinaryName, NucleiTemplatesDirName)
stateDir := filepath.Join(dir, "state")
state, err := json.Marshal(map[string]string{
"nuclei-templates-directory": root,
"nuclei-templates-directory-source": "xdg-system",
"nuclei-templates-version": "v1.0.0",
})
if err != nil {
t.Fatalf("marshal templates state: %v", err)
}
writeTestFile(t, filepath.Join(stateDir, TemplatesStateFileName), state, 0o600)
cfg := &Config{stateDir: stateDir, configDir: filepath.Join(dir, "config")}
if err := cfg.ReadTemplatesConfig(); err != nil {
t.Fatalf("read templates state: %v", err)
}
cfg.LatestNucleiTemplatesVersion = "v99.0.0"
if !cfg.NeedsTemplateUpdate() {
t.Fatal("discovered root did not request an available template update")
}
if !cfg.IgnoreFileNeedsUpdate("changed") {
t.Fatal("discovered root did not request a missing ignore file")
}
if err := cfg.WriteActiveIgnoreFile([]byte("tags: []\n")); err != nil {
t.Fatalf("write discovered-root ignore file: %v", err)
}
if err := cfg.WriteTemplatesIndex(map[string]string{"id": "path"}); err != nil {
t.Fatalf("write discovered-root template index: %v", err)
}
if err := cfg.SetTemplatesVersion("v2.0.0"); err != nil {
t.Fatalf("set discovered-root template version: %v", err)
}
for _, customDir := range cfg.GetAllCustomTemplateDirs() {
if filepath.Dir(customDir) != root {
t.Fatalf("custom directory %q is not under active root %q", customDir, root)
}
}
}
func TestNeedsIgnoreFileUpdateUsesActiveFile(t *testing.T) {
root := t.TempDir()
cfg := &Config{}
cfg.SetTemplatesDir(root)
contents := []byte("tags: [weak]\n")
latestHash := fmt.Sprintf("%x", md5.Sum(contents))
// The compatibility field must not hide a missing active file.
cfg.NucleiIgnoreHash = latestHash
if !cfg.IgnoreFileNeedsUpdate(latestHash) {
t.Fatal("missing active ignore file did not require an update")
}
if err := os.WriteFile(cfg.GetActiveIgnoreFilePath(), contents, 0o600); err != nil {
t.Fatalf("write active ignore file: %v", err)
}
// The compatibility field must not override the active file contents.
cfg.NucleiIgnoreHash = "stale"
if cfg.IgnoreFileNeedsUpdate(latestHash) {
t.Fatal("matching active ignore file required an update")
}
if err := os.WriteFile(cfg.GetActiveIgnoreFilePath(), []byte("tags: []\n"), 0o600); err != nil {
t.Fatalf("replace active ignore file: %v", err)
}
cfg.NucleiIgnoreHash = latestHash
if !cfg.IgnoreFileNeedsUpdate(latestHash) {
t.Fatal("modified active ignore file did not require an update")
}
if cfg.IgnoreFileNeedsUpdate("") {
t.Fatal("valid active ignore file required an update without a remote hash")
}
}
+73
View File
@@ -0,0 +1,73 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/adrg/xdg"
)
func defaultConfigDir() string {
return filepath.Join(xdg.ConfigHome, BinaryName)
}
func defaultStateDir() string {
return filepath.Join(xdg.StateHome, BinaryName)
}
func defaultCacheDir() string {
return filepath.Join(xdg.CacheHome, BinaryName)
}
func selectXDGTemplateRoot(dataHome string, dataDirs []string) (string, error) {
if !filepath.IsAbs(dataHome) {
return "", fmt.Errorf("XDG data home %q is not absolute", dataHome)
}
userRoot := filepath.Join(dataHome, BinaryName, NucleiTemplatesDirName)
exists, err := directoryExists(userRoot)
if err != nil {
return "", err
}
if exists {
return userRoot, nil
}
for _, dataDir := range dataDirs {
if !filepath.IsAbs(dataDir) {
continue
}
root := filepath.Join(dataDir, BinaryName, NucleiTemplatesDirName)
exists, err := directoryExists(root)
if err != nil {
return "", err
}
if exists {
return root, nil
}
}
return userRoot, nil
}
func directoryExists(path string) (bool, error) {
info, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("inspect directory %q: %w", path, err)
}
if !info.IsDir() {
return false, fmt.Errorf("path %q is not a directory", path)
}
return true, nil
}
+84
View File
@@ -0,0 +1,84 @@
package config
import (
"os"
"path/filepath"
"testing"
"github.com/adrg/xdg"
)
func TestXDGConfigPaths(t *testing.T) {
if got, want := defaultConfigDir(), filepath.Join(xdg.ConfigHome, BinaryName); got != want {
t.Fatalf("default config directory = %q, want %q", got, want)
}
if got, want := (&Config{}).GetCacheDir(), filepath.Join(xdg.CacheHome, BinaryName); got != want {
t.Fatalf("cache directory = %q, want %q", got, want)
}
if got, want := (&Config{}).GetStateDir(), filepath.Join(xdg.StateHome, BinaryName); got != want {
t.Fatalf("state directory = %q, want %q", got, want)
}
}
func TestSelectXDGTemplateRoot(t *testing.T) {
t.Run("user data wins", func(t *testing.T) {
dir := t.TempDir()
dataHome := filepath.Join(dir, "user")
userRoot := filepath.Join(dataHome, BinaryName, NucleiTemplatesDirName)
if err := os.MkdirAll(userRoot, 0o700); err != nil {
t.Fatalf("create user root: %v", err)
}
systemRoot := filepath.Join(dir, "system", BinaryName, NucleiTemplatesDirName)
if err := os.MkdirAll(systemRoot, 0o755); err != nil {
t.Fatalf("create system root: %v", err)
}
root, err := selectXDGTemplateRoot(dataHome, []string{filepath.Join(dir, "system")})
if err != nil {
t.Fatalf("select root: %v", err)
}
if root != userRoot {
t.Fatalf("root = %q, want %q", root, userRoot)
}
})
t.Run("first system directory wins", func(t *testing.T) {
dir := t.TempDir()
firstDir := filepath.Join(dir, "first")
secondDir := filepath.Join(dir, "second")
for _, base := range []string{firstDir, secondDir} {
if err := os.MkdirAll(filepath.Join(base, BinaryName, NucleiTemplatesDirName), 0o755); err != nil {
t.Fatalf("create system root: %v", err)
}
}
root, err := selectXDGTemplateRoot(filepath.Join(dir, "user"), []string{firstDir, secondDir})
if err != nil {
t.Fatalf("select root: %v", err)
}
want := filepath.Join(firstDir, BinaryName, NucleiTemplatesDirName)
if root != want {
t.Fatalf("root = %q, want %q", root, want)
}
})
t.Run("missing roots select user installation target", func(t *testing.T) {
dataHome := filepath.Join(t.TempDir(), "user")
root, err := selectXDGTemplateRoot(dataHome, nil)
if err != nil {
t.Fatalf("select root: %v", err)
}
want := filepath.Join(dataHome, BinaryName, NucleiTemplatesDirName)
if root != want {
t.Fatalf("root = %q, want %q", root, want)
}
})
t.Run("relative user data home is rejected", func(t *testing.T) {
if _, err := selectXDGTemplateRoot("relative", nil); err == nil {
t.Fatal("relative XDG data home was accepted")
}
})
}
+142
View File
@@ -0,0 +1,142 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
type templatesState struct {
TemplatesDirectory string `json:"nuclei-templates-directory,omitempty"`
TemplateVersion string `json:"nuclei-templates-version,omitempty"`
LatestNucleiVersion string `json:"nuclei-latest-version"`
LatestNucleiTemplatesVersion string `json:"nuclei-templates-latest-version"`
}
// ReadTemplatesConfig reads templates state, migrating the legacy config file
// only when the new state file does not exist.
func (c *Config) ReadTemplatesConfig() error {
statePath := c.getTemplatesConfigFilePath()
data, err := os.ReadFile(statePath)
migrate := false
if errors.Is(err, os.ErrNotExist) {
legacyPath := c.getLegacyTemplatesConfigFilePath()
data, err = os.ReadFile(legacyPath)
if err != nil {
return fmt.Errorf("read templates state %q or legacy state %q: %w", statePath, legacyPath, err)
}
migrate = true
} else if err != nil {
return fmt.Errorf("read templates state %q: %w", statePath, err)
}
var state templatesState
if err := json.Unmarshal(data, &state); err != nil {
return fmt.Errorf("decode templates state %q: %w", statePath, err)
}
if state.TemplatesDirectory == "" {
return fmt.Errorf("decode templates state %q: templates directory is empty", statePath)
}
c.setTemplatesDir(state.TemplatesDirectory)
c.TemplateVersion = state.TemplateVersion
c.LatestNucleiVersion = state.LatestNucleiVersion
c.LatestNucleiTemplatesVersion = state.LatestNucleiTemplatesVersion
if migrate {
if err := c.WriteTemplatesConfig(); err != nil {
return fmt.Errorf("migrate templates state: %w", err)
}
}
return nil
}
// WriteTemplatesConfig atomically writes restart-persistent templates state.
func (c *Config) WriteTemplatesConfig() error {
state := templatesState{
TemplatesDirectory: c.TemplatesDirectory,
TemplateVersion: c.TemplateVersion,
LatestNucleiVersion: c.LatestNucleiVersion,
LatestNucleiTemplatesVersion: c.LatestNucleiTemplatesVersion,
}
data, err := json.Marshal(&state)
if err != nil {
return fmt.Errorf("encode templates state: %w", err)
}
if err := atomicWriteFile(c.getTemplatesConfigFilePath(), data, 0o600); err != nil {
return fmt.Errorf("write templates state: %w", err)
}
return nil
}
func (c *Config) getTemplatesConfigFilePath() string {
return filepath.Join(c.GetStateDir(), TemplatesStateFileName)
}
// GetTemplatesStateFilePath returns the restart-persistent templates state
// file path.
func (c *Config) GetTemplatesStateFilePath() string {
return c.getTemplatesConfigFilePath()
}
// TODO(dwisiswant0): remove this in the future, only used for legacy migration.
func (c *Config) getLegacyTemplatesConfigFilePath() string {
return filepath.Join(c.configDir, legacyTemplatesConfigFileName)
}
func atomicWriteFile(path string, data []byte, mode os.FileMode) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("create directory %q: %w", dir, err)
}
temp, err := os.CreateTemp(dir, "."+filepath.Base(path)+"-*")
if err != nil {
return fmt.Errorf("create temporary file for %q: %w", path, err)
}
tempPath := temp.Name()
defer func() {
if temp != nil {
_ = temp.Close()
}
_ = os.Remove(tempPath)
}()
if err := temp.Chmod(mode); err != nil {
return fmt.Errorf("set temporary file mode for %q: %w", path, err)
}
if _, err := temp.Write(data); err != nil {
return fmt.Errorf("write temporary file for %q: %w", path, err)
}
if err := temp.Sync(); err != nil {
return fmt.Errorf("sync temporary file for %q: %w", path, err)
}
if err := temp.Close(); err != nil {
return fmt.Errorf("close temporary file for %q: %w", path, err)
}
temp = nil
if err := os.Rename(tempPath, path); err != nil {
return fmt.Errorf("replace %q: %w", path, err)
}
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package config
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestReadTemplatesConfigMigratesLegacyState(t *testing.T) {
dir := t.TempDir()
configDir := filepath.Join(dir, "config")
stateDir := filepath.Join(dir, "state")
// Keep a backslash in the path so JSON escaping is exercised on all platforms.
legacyRoot := filepath.Join(dir, `legacy\templates`)
legacyState, err := json.Marshal(map[string]string{
"nuclei-templates-directory": legacyRoot,
"nuclei-templates-version": "v1.2.3",
"nuclei-ignore-hash": "legacy-local-hash",
"nuclei-latest-ignore-hash": "legacy-remote-hash",
})
if err != nil {
t.Fatalf("marshal legacy state: %v", err)
}
writeTestFile(t, filepath.Join(configDir, legacyTemplatesConfigFileName), legacyState, 0o600)
cfg := &Config{configDir: configDir, stateDir: stateDir}
if err := cfg.ReadTemplatesConfig(); err != nil {
t.Fatalf("read templates state: %v", err)
}
if cfg.TemplatesDirectory != legacyRoot {
t.Fatalf("root = %q, want %q", cfg.TemplatesDirectory, legacyRoot)
}
if cfg.TemplateVersion != "v1.2.3" {
t.Fatalf("template version = %q, want v1.2.3", cfg.TemplateVersion)
}
if cfg.NucleiIgnoreHash != "" {
t.Fatalf("local ignore hash = %q, want empty", cfg.NucleiIgnoreHash)
}
if cfg.LatestNucleiIgnoreHash != "" {
t.Fatalf("remote ignore hash = %q, want empty", cfg.LatestNucleiIgnoreHash)
}
cfg.NucleiIgnoreHash = "current-local-hash"
cfg.LatestNucleiIgnoreHash = "current-remote-hash"
if err := cfg.WriteTemplatesConfig(); err != nil {
t.Fatalf("rewrite templates state: %v", err)
}
statePath := filepath.Join(stateDir, TemplatesStateFileName)
info, err := os.Stat(statePath)
if err != nil {
t.Fatalf("stat migrated state: %v", err)
}
if got := info.Mode().Perm(); runtime.GOOS != "windows" && got != 0o600 {
t.Fatalf("state mode = %o, want 600", got)
}
state, err := os.ReadFile(statePath)
if err != nil {
t.Fatalf("read migrated state: %v", err)
}
if strings.Contains(string(state), "nuclei-ignore-hash") {
t.Fatalf("migrated state persisted local ignore hash: %s", state)
}
if strings.Contains(string(state), "nuclei-latest-ignore-hash") {
t.Fatalf("migrated state persisted remote ignore hash: %s", state)
}
if _, err := os.Stat(filepath.Join(configDir, legacyTemplatesConfigFileName)); err != nil {
t.Fatalf("legacy state was removed: %v", err)
}
}
func TestReadTemplatesConfigDoesNotHideCorruptState(t *testing.T) {
dir := t.TempDir()
configDir := filepath.Join(dir, "config")
stateDir := filepath.Join(dir, "state")
writeTestFile(t, filepath.Join(configDir, legacyTemplatesConfigFileName), []byte(`{"nuclei-templates-directory":"legacy"}`), 0o600)
writeTestFile(t, filepath.Join(stateDir, TemplatesStateFileName), []byte(`{"nuclei-templates-directory":`), 0o600)
cfg := &Config{configDir: configDir, stateDir: stateDir}
err := cfg.ReadTemplatesConfig()
if err == nil {
t.Fatal("expected corrupt new state to fail")
}
if errors.Is(err, os.ErrNotExist) {
t.Fatalf("corrupt state reported as missing: %v", err)
}
if cfg.TemplatesDirectory == "legacy" {
t.Fatal("corrupt new state fell back to legacy state")
}
}
func TestReadTemplatesConfigIgnoresLegacyRootSource(t *testing.T) {
dir := t.TempDir()
stateDir := filepath.Join(dir, "state")
root := filepath.Join(dir, "system", BinaryName, NucleiTemplatesDirName)
state, err := json.Marshal(map[string]string{
"nuclei-templates-directory": root,
"nuclei-templates-directory-source": "xdg-system",
"nuclei-templates-version": "v1.2.3",
})
if err != nil {
t.Fatalf("marshal templates state: %v", err)
}
statePath := filepath.Join(stateDir, TemplatesStateFileName)
writeTestFile(t, statePath, state, 0o600)
cfg := &Config{stateDir: stateDir, configDir: filepath.Join(dir, "config")}
if err := cfg.ReadTemplatesConfig(); err != nil {
t.Fatalf("read templates state: %v", err)
}
if cfg.TemplatesDirectory != root {
t.Fatalf("root = %q, want %q", cfg.TemplatesDirectory, root)
}
if cfg.TemplateVersion != "v1.2.3" {
t.Fatalf("template version = %q, want v1.2.3", cfg.TemplateVersion)
}
if err := cfg.WriteTemplatesConfig(); err != nil {
t.Fatalf("rewrite templates state: %v", err)
}
rewritten, err := os.ReadFile(statePath)
if err != nil {
t.Fatalf("read rewritten templates state: %v", err)
}
if strings.Contains(string(rewritten), "nuclei-templates-directory-source") {
t.Fatalf("rewritten state retained legacy root source: %s", rewritten)
}
}
func writeTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("create test directory: %v", err)
}
if err := os.WriteFile(path, data, mode); err != nil {
t.Fatalf("write test file: %v", err)
}
}
+1 -2
View File
@@ -101,8 +101,7 @@ func IsTemplateWithRoot(fpath, rootDir string) bool {
// Only check components if pathToCheck is NOT absolute
// This avoids false positives on parent directories for absolute paths
if !filepath.IsAbs(pathToCheck) {
parts := strings.Split(pathToCheck, string(os.PathSeparator))
for _, p := range parts {
for p := range strings.SplitSeq(pathToCheck, string(os.PathSeparator)) {
for _, excluded := range knownMiscDirectories {
if strings.EqualFold(p, excluded) {
return false
+12 -15
View File
@@ -15,9 +15,10 @@ import (
// Inclusion fields (e.g., Authors, Tags, IDs, Severities, ProtocolTypes) use
// AND logic across different filter types and OR logic within each type.
// Exclusion fields (e.g., ExcludeTags, ExcludeIDs, ExcludeSeverities,
// ExcludeProtocolTypes) take precedence over inclusion fields. Additionally,
// IncludeTemplates and IncludeTags can force inclusion of templates even if
// they match exclusion criteria.
// ExcludeProtocolTypes) take precedence over normal inclusion fields.
// IncludeTags can force inclusion only for the same excluded tag; other
// exclusion criteria and unrelated excluded tags still apply. IncludeTemplates
// remains an explicit template override.
type Filter struct {
// once ensures that IDs and ExcludedIDs are compiled the first time Matches is called.
once sync.Once
@@ -31,7 +32,7 @@ type Filter struct {
// ExcludeTags to exclude (takes precedence over Tags).
ExcludeTags []string
// IncludeTags to force include even if excluded.
// IncludeTags to force include matching tag exclusions.
IncludeTags []string
// IDs to include (supports wildcards).
@@ -75,7 +76,7 @@ type Filter struct {
func (f *Filter) Matches(m *Metadata) bool {
f.once.Do(f.Compile)
if f.isForcedInclude(m) {
if f.matchesIncludeTemplate(m) {
return true
}
@@ -90,8 +91,8 @@ func (f *Filter) Matches(m *Metadata) bool {
return true
}
// isForcedInclude checks if template is forced to be included.
func (f *Filter) isForcedInclude(m *Metadata) bool {
// matchesIncludeTemplate checks if template is explicitly included by path.
func (f *Filter) matchesIncludeTemplate(m *Metadata) bool {
if len(f.IncludeTemplates) > 0 {
for _, includePath := range f.IncludeTemplates {
if matchesPath(m.FilePath, includePath) {
@@ -100,12 +101,6 @@ func (f *Filter) isForcedInclude(m *Metadata) bool {
}
}
if len(f.IncludeTags) > 0 {
if slices.ContainsFunc(f.IncludeTags, m.HasTag) {
return true
}
}
return false
}
@@ -120,8 +115,10 @@ func (f *Filter) isExcluded(m *Metadata) bool {
}
if len(f.ExcludeTags) > 0 {
if slices.ContainsFunc(f.ExcludeTags, m.HasTag) {
return true
for _, excludeTag := range f.ExcludeTags {
if m.HasTag(excludeTag) && !slices.Contains(f.IncludeTags, excludeTag) {
return true
}
}
}
+101 -2
View File
@@ -58,12 +58,111 @@ func TestFilterMatches(t *testing.T) {
require.False(t, filter.Matches(metadata))
})
t.Run("Include tags overrides exclude", func(t *testing.T) {
t.Run("Include tags overrides same exclude tag", func(t *testing.T) {
filter := &Filter{
ExcludeTags: []string{"cve"},
IncludeTags: []string{"cve"},
}
require.True(t, filter.Matches(metadata))
})
t.Run("Include tags does not override unrelated exclude tags", func(t *testing.T) {
filter := &Filter{
ExcludeTags: []string{"rce"},
IncludeTags: []string{"cve"},
}
require.True(t, filter.Matches(metadata))
require.False(t, filter.Matches(metadata))
})
t.Run("Include tags does not override other exclusions", func(t *testing.T) {
tests := []struct {
name string
filter *Filter
}{
{
name: "exclude id",
filter: &Filter{
ExcludeIDs: []string{"test-template-1"},
IncludeTags: []string{"cve"},
},
},
{
name: "exclude template",
filter: &Filter{
ExcludeTemplates: []string{"/templates/cves/"},
IncludeTags: []string{"cve"},
},
},
{
name: "exclude severity",
filter: &Filter{
ExcludeSeverities: []severity.Severity{severity.Critical},
IncludeTags: []string{"cve"},
},
},
{
name: "exclude protocol",
filter: &Filter{
ExcludeProtocolTypes: []types.ProtocolType{types.HTTPProtocol},
IncludeTags: []string{"cve"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.False(t, tt.filter.Matches(metadata))
})
}
})
t.Run("Include tags does not override normal includes", func(t *testing.T) {
tests := []struct {
name string
filter *Filter
}{
{
name: "author",
filter: &Filter{
Authors: []string{"unknown"},
IncludeTags: []string{"cve"},
},
},
{
name: "tag",
filter: &Filter{
Tags: []string{"xss"},
IncludeTags: []string{"cve"},
},
},
{
name: "id",
filter: &Filter{
IDs: []string{"other-template"},
IncludeTags: []string{"cve"},
},
},
{
name: "severity",
filter: &Filter{
Severities: []severity.Severity{severity.High},
IncludeTags: []string{"cve"},
},
},
{
name: "protocol",
filter: &Filter{
ProtocolTypes: []types.ProtocolType{types.DNSProtocol},
IncludeTags: []string{"cve"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.False(t, tt.filter.Matches(metadata))
})
}
})
t.Run("ID filter - exact match", func(t *testing.T) {
+58 -25
View File
@@ -2,6 +2,7 @@ package index
import (
"encoding/gob"
"errors"
"maps"
"os"
"path/filepath"
@@ -10,7 +11,6 @@ import (
"github.com/maypok86/otter/v2"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
folderutil "github.com/projectdiscovery/utils/folder"
)
const (
@@ -19,7 +19,7 @@ const (
// IndexVersion is the schema version for cache invalidation on breaking
// changes.
IndexVersion = 1
IndexVersion = 2
// DefaultMaxSize is the default maximum number of templates to cache.
DefaultMaxSize = 50000
@@ -34,6 +34,7 @@ type Index struct {
cacheFile string
mu sync.RWMutex
version int
dirty bool
}
// cacheSnapshot represents the serialized cache structure.
@@ -45,10 +46,10 @@ type cacheSnapshot struct {
// NewIndex creates a new template metadata cache with the given options.
func NewIndex(cacheDir string) (*Index, error) {
if cacheDir == "" {
cacheDir = folderutil.AppCacheDirOrDefault(".nuclei-cache", config.BinaryName)
cacheDir = config.DefaultConfig.GetCacheDir()
}
if err := os.MkdirAll(cacheDir, 0755); err != nil {
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
return nil, err
}
@@ -70,6 +71,8 @@ func NewIndex(cacheDir string) (*Index, error) {
weight += len(value.Severity)
weight += len(value.ProtocolType)
weight += len(value.TemplateVerifier)
weight += len(value.VerifierFingerprint)
weight += len(value.ContentDigest)
for _, author := range value.Authors {
weight += len(author)
@@ -105,20 +108,26 @@ func NewDefaultIndex() (*Index, error) {
// Get retrieves metadata for a template path, validating freshness via mtime.
func (i *Index) Get(path string) (*Metadata, bool) {
i.mu.RLock()
defer i.mu.RUnlock()
metadata, found := i.cache.GetIfPresent(path)
i.mu.RUnlock()
if !found {
return nil, false
}
if !metadata.IsValid() {
go i.Delete(path)
return nil, false
if metadata.IsValid() {
return metadata.clone(), true
}
return metadata, true
i.mu.Lock()
current, found := i.cache.GetIfPresent(path)
if found && current == metadata {
i.cache.Invalidate(path)
i.dirty = true
}
i.mu.Unlock()
return nil, false
}
// Set stores metadata for a template path.
@@ -132,7 +141,10 @@ func (i *Index) Set(path string, metadata *Metadata) (*Metadata, bool) {
i.mu.Lock()
defer i.mu.Unlock()
return i.cache.Set(path, metadata)
cached, ok := i.cache.Set(path, metadata.clone())
i.dirty = true
return cached.clone(), ok
}
// SetFromTemplate extracts metadata from a parsed template and stores it.
@@ -171,7 +183,12 @@ func (i *Index) Delete(path string) {
i.mu.Lock()
defer i.mu.Unlock()
if _, found := i.cache.GetIfPresent(path); !found {
return
}
i.cache.Invalidate(path)
i.dirty = true
}
// Size returns the number of cached entries.
@@ -188,26 +205,36 @@ func (i *Index) Clear() {
defer i.mu.Unlock()
i.cache.InvalidateAll()
i.dirty = true
}
// Save persists the cache to disk using gob encoding.
func (i *Index) Save() error {
i.mu.RLock()
defer i.mu.RUnlock()
i.mu.Lock()
defer i.mu.Unlock()
if !i.dirty {
return nil
}
snapshot := &cacheSnapshot{
Version: i.version,
Data: make(map[string]*Metadata),
Data: make(map[string]*Metadata, i.cache.EstimatedSize()),
}
maps.Insert(snapshot.Data, i.cache.All())
// NOTE(dwisiswant0): write to temp for atomic op.
tmpFile := i.cacheFile + ".tmp"
file, err := os.Create(tmpFile)
file, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
if err := file.Chmod(0o600); err != nil {
_ = file.Close()
_ = os.Remove(tmpFile)
return err
}
encoder := gob.NewEncoder(file)
if err := encoder.Encode(snapshot); err != nil {
@@ -229,11 +256,20 @@ func (i *Index) Save() error {
return err
}
i.dirty = false
return nil
}
// Load loads the cache from disk using gob decoding.
func (i *Index) Load() error {
i.mu.Lock()
defer i.mu.Unlock()
if i.dirty {
return errors.New("cannot load metadata cache into a modified index")
}
file, err := os.Open(i.cacheFile)
if err != nil {
if os.IsNotExist(err) {
@@ -261,9 +297,6 @@ func (i *Index) Load() error {
return nil
}
i.mu.Lock()
defer i.mu.Unlock()
for key, value := range snapshot.Data {
i.cache.Set(key, value)
}
@@ -296,11 +329,8 @@ func (i *Index) FilterFunc(fn FilterFunc) []string {
return i.All()
}
i.mu.RLock()
defer i.mu.RUnlock()
var matched []string
for path, metadata := range i.cache.All() {
for path, metadata := range i.GetAll() {
if fn(metadata) {
matched = append(matched, path)
}
@@ -327,7 +357,10 @@ func (i *Index) GetAll() map[string]*Metadata {
i.mu.RLock()
defer i.mu.RUnlock()
result := maps.Collect(i.cache.All())
result := make(map[string]*Metadata, i.cache.EstimatedSize())
for path, metadata := range i.cache.All() {
result[path] = metadata.clone()
}
return result
}
Loaded 100 of 281 files, more files were not shown because too many files have changed in this diff. Show more