Compare commits

...
Author SHA1 Message Date
Mzack9999 e5e8137d4e Fix lint 2026-10-26 17:28:55 +01:00
Mzack9999 0480998978 SDK options 2026-10-26 12:40:00 +01:00
Mzack9999 2ee294c50c Scope filters 2026-10-26 12:40:00 +01:00
Mzack9999 bbdfcc39c6 Share inflight 2026-10-26 12:40:00 +01:00
Mzack9999 539eb534c5 Merge dev 2026-10-26 12:20:39 +01:00
Mzack9999 4eb4697807 Align fields 2026-10-26 11:10:37 +01:00
Mzack9999 19e1f05f09 Align cache 2026-10-26 10:59:16 +01:00
Mzack9999 06b120a7aa Share cache 2026-10-26 10:38:13 +01:00
Mzack9999 ae084e64f6 Group flags 2026-10-26 08:21:39 +01:00
Mzack9999 399f6c97bf Cache bodies 2026-10-26 08:21:38 +01:00
Mzack9999 a8a2c939ce Fix review 2026-10-26 08:02:31 +01:00
Mzack9999 74e647e747 Merge dev 2026-10-26 07:56:40 +01:00
Mzack9999 263bc13412 Merge pull request #7941 from TassioSales/fix/passive-raw-response-http-body
fix(offlinehttp): handle request body and unversioned HTTP/2 in passive mode (#7924)
2026-10-26 07:35:25 +01:00
dependabot[bot] 660b7438d8 chore(deps): bump crate-ci/typos (#7943)
Bumps the workflows group with 1 update: [crate-ci/typos](https://github.com/crate-ci/typos).


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

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.51.1
  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-10-26 06:27:48 +00:00
TassioSales aa761b9c33 fix(offlinehttp): handle request body and unversioned HTTP/2 in passive mode (#7924)
Signed-off-by: TassioSales <[email protected]>
2026-10-25 18:20:30 -03:00
Mzack9999 731d438319 Merge pull request #7690 from IgorDaniel45/fix-threadsafe-compiled-template-cache
fix: reuse compiled templates in thread-safe engine
2026-10-25 22:09:59 +01:00
Mzack9999 a85a545323 Merge pull request #7655 from hu-matin/translation-fa
docs: add Persian(FA) translation for README
2026-10-25 22:02:43 +01:00
Mzack9999 5ea11e5f43 Merge pull request #7784 from root-Manas/feat/openapi-oauth-security
feat(openapi): support OAuth2 and OpenID Connect access tokens
2026-10-25 22:02:38 +01:00
Mzack9999 230a17d4fa Merge pull request #7680 from chauhand2463/feat/http-tls-key-exchange
feat (http) : expose TLS key exchange metadata
2026-10-25 22:02:33 +01:00
Mzack9999 d2499b74da trim cache 2026-10-25 21:46:31 +01:00
Mzack9999 93483eaff9 merge dev 2026-10-25 21:46:30 +01:00
Mzack9999 a81624e595 fix readme 2026-10-25 21:20:18 +01:00
Mzack9999 a9df3eff9e merge dev 2026-10-25 21:19:34 +01:00
Mzack9999 08820abff6 merge dev 2026-10-25 21:18:56 +01:00
Mzack9999 fa3fa92092 merge dev 2026-10-25 21:15:30 +01:00
Mzack9999 be7b2648ef document ssl 2026-10-25 21:15:30 +01:00
Mzack9999 47398198a0 Merge pull request #7770 from hodeswildsmith455-boop/orcarouter/task-7500
Select OrcaRouter as an LLM provider
2026-10-25 19:29:59 +01:00
Mzack9999 f846a5553d Merge pull request #7863 from mazzz3r/feat/target-line-ip
feat(input): pin a target to an ip with the ip key of target lines
2026-10-25 18:56:19 +01:00
Mzack9999 39c5d8764e Merge pull request #7928 from projectdiscovery/fix/http-large-request-body-memory
http: stop copying large request bodies (fixes OOM on multi-MB body templates)
2026-10-25 18:54:34 +01:00
Mzack9999 93b64043b1 merge dev 2026-10-25 18:27:19 +01:00
Mzack9999 7d6b2fef6f bump utils 2026-10-25 18:27:03 +01:00
Mzack9999 445e6ab972 Merge pull request #7753 from gnuletik/perf/lazy-full-response
perf(http): build the full response only when an operator reads it
2026-10-25 18:24:42 +01:00
Mzack9999 7f8abe2c0f Merge pull request #7678 from gnuletik/fix/cancellable-template-and-js-waits
fix(core): honour the scan context when acquiring worker slots and JS runtimes
2026-10-25 18:20:28 +01:00
Mzack9999 bfd8d4585c slim provider 2026-10-25 18:18:28 +01:00
Mzack9999 1c3cc97748 merge dev 2026-10-25 18:17:48 +01:00
Georgii Butakov 947a2a9a2a fix(http): dial pipelined requests through the pinned ip
executeTurboHTTP dialed pipelined connections with context.Background(),
so a pipelined request of a pinned target resolved the host instead of
connecting to its ip. It now dials with the input context, which carries
the pin. protocols/http/target-line-ip-pipeline.yaml runs the pinned
target test with pipeline: true, and gets no result without the change.

ProbeKey joins the input and the ip with a NUL byte, which no input
holds, instead of "@", which a scheme-less input may hold itself.
2026-10-25 17:12:13 +00:00
Mzack9999 8e1e613a15 merge dev 2026-10-25 18:10:14 +01:00
Mzack9999 5a339a8da1 fix lint 2026-10-25 17:56:27 +01:00
Georgii Butakov 779868985d fix(input): do not give a pinned input the probe result of its host
A pinned input whose ip had no probe result fell back to Transform,
which takes the result probed for the input without an ip, so the
pinned target could get the scheme of another server. It now gets the
conversion of an input that was not probed.
2026-10-25 16:50:30 +00:00
Mzack9999 c4d933d165 merge dev 2026-10-25 17:50:20 +01:00
Mzack9999 f58b21e6c3 merge dev 2026-10-25 17:45:44 +01:00
Mzack9999 71805f5093 Merge pull request #7857 from AdamMagued/fix-issue-7829
perf(protocols/http): defer building FullResponseString when unused by operators
2026-10-25 17:33:54 +01:00
Mzack9999 1086d5d391 keep matchers 2026-10-25 17:32:15 +01:00
Mzack9999 75434a01e9 Merge pull request #7747 from MGreif/mgr-interact-client-interface
Improvement: implementation of interface for interactsh
2026-10-25 17:24:11 +01:00
Mzack9999 e9f062f600 Merge pull request #7861 from mazzz3r/feat/task-based-progress
feat(progress): report completion by executed templates
2026-10-25 17:19:28 +01:00
root-Manas 5bd7c79be4 test(openapi): check request body cleanup errors 2026-10-25 21:45:04 +05:30
Mzack9999 8f31e9d24d Merge pull request #7612 from DPS0340/fix/regex-group-out-of-range
fix(extractors): reject a regex group the pattern cannot produce
2026-10-25 17:10:25 +01:00
Mzack9999 e17273911b Merge pull request #7914 from mazzz3r/feat/payload-random-order
feat(payloads): iterate payload combinations in a random order
2026-10-25 17:10:19 +01:00
Mzack9999 e2840f3644 fix lint 2026-10-25 17:09:07 +01:00
Georgii Butakov be6e32d5ae fix(runner): keep the http probe result of each pinned ip
A target without a scheme is probed through its pinned ip, but the
result was stored under the target alone, so the last probe of a host
pinned to several ips set the scheme for all of them, and a server that
answered only on the other scheme was not scanned. The result is now
stored under the target and its ip, and TransformPinned looks it up for
a pinned input.
2026-10-25 16:05:09 +00:00
Mzack9999 86d9a9bd54 Merge pull request #7882 from NitinReddy-A/feat/5608-templates-update-channel
feat(installer): add templates update channel
2026-10-25 16:57:00 +01:00
Mzack9999 7c2b74424e Merge pull request #7618 from aviadavi/feat/dsl-in-custom-headers
feat: evaluate DSL helpers in -H values per request
2026-10-25 16:56:54 +01:00
Mzack9999 f1e79cd82b fix response 2026-10-25 16:54:03 +01:00
Mzack9999 4ae1faa64f merge dev 2026-10-25 16:53:53 +01:00
Mzack9999 834cff9fa2 Merge pull request #7786 from omlahore/fix/openapi-schemaless-bodies
fix(openapi): build request bodies from an example of the empty schema
2026-10-25 16:18:34 +01:00
Georgii Butakov 23f766eb61 feat(runner): refuse -replay-proxy with targets pinned to an ip
-replay-proxy re-sends the requests behind a finding through a proxy,
which resolves the host itself, so the replay of a pinned target would
not reach the pinned ip. The pinned-target checks move into
pinnedTargetConflict with a table test, and the input README lists
-replay-proxy next to -proxy and -automatic-scan.
2026-10-25 15:14:50 +00:00
Georgii Butakov 43f99eadde Merge branch 'dev' into feat/target-line-ip
dev requires fastdialer v0.6.0, which has WithResolveOverride and the
dial cache fix, so go.mod and go.sum take dev's versions and the
fastdialer replace is dropped.
2026-10-25 15:14:50 +00:00
Mzack9999 baaeb15022 merge dev 2026-10-25 16:05:21 +01:00
Mzack9999 d4b8a07e80 add scope 2026-10-25 15:57:30 +01:00
Mzack9999 319baa3152 merge dev 2026-10-25 15:51:39 +01:00
Mzack9999 e24855067e merge dev 2026-10-25 15:50:36 +01:00
Georgii Butakov a4c597fd31 Merge branch 'dev' into feat/task-based-progress
Resolved pkg/core/executors.go: kept the task accounting and took dev's
per-template host-errors cache key from #7859.
2026-10-25 14:50:25 +00:00
Mzack9999 87afa2fd4e Merge pull request #7926 from projectdiscovery/feat/7894-js-backoff
apply host backoff to javascript template dials
2026-10-25 15:46:57 +01:00
Mzack9999 b4c9cc10c9 pass options 2026-10-25 15:46:14 +01:00
Mzack9999 7f6cfd42f3 fix callback 2026-10-25 15:35:50 +01:00
Mzack9999 a03f13e929 merge dev 2026-10-25 15:35:50 +01:00
Mzack9999 02f5d9a6c3 merge dev 2026-10-25 15:32:51 +01:00
Mzack9999 f46ee55118 Merge pull request #7930 from projectdiscovery/compiled-cache-options
Keep template options on compiled-cache hits
2026-10-25 15:26:37 +01:00
Mzack9999 a9d358a56a Merge pull request #7929 from projectdiscovery/sdk-ignore-after-init
Read the nuclei ignore file after init updates it
2026-10-25 15:24:17 +01:00
Mzack9999 9545a9246c copy operators 2026-10-25 14:51:11 +01:00
Mzack9999 48e04c7317 fix ignore 2026-10-25 14:51:10 +01:00
Ice3manandClaude Opus 5.5 2954b81c85 http: stop copying large request bodies
Templates that send multi-MB bodies (e.g. CVE-2026-109618 sends 1, 8 and
20 MB) kept 8-10 copies of each body alive per request. With 25 hosts in
flight this reached ~10 GB RSS and OOM-killed scan VMs.

- add pkg/utils/httpbody: a zero-copy request body that rewinds on EOF;
  GetBody hands out new cursors over the same slice instead of cloning
- build raw, model and fuzzed request bodies with httpbody instead of
  retryablehttp's ReusableReadCloser, which tees every read into a second
  growing buffer and clones on each GetBody
- dump requests with http.Request.Write into a capped writer instead of
  retryablehttp Dump/httputil.DumpRequestOut; output is unchanged for
  bodies under the cap
- keep only the first 32 KiB of a body in request dumps (new
  -rqss/-request-size-save flag, 0 keeps all) and leave bodies over 4 KiB
  out of curl commands
- build the project cache key only when a project file is used, hashing
  the body when the dump is truncated
- replay sends the full body when the dump was truncated
- slice the raw request body from the request instead of reading it out
- pass replacer values to fasttemplate as []byte so writes don't copy them

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 18:48:04 +05:30
Mzack9999 052855717d Merge pull request #7679 from projectdiscovery/idle-conn-quarantine
Stop reusing hosts after idle leftovers
2026-10-25 14:09:42 +01:00
Mzack9999 e1f656033d fix idle 2026-10-25 13:39:50 +01:00
Mzack9999 9301048bc4 merge dev 2026-10-25 13:37:52 +01:00
Mzack9999 e21e636530 Merge pull request #7922 from MasterchiefGalaxy/fix/offline-http-preserve-response
fix(offlinehttp): preserve response content during HTTP version normalization
2026-10-25 00:59:31 +02:00
Mzack9999 6d433e6a6f Merge pull request #7855 from projectdiscovery/dependabot/go_modules/go_modules-e1bf5cf652
chore(deps): bump the go_modules group across 1 directory with 2 updates
2026-10-24 23:48:14 +02:00
ghost e8f1a788ac chore(js): update memoized functions 🤖 2026-10-24 21:45:37 +00:00
Mzack9999 f1702dc401 Merge pull request #7908 from gnuletik/fix/js-redis-reply-cap
fix(js): cap what a redis connection may read
2026-10-24 23:44:46 +02:00
Doğan Can Bakır 3ab0a66962 bump fastdialer to v0.6.0 2026-10-24 23:22:28 +03:00
Doğan Can Bakır bf53db167d apply host backoff to javascript template dials 2026-10-24 23:22:24 +03:00
Mzack9999 d13b58d987 fix lint 2026-10-24 18:19:06 +02:00
MasterchiefGalaxy 206446f567 fix(offlinehttp): preserve response content during version normalization 2026-10-23 21:46:03 -05:00
Mzack9999 b901fb8aea Merge pull request #7868 from TassioSales/fix/headless-ws-exclude-hosts
fix(headless): enforce network policy on ws and wss schemes (#7867)
2026-10-24 02:28:10 +02:00
Mzack9999 7e1a8f7da9 Merge pull request #7633 from prithvee07/dev
Keep unique markdown report names
2026-10-24 01:51:15 +02:00
Mzack9999 943ec350bb Merge pull request #7876 from TassioSales/fix/proxy-internal-env-vars
fix(runner): set HTTPS_PROXY and all proxy env vars when proxy-internal is enabled
2026-10-24 01:47:18 +02:00
Mzack9999 a54a6252a6 fix test 2026-10-24 01:45:56 +02:00
Mzack9999 131b7b5e2c fix filenames 2026-10-24 01:19:20 +02:00
Mzack9999 773ddf1411 merge dev 2026-10-24 01:17:29 +02:00
Mzack9999 665914747c fix lint 2026-10-24 01:11:55 +02:00
Mzack9999 bb06c6a470 Merge pull request #7812 from CalvinTjoaquinn/fix/raw-versionless-request-line
fix(raw): keep a path that contains "HTTP" in a versionless request line
2026-10-24 01:08:00 +02:00
Mzack9999 bdbdc1e8de fix lint 2026-10-24 01:07:20 +02:00
Mzack9999 68cc4b17cd guard proxy 2026-10-24 01:04:17 +02:00
Mzack9999 308044e234 merge dev 2026-10-24 01:01:45 +02:00
Mzack9999 64ad459151 Merge pull request #7756 from baturdenada/fix/dialer-panic
Fix dialer panics in JS protocol and input providers
2026-10-24 00:54:34 +02:00
Mzack9999 4e6f69cb97 merge dev 2026-10-24 00:53:47 +02:00
Mzack9999 cdf3798bb7 Merge pull request #7796 from comradezephyr/fix/requestresponse-json-roundtrip
fix(input): emit JSON objects in RequestResponse.MarshalJSON
2026-10-24 00:48:47 +02:00
Mzack9999 d2699653fd Merge pull request #7810 from TassioSales/fix/extractor-field-type-validation
fix(extractors): reject misapplied group and attribute fields on wrong extractor types (#7808)
2026-10-24 00:46:56 +02:00
Mzack9999 663c3528c7 Merge pull request #7802 from omlahore/fix/openapi-binary-content-type
fix(openapi): send format: binary octet-stream bodies as octet-stream
2026-10-24 00:45:31 +02:00
Mzack9999 9dbbe4c225 Merge pull request #7831 from m10x/dev
fix(installer): fail fast on unremovable templates update lock
2026-10-24 00:39:05 +02:00
Mzack9999 40e165a065 merge dev 2026-10-24 00:37:21 +02:00
Mzack9999 25c4b0d623 Merge pull request #7799 from CalvinTjoaquinn/fix/raw-header-without-colon
fix(raw): two defects in the raw header loop
2026-10-24 00:35:12 +02:00
Mzack9999 cb8434c81c Merge pull request #7828 from ruzfi/fix/http/ip-var-empty-behind-proxy
fix(http): set ip var from IP-literal target when dial history is empty
2026-10-24 00:28:52 +02:00
Mzack9999 e7a383124b merge dev 2026-10-24 00:27:32 +02:00
Mzack9999 9cd0fbb983 fix dialers 2026-10-24 00:25:03 +02:00
Mzack9999 1e8b85a133 drop duplicate 2026-10-24 00:18:52 +02:00
Mzack9999 c1b2553022 merge dev 2026-10-24 00:17:51 +02:00
Mzack9999 99c22b36f0 Merge pull request #7853 from NitinReddy-A/fix/7610-exact-config-basename
fix(catalog): match known config files by exact basename
2026-10-24 00:17:22 +02:00
Mzack9999 213b74b683 Merge pull request #7809 from sandiyochristan/fix/extractor-field-type-ownership
fix(extractors): reject fields the extractor type never reads
2026-10-24 00:13:55 +02:00
Mzack9999 ee9a2ed36d quote paths 2026-10-24 00:08:43 +02:00
Mzack9999 9fd430d803 merge dev 2026-10-24 00:05:23 +02:00
Mzack9999 20c1510069 merge dev 2026-10-24 00:03:13 +02:00
Mzack9999 6ab068a356 merge dev 2026-10-24 00:01:57 +02:00
Mzack9999 a4f855aeee Merge pull request #7850 from SamIntruder/fix/http-to-https-self-heal-on-detect
fix(http): self-heal HTTP-to-HTTPS requests immediately on detection
2026-10-24 00:01:17 +02:00
Mzack9999 30c6e1f0ca merge dev 2026-10-23 23:53:03 +02:00
Mzack9999 7448d2ab23 keep group 2026-10-23 23:49:32 +02:00
Mzack9999 833bd0102c merge dev 2026-10-23 23:48:57 +02:00
Mzack9999 cefccf97f3 merge dev 2026-10-23 23:43:57 +02:00
Mzack9999 b229dadf45 Merge pull request #7854 from TassioSales/fix/xpath-extractor-validation
fix(extractors): reject invalid xpath expressions during extractor compilation (#7750)
2026-10-23 23:43:06 +02:00
Mzack9999 332cfd5372 merge dev 2026-10-23 23:37:09 +02:00
Mzack9999 bad0e62459 fix lint 2026-10-23 23:34:38 +02:00
Mzack9999 cebb1a45b0 merge dev 2026-10-23 23:32:09 +02:00
Mzack9999 39ab5141a8 retry lock 2026-10-23 23:24:19 +02:00
Mzack9999 347b996d2c merge dev 2026-10-23 23:23:10 +02:00
Mzack9999 5dde4c2b30 retry https 2026-10-23 23:16:53 +02:00
Mzack9999 3a686c1ba5 Merge pull request #7877 from TassioSales/fix/honour-template-max-size
fix(http): honour template max-size under global response-size-read limit
2026-10-23 22:59:50 +02:00
Mzack9999 8c214de616 merge dev 2026-10-23 22:58:20 +02:00
Mzack9999 8081bcbee8 guard xpath 2026-10-23 22:52:44 +02:00
Mzack9999 913d4161d1 merge dev 2026-10-23 22:52:11 +02:00
Mzack9999 5c3c00f2fa guard limit 2026-10-23 22:31:36 +02:00
Mzack9999 ab5c51640e merge dev 2026-10-23 22:30:05 +02:00
Mzack9999 beccd36038 Merge pull request #7910 from gnuletik/fix/js-smtp-reply-cap
fix(js): cap what an smtp connection may read
2026-10-23 22:11:41 +02:00
Mzack9999 2b39243c73 merge dev 2026-10-23 19:32:40 +02:00
Mzack9999 bc53323223 Merge pull request #7906 from gnuletik/fix/js-ldap-smtp-close-on-cancel
fix(js): close ldap and smtp connections when the template ends
2026-10-23 19:31:25 +02:00
Mzack9999 0cfd312330 unstick timeout 2026-10-23 19:04:09 +02:00
Mzack9999 8be6ff6389 allow helo 2026-10-23 18:40:53 +02:00
Mzack9999 9ece5bc5f6 Merge pull request #7886 from TassioSales/fix/sdk-intermittent-zero-templates-7859
fix(sdk): prevent custom-port HTTP failures from poisoning base host cache and filter skip events
2026-10-23 18:36:14 +02:00
Mzack9999 d117fcada4 merge dev 2026-10-23 18:29:52 +02:00
Mzack9999 316de3dc43 cap lengths 2026-10-23 18:28:58 +02:00
Mzack9999 86b0f3f14f merge dev 2026-10-23 18:12:05 +02:00
Mzack9999 e284b6bc24 Merge pull request #7897 from projectdiscovery/feat/7893-headless-backoff
apply host backoff to headless navigations
2026-10-23 18:05:53 +02:00
Mzack9999 1248a14a62 merge dev 2026-10-23 18:03:30 +02:00
Mzack9999 4a2eb3b29d Merge pull request #7904 from TassioSales/fix/interactsh-client-fastdialer
fix(runner): use protocolstate fastdialer for default interactsh client (#7903)
2026-10-23 17:50:29 +02:00
Mzack9999 413a418714 reuse transport 2026-10-23 17:18:43 +02:00
Mzack9999 4cf0bbc08a merge dev 2026-10-23 17:16:07 +02:00
Mzack9999 1628b4c437 follow redirects 2026-10-23 17:16:07 +02:00
Mzack9999 e554a7e514 merge dev 2026-10-23 17:04:02 +02:00
Mzack9999 1fb6a9a287 keep proxy 2026-10-23 17:04:00 +02:00
Mzack9999 214d5e1310 Merge pull request #7889 from IBazsecI/fix/js-net-proxy-network-policy
fix(js/net): enforce network policy on proxied target in JS net dial
2026-10-23 16:51:43 +02:00
Mzack9999 54134f0f5b reorder check 2026-10-23 16:12:43 +02:00
Mzack9999 3e68f2b99f merge dev 2026-10-23 15:43:27 +02:00
Mzack9999 f8451703a2 check host 2026-10-23 15:43:27 +02:00
Mzack9999 5e0c3e3d4f Merge pull request #7902 from projectdiscovery/fix/go-toolchain-cve
bump Go toolchain for CVE-2026-111943
2026-10-23 14:27:52 +02:00
Georgii Butakov 9f4e53b933 chore(deps): point the fastdialer replace at the upstream commit
fastdialer#580 is merged into main, so the fork branch it was on no longer
exists. The replace resolves the same commit through the upstream module
path github.com/projectdiscovery/fastdialer.
2026-10-22 12:00:39 +00:00
mazzz3r aba8fda759 fix(payloads): widen the Feistel domain to cover the full int range
The domain search stopped at 62 bits, so a total above 2^62 was handed a
domain smaller than the range and encrypt dropped the top bit, breaking the
one-to-one mapping. Stop at 63 bits (rounded up to an even 64), which covers
every representable combination total.
2026-10-22 11:10:55 +00:00
mazzz3r c2767d6ef6 fix(payloads): generate the random order on demand with bounded memory
The random-order iterator built the shuffled order with rand.Perm(total),
allocating an int slice for the whole combination space before the first
request. For a clusterbomb product that is O(total): four 100-value lists
need ~800 MB up front, which can exhaust memory on the large, partially
sent scans the flag is meant for.

Replace it with an on-demand pseudo-random permutation of [0,total): a
4-round Feistel network over the enclosing power-of-two domain, restricted
to the range by cycle-walking. It visits every combination exactly once in
a random order, stays deterministic per iterator so Reset replays the same
order, and uses O(1) memory. The combination decode and Total() are
unchanged.

Add coverage for larger, non-power-of-two totals to exercise the
cycle-walking, and point the default-off guard at the new field.
2026-10-22 10:24:42 +00:00
mazzz3r c99bc1b959 test(payloads): cover random order totals, reset, default-off and option copy 2026-10-22 09:02:02 +00:00
mazzz3r 05d328d936 feat(payloads): iterate payload combinations in a random order
Add -pro, -payload-random-order to run a template's payload combinations
in a random order instead of the sequential order of the payload sets.

The iterator shuffles the combination indices [0,total) and decodes each
index back to a combination, so it visits every combination exactly once
for batteringram, pitchfork and clusterbomb, keeping pitchfork's
index-aligned pairs and the full clusterbomb product. Total() is
unchanged, so request estimates and progress stay correct. When the flag
is off the sequential path is used as before.
2026-10-22 08:49:42 +00:00
Georgii Butakov c1fcd4d8d2 Merge branch 'dev' into feat/target-line-ip
go.mod takes dev's versions, including fastdialer v0.5.26, and keeps the
fastdialer replace.
2026-10-21 20:25:04 +00:00
gnuletikandClaude Opus 5.5 d1bc79be95 fix(js): cap what an smtp connection may read
net/smtp reads replies through net/textproto, which buffers a line until
"\n" with no length limit, so IsOpenRelay and SendMail buffered whatever a
port streaming without newlines sent. Their reads now fail after 1 MiB.

SendMail dials through the execution's dialer like IsOpenRelay so its
connection can be wrapped; smtp.SendMail dials on its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:10:57 +02:00
gnuletikandClaude Opus 5.5 c505b5f3c4 fix(js): cap what a redis connection may read
go-redis reads a reply line until "\n" with no length limit, so a port that
streams bytes without one was buffered for as long as it kept sending: one
GetServerInfoAuth call against such a port grew the heap by about 1 GiB.

The redis clients now dial through go-redis's default dialer and wrap the
connection so reads fail past 16 MiB, far above any reply these calls expect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 18:26:16 +02:00
gnuletikandClaude Opus 5.5 6a4e6fab6f fix(js): close ldap and smtp connections when the template ends
go-ldap waits for a reply with no timeout and net/smtp reads with no
deadline. Neither takes a context, so on a server that accepts and never
answers the call stays blocked after the session is abandoned, and the
goroutine keeps the runtime and everything it references alive for the life
of the process.

Close the connection when the template's context is done. SendMail now
dials through the execution's dialer like IsSMTP and IsOpenRelay, instead of
net/smtp.SendMail dialing on its own past the network policy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 13:45:11 +02:00
Doğan Can Bakır 9bf950c861 require patched go version 2026-10-21 11:43:54 +03:00
TassioSales 4edd5c2c68 fix(runner): use protocolstate fastdialer for default interactsh client (fixes #7903) 2026-10-20 22:36:36 -03:00
TassioSales 5cf7f3ba47 fix(http): scope projectCacheKey with effective body read limit
Include the effective read limit in projectCacheKey so requests with different
limits (such as small template max-size previews) do not share truncated cached
bodies with larger reads.
2026-10-20 21:07:35 -03:00
TassioSales c918d010c5 fix(http): honour template max-size under global response-size-read limit
When -response-size-read (-rsr) is configured, use it as a ceiling while
preserving a template's own MaxSize when smaller, rather than unconditionally
overwriting it. Also account for ResponseReadSize in response truncation detection.

Fixes #7873
2026-10-20 21:07:35 -03:00
Doğan Can Bakır ed46eb76e3 bump go toolchain for CVE-2026-111943 2026-10-20 21:39:34 +03:00
Mzack9999 b0eeadc554 Merge pull request #7899 from projectdiscovery/chore/zcrypto-tls13-deps
bump utils and tlsx to the zcrypto TLS 1.3 revision
2026-10-20 16:58:14 +02:00
Mzack9999 5670af4c66 pin mongo 2026-10-20 16:17:17 +02:00
Mzack9999 6c4fa11c96 match redirect 2026-10-20 14:26:39 +02:00
Doğan Can Bakır 88f169d56d bump utils and tlsx, drop the zcrypto replace 2026-10-20 08:49:09 +03:00
Doğan Can Bakır d7730705af apply host backoff to headless navigations 2026-10-19 22:04:52 +03:00
Nakul Bharti f3b817b818 feat(reporting): close Linear issues when a finding is fixed (#7856)
* feat(reporting): close Linear issues when a finding is fixed

CloseIssue moves the matching Linear issue to the team's completed
workflow state (lowest position), or to closed-state-id when set.
Issues already completed or canceled are left alone.

Claude-Session: https://claude.ai/code/session_01Ud2FL9XNTahVE1Ke8W6y1M
Claude-Local-Session: claude --resume c4f3697b-a416-4c11-b645-e5dde2109626

* fix: address PR review comments

Claude-Session: https://claude.ai/code/session_01Ud2FL9XNTahVE1Ke8W6y1M
Claude-Local-Session: claude --resume c4f3697b-a416-4c11-b645-e5dde2109626

* fix: address PR review comments

Claude-Session: https://claude.ai/code/session_01Ud2FL9XNTahVE1Ke8W6y1M
Claude-Local-Session: claude --resume c4f3697b-a416-4c11-b645-e5dde2109626
2026-10-19 23:56:19 +05:30
dependabot[bot]andMzack9999 be87581d1e chore(deps): bump the modules group with 7 updates (#7891)
* chore(deps): bump the modules group with 7 updates

Bumps the modules group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.25` | `0.5.26` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.54` | `0.1.55` |
| [github.com/projectdiscovery/ratelimit](https://github.com/projectdiscovery/ratelimit) | `0.0.93` | `0.0.94` |
| [github.com/projectdiscovery/rawhttp](https://github.com/projectdiscovery/rawhttp) | `0.1.92` | `0.2.0` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.30` | `1.3.31` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.3.4` | `0.3.5` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.3.1` | `1.3.2` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.25 to 0.5.26
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.25...v0.5.26)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.54 to 0.1.55
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.54...v0.1.55)

Updates `github.com/projectdiscovery/ratelimit` from 0.0.93 to 0.0.94
- [Release notes](https://github.com/projectdiscovery/ratelimit/releases)
- [Commits](https://github.com/projectdiscovery/ratelimit/compare/v0.0.93...v0.0.94)

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

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

Updates `github.com/projectdiscovery/wappalyzergo` from 0.3.4 to 0.3.5
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.3.4...v0.3.5)

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

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.55
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/ratelimit
  dependency-version: 0.0.94
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/rawhttp
  dependency-version: 0.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.3.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <[email protected]>

* pin zcrypto

* keep transport

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mzack9999 <[email protected]>
2026-10-19 20:12:07 +02:00
Mzack9999 b4761d4fc4 merge dev 2026-10-19 15:05:46 +02:00
Mzack9999 e78e9ddea8 Merge pull request #7895 from projectdiscovery/feat/7794-dial-backoff
apply host backoff to network, ssl and websocket dials
2026-10-19 15:04:31 +02:00
Mzack9999 e4d1535691 merge dev 2026-10-19 14:26:55 +02:00
Mzack9999 339578d415 split tls 2026-10-19 14:19:10 +02:00
Mzack9999 a3109229fd fix dials 2026-10-19 13:21:59 +02:00
Doğan Can Bakır 69f4fe915b ignore dial failures on hosts that never accepted 2026-10-19 09:42:49 +03:00
Doğan Can Bakır c8a0d45d38 apply host backoff to network, ssl and websocket dials 2026-10-19 09:10:52 +03:00
Mzack9999 10f1967cdc Merge pull request #7879 from projectdiscovery/fix/7878-dump-body-race
keep the request body in the dump taken after sending
2026-10-18 17:19:50 +02:00
Mzack9999 767655e306 copy bodies 2026-10-18 16:29:21 +02:00
Mzack9999 0bd50f957a Merge pull request #7881 from projectdiscovery/fix/7880-stdin-with-targets
skip stdin when targets are given with -u or -l
2026-10-18 15:56:16 +02:00
Mzack9999 e264bd82f9 skip uncover 2026-10-18 15:29:23 +02:00
Kemal BazandClaude Opus 4.8 1c0f77d28e fix(js/net): enforce network policy on proxied target in JS net dial
When an HTTP(S) proxy is configured, the nuclei/net JS library dialed the
proxy through Fastdialer (which validates the proxy host) but tunneled the
connection target inside an HTTP CONNECT without validating it. This let a
template reach a deny-listed destination (-exclude-hosts /
-restrict-local-network-access) that a direct dial would have blocked,
bypassing the network policy.

Validate the target host against the network policy in the proxy branch of
Dial() before contacting the proxy, matching the guard the direct path gets
from Fastdialer. Adds a regression test asserting the proxy is never
contacted for a deny-listed target.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-18 14:32:26 +03:00
kleonyuan 6a42a7bf52 chore: fix some comments (#7830)
Signed-off-by: kleonyuan <[email protected]>
2026-10-18 05:00:43 +05:30
TassioSales 3827928531 fix(sdk): separate template auto-upgrade sync.Once and avoid FailNow in callback goroutine 2026-10-17 13:32:20 -03:00
TassioSales b296cc0d46 fix(sdk): prevent custom-port HTTP failures from poisoning base host cache and filter skip events (#7859) 2026-10-17 13:06:21 -03:00
IgorDaniel45 9a20d1cc69 merge dev and preserve both protocol test suites 2026-10-16 17:34:39 -03:00
IgorDaniel45 3bb429522f fix template flow options on compiled cache hits 2026-10-16 17:27:25 -03:00
AdamMagued 535e04d555 perf(protocols/http): defer building FullResponseString when unused by operators
Signed-off-by: AdamMagued <[email protected]>
2026-10-16 01:35:26 +03:00
TassioSales 4ce6dea3e9 fix(runner): set HTTPS_PROXY and all proxy env vars when proxy-internal is enabled
Fixes #7875

When options.ProxyInternal was enabled, only HTTP_PROXY was set in the environment.
Because Go standard library http.ProxyFromEnvironment inspects HTTPS_PROXY for https URLs,
internal HTTPS requests (such as template updates via GitHub API / releases, pdtm, etc.)
failed to route through the specified proxy in restricted or proxy-required environments.

This sets HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (and lowercase counterparts) when ProxyInternal
is enabled, and adds unit test coverage. Also fixes operator precedence for ProxyInternal
checks in runner.go and sdk_private.go.
2026-10-15 09:15:21 -03:00
Nitin b31f787d79 chore: fix ValidateOptions doc comment name 2026-10-15 16:59:22 +05:30
Nitin 17101d0501 chore: add doc comments to functions touched by the templates channel 2026-10-15 16:54:00 +05:30
Nitin a4cc18f30e fix(installer): download templates archive without the github token
The archive link points to a different host than the API, and the github
client sends the token to every request it makes. The archive is public,
so only the API call needs the token now, and the archive url has to be
https.

Also adds doc comments to the new functions and tests.
2026-10-15 16:43:31 +05:30
Nitin 866da46866 feat(installer): add templates update channel
Adds -utc / -update-templates-channel (stable, latest). stable keeps the
current behaviour. latest installs the head of the nuclei-templates main
branch, so templates merged after the last release are available without
waiting for the next one.

The version for the latest channel is the last release plus the short
commit as semver build metadata (e.g. v10.5.0+3de629d). Build metadata is
ignored when comparing versions, so switching back to stable doesn't
downgrade, and only a newer release makes it outdated there. Since the
version check only knows about releases, the latest channel compares the
installed version against the current head to decide when to update.

The channel can also be set in the config file like the other options.

Closes #5608
2026-10-15 16:26:17 +05:30
Doğan Can Bakır 788973d974 fix: skip stdin when targets are given with -u or -l 2026-10-15 11:58:30 +03:00
Doğan Can Bakır 7989011cdf fix: keep the request body in the dump taken after sending 2026-10-15 11:52:15 +03:00
Mzack9999 f121a7cf0d redirect cache 2026-10-14 23:54:14 +02:00
Mzack9999 c0e572d14c merge dev
# Conflicts:
#	pkg/protocols/http/request.go
2026-10-14 23:30:27 +02:00
Mzack9999 a2f9d3c219 merge remote 2026-10-14 23:24:55 +02:00
Dogan Can Bakir a151a1d54f Merge pull request #7793 from projectdiscovery/feat/7783-host-backoff
slow down a host that starts blocking
2026-10-14 16:06:57 +03:00
Dogan Can Bakir 1db63fdb42 Merge pull request #7852 from projectdiscovery/feat/replay-proxy
add -replay-proxy to re-send requests that produced a finding
2026-10-14 16:04:55 +03:00
TassioSales 8b440a5a77 fix(headless): address errcheck linter warnings for unchecked returns in proxy and tests 2026-10-14 00:34:33 -03:00
Mzack9999 af61439dc5 merge dev 2026-10-14 00:00:38 +02:00
Georgii Butakov 4716246849 Merge branch 'dev' into feat/target-line-ip
go.mod takes dev's versions, including fastdialer v0.5.25, and keeps
fastdialer replaced by the branch of projectdiscovery/fastdialer#580,
which now includes v0.5.25.
2026-10-13 19:55:18 +00:00
Mzack9999 9a13e64144 replay findings 2026-10-13 20:30:34 +02:00
TassioSales d43c1f547f fix(headless): canonicalize IPv4-mapped IPv6 literals and re-validate dial targets 2026-10-13 15:01:50 -03:00
TassioSales 506e4fb0fc fix(headless): enforce network policy on WebSocket handshakes via loopback proxy 2026-10-13 14:16:53 -03:00
Nakul Bharti a5715288aa fix(runtime): honor cancellation in HTTP and headless waits (#7814)
* fix(http): cancel per-host rate admission

* fix: address PR review comments

* test: keep request coverage with request tests

* fix(headless): bound browser setup by request context

* fix: address PR review comments

* fix: address PR review comments

* fix cancellation-safe rate and headless cleanup

* fix: cancel global rate admission waits

* chore: bump ratelimit to v0.0.93
2026-10-13 23:32:09 +07:00
Nakul Bharti cc5645e674 fix(http): preserve iterate-all extraction branches (#7858)
* fix(http): preserve extractor branch correlation

* fix(http): preserve re-extracted dynamic values

* fix(http): isolate iterate-all branch state

* fix(http): address iterate-all review findings
2026-10-13 20:02:58 +05:30
dependabot[bot] 40075b0b20 chore(deps): bump the modules group across 1 directory with 5 updates (#7869)
Bumps the modules group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/interactsh](https://github.com/projectdiscovery/interactsh) | `1.4.0` | `1.4.1` |
| [github.com/projectdiscovery/ratelimit](https://github.com/projectdiscovery/ratelimit) | `0.0.91` | `0.0.92` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.29` | `1.3.30` |
| [github.com/projectdiscovery/tlsx](https://github.com/projectdiscovery/tlsx) | `1.4.0` | `1.4.1` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.3.2` | `0.3.4` |



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

Updates `github.com/projectdiscovery/ratelimit` from 0.0.91 to 0.0.92
- [Release notes](https://github.com/projectdiscovery/ratelimit/releases)
- [Commits](https://github.com/projectdiscovery/ratelimit/compare/v0.0.91...v0.0.92)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.29 to 1.3.30
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.29...v1.3.30)

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

Updates `github.com/projectdiscovery/wappalyzergo` from 0.3.2 to 0.3.4
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.3.2...v0.3.4)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/interactsh
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/ratelimit
  dependency-version: 0.0.92
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/tlsx
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.4
  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-10-13 10:45:51 +00:00
Dogan Can Bakir 90a0cce807 Merge pull request #7824 from projectdiscovery/fix/smb-connect-race
cancel a stalled SMB connect through its context
2026-10-13 13:16:21 +03:00
Doğan Can Bakır 1caa955c77 chore: bump fastdialer to v0.5.25 2026-10-13 12:43:26 +03:00
TassioSales 52960db710 fix(headless): harden blocked URL staging, URL target parsing, and wsMap cleanup 2026-10-13 02:37:33 -03:00
TassioSales ca3e27c5f5 fix(headless): accumulate dynamic blocked URL patterns across denied WebSockets 2026-10-13 02:22:21 -03:00
TassioSales a94aa7f00d fix(headless): propagate NetworkEnable errors during policy initialization 2026-10-13 02:12:01 -03:00
TassioSales c14335d351 fix(headless): address code review feedback on page lifecycle and CIDR handling 2026-10-13 02:02:26 -03:00
TassioSales c5a528cc60 fix(headless): enforce network policy and blocked URLs for WebSocket connections via CDP 2026-10-13 01:45:12 -03:00
TassioSales d9dabbb4a1 fix(headless): enforce network policy on ws and wss schemes (#7867)
Previously, isValidHost only checked targets with http: and https:
prefixes, allowing ws: and wss: connections in headless mode to bypass
configured network policies (e.g. -exclude-hosts or local network restrictions).

This commit normalizes the scheme check and enforces host validation
against the network policy for ws: and wss: protocols as well.
2026-10-12 23:50:25 -03:00
Georgii Butakov d21749be3a feat(input): pin a target to an ip with the ip key of target lines
Scanning a vhost on a given server, or an origin behind a CDN, needs the
hostname in the Host header, SNI and template variables while the
connection goes to another ip, and one host often has to be scanned on
several ips. -sa scans the ips DNS returns, and a target of the ip with
a Host header sends no SNI and sets {{Hostname}} to the ip.

A target line can now pin its target to an ip, like curl --resolve:

  {"target": "https://app.example", "ip": "192.0.2.10"}

Each target and ip pair is a separate target with its own template
selection. The ip is validated and canonicalized, the target must have
a hostname, -sa and -iv don't resolve a pinned target, and an ip the
network policy denies is skipped. Pinned targets are refused with
-proxy and -automatic-scan.

The engine pins the host of every target with a CustomIP, from a target
line or -sa, with fastdialer.WithResolveOverride on the context each
template runs with. HTTP, network, websocket and javascript templates
then connect to the ip, while redirects to other hosts and the proxy
dial are no longer pinned. HTTP keeps a client per pinned ip, network
and websocket results report the ip, and scheme-less targets are probed
through it.

The host errors cache keys a failed dial of a pinned target by its ip,
also for IDN hosts, and -mup counts the urls of each ip apart, so a url
listed with several ips, or resolved to several with -sa, is kept on
each of them.

go.mod points fastdialer at a fork commit with WithResolveOverride until
a fastdialer release includes it.
2026-10-12 17:02:39 +00:00
Doğan Can Bakır 830e012a10 Merge remote-tracking branch 'origin/dev' into feat/7783-host-backoff
# Conflicts:
#	pkg/protocols/common/hostratelimit/host_normalize.go
#	pkg/protocols/http/request.go
#	pkg/protocols/http/request_fuzz.go
#	pkg/types/types.go
2026-10-12 12:04:34 +03:00
Doğan Can Bakır 9b368f3eaa Merge remote-tracking branch 'origin/dev' into fix/smb-connect-race
# Conflicts:
#	go.mod
#	go.sum
2026-10-12 11:57:44 +03:00
Dogan Can Bakir 59e6ffa6fb Merge pull request #7822 from projectdiscovery/fix/7821-js-call-cancellation
cancel javascript library calls with the template execution timeout
2026-10-12 11:33:23 +03:00
Doğan Can Bakır b870cdcd67 chore: bump goimpacket to main 2026-10-12 11:03:19 +03:00
Doğan Can Bakır 3f776b5ff0 chore: bump goja to master 2026-10-12 10:58:22 +03:00
Doğan Can Bakır ad74f0094c chore: pin goja to the projectdiscovery/goja branch instead of a fork 2026-10-11 21:57:38 +03:00
Doğan Can Bakır 4137bedd31 fix: keep replayed requests free of an added Accept-Encoding 2026-10-11 19:39:49 +03:00
Georgii Butakov 1568f1e68e feat(progress): report completion by executed templates
The -stats percentage was requests sent against a total estimated
before the scan: TotalRequests of each template after clustering, times
the number of targets. The estimate leaves out workflows, whose requests
are added while they run, counts a self-contained template once per
target although it runs once, and doesn't follow stop-at-first-match or
a template that runs once per origin. A scan could end at 108% or stop
at 25%.

The engine now counts tasks: one per template, cluster or workflow run
on a target, and one per self-contained template. The total is known
when the scan starts. A task is done when it runs, when -resume skips
it, when its host is unresponsive, or when it repeats its origin.

progress.TaskTracker is an optional interface that StatsTicker
implements, so Progress and lib.StatsWriter don't change. The stats
line shows Tasks: done/total and takes the percentage from it, and -sj
and the metrics endpoint report tasks, totalTasks and the same percent.
2026-10-10 12:00:33 +00:00
Dogan Can Bakir a48a58ef59 Merge pull request #7847 from projectdiscovery/fix/per-host-rate-limit-all-protocols
apply per-host rate limiting to every protocol
2026-10-09 21:11:47 +02:00
dependabot[bot] 007ee7d4ba chore(deps): bump the go_modules group across 1 directory with 2 updates
Bumps the go_modules group with 2 updates in the / directory: [github.com/moby/sys/user](https://github.com/moby/sys) and [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go).


Updates `github.com/moby/sys/user` from 0.4.0 to 0.4.1
- [Release notes](https://github.com/moby/sys/releases)
- [Commits](https://github.com/moby/sys/compare/user/v0.4.0...user/v0.4.1)

Updates `go.opentelemetry.io/otel/sdk` from 1.44.0 to 1.45.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...v1.45.0)

---
updated-dependencies:
- dependency-name: github.com/moby/sys/user
  dependency-version: 0.4.1
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.45.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-09 18:51:19 +00:00
Mzack9999 0de51237b7 Merge pull request #7588 from projectdiscovery/5717-graphql-fuzz
fuzz: GraphQL-over-HTTP body support for DAST
2026-10-09 20:48:41 +02:00
Mzack9999 4857883667 Merge remote-tracking branch 'origin/fix/rate-limit-all-protocols' into fix/per-host-rate-limit-all-protocols 2026-10-09 20:34:20 +02:00
Mzack9999 bccaa4410d merge dev 2026-10-09 20:34:08 +02:00
Mzack9999 d8253b97e9 retrigger checks 2026-10-09 20:30:35 +02:00
Mzack9999 65f60dd0a6 retrigger checks 2026-10-09 20:30:35 +02:00
Mzack9999 1115f18b68 fix keys 2026-10-09 20:26:43 +02:00
Mzack9999 458367feb7 add tests 2026-10-09 20:20:02 +02:00
Mzack9999 0af8c98679 replay once 2026-10-09 20:03:07 +02:00
TassioSales 50a6526721 fix(extractors): reject invalid xpath expressions during extractor compilation (fixes #7750) 2026-10-08 17:33:24 -03:00
Nitin 7010ce6a91 test(catalog): add doc comments to catalog tests 2026-10-08 15:46:15 +05:30
Nitin 5d9da1d199 fix(catalog): match known config files by exact basename
The catalog skipped any template whose name contained a known config file
name (cves.json, contributors.json, TEMPLATES-STATS.json) since the check
used substring matching. A template like http/cves.json.yaml was silently
dropped both when passed directly and during directory traversal.

Add config.IsKnownConfigFile which compares the base name exactly, use it
in IsTemplateWithRoot and GetTemplatesPath, and drop the duplicate list in
the disk package.

Closes #7610
2026-10-08 15:23:23 +05:30
Doğan Can Bakır 0217339d5d fix: reuse the scan's TLS config for replayed requests 2026-10-08 10:15:29 +03:00
Doğan Can Bakır d23849e996 feat: add -replay-proxy to re-send requests that produced a finding 2026-10-08 09:41:24 +03:00
Dogan Can Bakir 59dad1b619 Merge pull request #7834 from projectdiscovery/perf/functional-runtime
walk and parse the templates directory once for workflow tag lookups
2026-10-08 09:33:23 +03:00
Dogan Can Bakir 889c8ff9c5 Merge pull request #7840 from projectdiscovery/fix/7836-fuzz-progress-total
count fuzzing requests in the progress total
2026-10-08 09:33:10 +03:00
Dogan Can Bakir c990724404 Merge pull request #7841 from projectdiscovery/fix/7837-js-port-requests
count javascript requests per port
2026-10-08 09:32:56 +03:00
Dogan Can Bakir 50e0afd188 Merge pull request #7842 from projectdiscovery/fix/7838-flow-request-count
keep flow progress at 100% for repeated and named requests
2026-10-08 09:32:41 +03:00
Mzack9999 04d0caa646 merge dev 2026-10-07 23:07:06 +02:00
Mzack9999 80a001ca0f Merge pull request #7843 from projectdiscovery/fix/7839-js-precondition-count
count javascript pre-condition requests without -debug
2026-10-07 23:04:03 +02:00
Sam PizzeyandClaude Sonnet 5 8d33e4a4c7 fix(http): self-heal HTTP-to-HTTPS requests immediately on detection
The http-to-https-tracker only recorded a host:port as https-only
after a plain-HTTP request received the "plain HTTP request was sent
to HTTPS port" 400 response, and used that record solely to
pre-emptively rewrite the scheme of later requests. The request that
first discovered the mismatch was never retried, so it matched
against the wrong-scheme 400 instead of the real page.

This produced a false negative on whichever request happened to run
first against a given host:port, including every request racing out
before the tracker learned about it under default concurrency - and
template loading itself races, so results could flip between runs of
the same scan even at -c 1.

Peek the response immediately after the first send and, if it matches
the signature, retry the same request over https before handing it to
the matcher pipeline, instead of only caching the fact for next time.

Fixes #7849

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-10-07 11:46:38 +01:00
Doğan Can Bakır 1b080a7d19 fix: apply per-host rate limiting to every protocol 2026-10-07 12:45:14 +03:00
Doğan Can Bakır a847374f2c fix: apply the rate limit to network, ssl, websocket, whois, javascript and headless 2026-10-07 00:17:52 +03:00
Doğan Can Bakır 9c9247895d fix(flow): count skipped requests as processed like other protocols 2026-10-06 23:21:34 +03:00
Doğan Can Bakır 37052a7e3f fix(fuzz): count nothing up front for fuzzing requests with path or raw 2026-10-06 23:16:08 +03:00
Doğan Can Bakır c4825e98c9 fix(js): count pre-condition requests without -debug 2026-10-06 23:07:25 +03:00
Doğan Can Bakır 0c0e886c2c fix(flow): count only requests a flow sends 2026-10-06 23:03:36 +03:00
Doğan Can Bakır b3e02efa7f fix(js): count requests per port 2026-10-06 23:03:06 +03:00
Doğan Can Bakır aa3c7a361c fix(fuzz): count fuzzing requests in the progress total 2026-10-06 23:02:53 +03:00
Dogan Can Bakir afb2afaf4f Merge pull request #7800 from projectdiscovery/feat/7782-dedup-crawled-urls
deduplicate crawled urls before template execution
2026-10-06 22:13:56 +03:00
Doğan Can Bakır 6fde18d311 fix(js): reset the runtime context with context.TODO 2026-10-06 21:58:07 +03:00
Doğan Can Bakır 62721e589a perf(workflow): walk and parse the templates directory once per loader for tag lookups 2026-10-06 20:50:31 +03:00
Doğan Can Bakır 66ef296067 test(functional): report per-test durations 2026-10-06 20:50:31 +03:00
Mzack9999 496589e7d0 merge dev 2026-10-06 16:07:21 +02:00
Mzack9999 113ea8afa9 fix lint 2026-10-06 15:19:34 +02:00
Mzack9999 a5d0eb9fda fix cluster 2026-10-06 15:13:19 +02:00
Mzack9999 65732c382f add tests 2026-10-06 14:56:50 +02:00
Mzack9999 ab8c7feeba fix exports 2026-10-06 14:30:41 +02:00
Mzack9999 bd510ac6bf Merge pull request #7827 from projectdiscovery/dwisiswant0/fix/fuzz/preserve-cookie-payloads-when-rebuilding-requests
fix(fuzz): preserve cookie payloads when rebuilding requests
2026-10-06 14:10:54 +02:00
Mzack9999 71457b4f69 Merge pull request #7820 from projectdiscovery/perf/jobs-after-lint
start integration, functional and other jobs right after lint
2026-10-06 14:08:15 +02:00
Maximilian Hildebrand 2815cb2aef fix(installer): fail fast on unremovable templates update lock 2026-10-06 10:12:11 +02:00
Dwi Siswanto 97c8233b72 test: avoid DNS lookups in cookie redirect tests
Dial the test listener directly while retaining distinct request
hosts for cookie scope checks. This avoids localhost lookup failures
on Windows.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-10-06 12:30:55 +07:00
Mzack9999 cb824a4718 cover consumers 2026-10-05 23:43:43 +02:00
Mzack9999 78a5cadc67 fix response 2026-10-05 23:28:43 +02:00
Mzack9999 6bb5b5dbd1 fix lint 2026-10-05 22:31:44 +02:00
Riyan Firmansyah f5aa294aa6 Merge pull request #1 from ruzfi/fix/http-ip-var-empty-behind-proxy 2026-10-06 02:19:05 +07:00
ruzfi 77b34db0cc fix(http): set hostname for pipelined non-raw requests
The pipeline branch that sends a built request via pipelinedClient.Dor
never set hostname, unlike the raw pipeline branch. With an empty
hostname, the dial-history lookup, the IP-literal fallback in outputIP
and the cname lookup had nothing to work with, so `ip` was always empty
for these requests.

Set hostname from the request URL's host when the URL is non-nil,
matching the normal request path.
2026-10-06 02:14:36 +07:00
Dwi Siswanto 8f23a8633f fix(fuzz): preserve cookie payloads when rebuilding requests
`Cookie.String` sanitizes values that can be deliberate fuzz payloads.
Stored cookies can also introduce a second value for the cookie under
test.

Build Cookie headers from raw fields and track the keys selected for
fuzzing. Use a filtered jar on an isolated client to retain session
updates without replacing those fields.

Fixes #7813
Fixes #7826

Signed-off-by: Dwi Siswanto <[email protected]>
2026-10-06 01:48:04 +07:00
ruzfi 87e0a1d2a4 fix(http): set ip var from IP-literal target when dial history is empty
Resolve the output ip in a single helper, outputIP, used by both the
request-error and response paths: CustomIP first, then dial history,
then the target host if it is an IP literal. The helper splits the port
from the unstripped host with net.SplitHostPort, because the existing
LastIndex(":") strip truncates bracketed IPv6 without a port and, in
the redirect loop, truncates bracketed IPv6 further on each iteration.
2026-10-06 01:14:35 +07:00
Doğan Can Bakır 28d5b3e8c0 fix(smb): cancel a stalled connect through its context instead of closing it concurrently 2026-10-05 17:17:47 +03:00
Doğan Can Bakır c2d38083dc fix(js): cancel library calls with the template execution timeout 2026-10-05 16:35:38 +03:00
Dogan Can Bakir 1884ed920f Merge pull request #7807 from projectdiscovery/perf/fast-downloader-timeout-tests
cut unit and integration test runtime
2026-10-05 16:22:40 +03:00
Doğan Can Bakır d28dd2c745 ci: start every job after lint instead of after unit tests 2026-10-05 15:59:27 +03:00
Mzack9999 3336556187 Merge pull request #7818 from projectdiscovery/perf/release-test-after-lint
run release test right after lint
2026-10-05 14:48:01 +02:00
Mzack9999 2c8f4a0318 resolve aliases 2026-10-05 14:26:23 +02:00
Doğan Can Bakır 8c8a1a9d78 ci: run the release test after lint instead of after unit tests 2026-10-05 13:51:35 +03:00
Doğan Can Bakır 81749cf750 test(integration): install templates once before cases and never auto-update in cases 2026-10-05 12:57:09 +03:00
Doğan Can Bakır 846f4425b0 perf(installer): open the templates root and create each directory once per install 2026-10-05 12:53:47 +03:00
Doğan Can Bakır fa876a4d92 fix(installer): give every alias of a templates directory the same update lock 2026-10-05 12:49:20 +03:00
Sandiyo Christan 352f46cc35 fix(extractors): reject attribute on non-xpath extractors
`attribute` is read only by the xpath extractors, but it is accepted on
every extractor type and then dropped. A template that sets it on a json
extractor compiles, runs, and returns whatever the extractor collects.
Nothing reports the drop, so the result is indistinguishable from "the
target did not match" — a silent false negative for a scanner.

CompileExtractors already rejects a misapplied field, `case-insensitive`
outside kval extractors. Extend that pattern so a misapplied `attribute`
fails at template load through Operators.Compile, which reports it as
"could not compile extractor". The check runs after the per-type
required-value validation so the more useful "requires at least one
<field> value" error still wins.

`group` is a no-op outside regex extractors in the same way but is
deliberately left alone. A survey of all 14,029 templates in
nuclei-templates found 17 extractors that set `group` on a json or kval
extractor, all published CVE detections such as CVE-2024-3848, so
enforcing it breaks real templates. Two tests pin that behaviour.

Verified with nuclei -validate over all 14,029 templates: "All templates
validated successfully".
2026-10-02 12:09:18 +05:30
Doğan Can Bakır 0ae69a7d53 fix(installer): reload the templates version after taking the update lock 2026-10-01 18:10:08 +03:00
Doğan Can Bakır 80b58e6e9b test(integration): report per-test durations 2026-10-01 16:36:38 +03:00
Doğan Can Bakır 4d7e91e4e0 ci: drop unused build and disk cleanup from the tests job 2026-10-01 15:55:23 +03:00
Doğan Can Bakır 6b9208cdbf refactor(installer): inject the templates release and scope the update lock per directory 2026-10-01 15:55:17 +03:00
Doğan Can Bakır 26b28c69a4 test(installer): serve releases from memory and isolate the update lock 2026-10-01 15:13:39 +03:00
Calvin Tjoaquinn 98df72854b fix(raw): keep a path that contains "HTTP" in a versionless request line
A two-field request line omits the path, so the second field is the
version, and the branch that handles it tested the field with
strings.Contains(parts[1], "HTTP"). Any path containing that substring
matched, so the field was treated as the version and the path was
replaced with "".

    GET /HTTPProxy          ->  Method=GET  Path=""
    GET /api/HTTPHandler    ->  Method=GET  Path=""
    GET /admin              ->  Method=GET  Path="/admin"

For a normal raw template the request then goes to the path of the input
URL instead of the one the template asked for, with no warning. For a
self-contained template the symptom is worse: ParseRawRequest refuses an
empty path, so the template fails with "path cannot be empty in self
contained request" while the path is plainly there.

Testing the field with HasPrefix(parts[1], "HTTP/") keeps the case the
branch exists for, "GET HTTP/1.1", and stops a path from being mistaken
for a version. The sibling raw parser in pkg/input/types already works
this way: it requires three fields and uses a prefix test for the scheme.
2026-10-01 18:04:42 +07:00
Dogan Can Bakir a14b3de2a4 Merge pull request #7791 from projectdiscovery/feat/7743-llm-non-http-protocols
llm matchers and extractors on all protocols
2026-10-01 14:00:19 +03:00
Calvin Tjoaquinn 7fa79ba6a6 fix(raw): stop an empty line at EOF from becoming a header
The header loop treats an empty line as the end of the block only when
the read succeeded. When the request ends with a newline and the next
read returns io.EOF, the guard falls through and the empty line is
processed as a header, so Headers gains an entry named "".

That entry reaches the wire. rawhttp's serializer writes a header with
an empty value as just the key followed by CRLF, so a key of "" is
written as a bare CRLF, which ends the header block. Map iteration order
decides where it lands, so a random subset of the remaining headers is
sent as body instead:

  GET /admin HTTP/1.1
  Host: example.com
                                  <- header block ends here
  Authorization: Bearer s3cr3t

Before the previous commit the entry held the preceding header's value
and serialized as a malformed ": value" line. With values no longer
reused it holds "", which is why the fix belongs with that change rather
than after it.

An empty line now ends the block regardless of the read error, and a
real read error still stops the loop without storing anything.
2026-10-01 14:23:28 +07:00
TassioSales 52245202ff fix(extractors): reject misapplied group and attribute fields on wrong extractor types (#7808)
- Reject non-zero 'group' on non-regex extractor types with descriptive compilation error

- Reject non-empty 'attribute' on non-xpath extractor types with descriptive compilation error

- Implement String() on ExtractorTypeHolder so formatted error strings output clean type names

- Add unit tests covering rejection of misapplied fields across all extractor types
2026-09-30 22:13:48 -03:00
Mzack9999 3c434fb73a fix lint 2026-09-30 22:48:03 +02:00
Mzack9999 f40fa195b6 fix dedup 2026-09-30 22:28:21 +02:00
Mzack9999 a2b6d2abc7 Merge pull request #7805 from projectdiscovery/perf/windows-ci-defender-exclusion
exclude the workspace from defender scanning on windows
2026-09-30 22:05:13 +02:00
Doğan Can Bakır b87b28d7ad test: drop the interactsh cooldown from test defaults 2026-09-30 17:47:56 +03:00
Doğan Can Bakır 4d9f2f9867 test(headless): reset debug state between engine tests 2026-09-30 17:41:21 +03:00
Doğan Can Bakır 4177ca752b test(headless): share one browser across the engine tests 2026-09-30 17:38:49 +03:00
Doğan Can Bakır 8ffccaf639 test(input): stop waiting out the real timeout in downloader tests 2026-09-30 17:25:14 +03:00
Doğan Can Bakır fd1da307b6 ci: exclude the workspace from defender scanning on windows 2026-09-30 16:04:42 +03:00
Om b104625865 fix(openapi): send format: binary octet-stream bodies as octet-stream
The octet-stream branch compared the schema format against "bindary",
so a requestBody with format: binary went out with Content-Type:
text/plain and only format: byte kept application/octet-stream.
2026-09-30 03:51:50 +05:30
Mzack9999 5674792a7c pace waiters 2026-09-29 23:34:53 +02:00
Mzack9999 a80d0b881a fix backoff 2026-09-29 22:21:55 +02:00
Mzack9999 0e243e3edb seed audits 2026-09-29 21:43:54 +02:00
dw1 54f9310945 chore: migrate github clients to go-github/v92 (#7778)
Use v92 for template downloads and issue reporting, removing the direct
v30 dependency. Adapt client construction, issue request types, and
archive calls while preserving authentication fallback, custom API
paths, and the single repository-rename redirect.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-09-29 21:10:11 +02:00
Doğan Can Bakır 3a827b8c8b feat(input): deduplicate crawled urls before template execution 2026-09-29 16:10:55 +03:00
Calvin Tjoaquinn 73d976be6c fix(raw): stop a colon-less header line from taking the previous value
readRawRequest reused key and value across loop iterations and only
reassigned value when the line contained a colon. A line carrying no colon
therefore stored the value of the line before it under its own name:

  GET /admin HTTP/1.1
  Host: example.com
  Authorization: Bearer s3cr3t
  X-Forwarded-For

parsed to X-Forwarded-For: Bearer s3cr3t, so the credential went out under
the wrong header name.

Declaring both inside the loop fixes it by construction rather than by
remembering to reset. The parser stays as lenient as before: the malformed
line is still accepted as a header, now with an empty value.
2026-09-29 14:02:24 +07:00
comradezephyr 06dd497f85 fix(input): emit JSON objects in RequestResponse.MarshalJSON
RequestResponse.MarshalJSON stored the pre-marshaled request/response bytes as []byte inside map[string]interface{}, which encoding/json (and sonic) encode as base64 strings. The type's own UnmarshalJSON expects JSON objects under those keys, so the marshal -> unmarshal round-trip always failed when Request != nil, breaking the json.JSONCodec contract asserted at pkg/input/types/http.go.

MetaInput.ReqResp serializes under the raw-request key with this codec (MetaInput.MarshalString -> MetaInput.Unmarshal), used by host-map scans and exclusion processing in pkg/input/provider/list/hmap.go and internal/runner/preflight_portscan.go: a captured request attached to a MetaInput produced an encoded key that could not be decoded.

Wrap the pre-marshaled bytes as json.Message so they are emitted verbatim, and omit the keys for nil Request/Response instead of encoding base64 null, mirroring UnmarshalJSON's optional-key behavior.

Fixes #7795
2026-09-28 16:02:40 -04:00
Doğan Can Bakır 97e13fae6e feat(scan): slow down a host that starts blocking 2026-09-28 15:40:19 +03:00
Doğan Can Bakır 1b8e3515bf feat(llm): attach llm audit on every protocol 2026-09-28 13:46:17 +03:00
Doğan Can Bakır 0de1d12cc9 feat(llm): evaluate llm matchers and extractors on all protocols 2026-09-28 12:43:01 +03:00
ghost 47f74706a5 docs: update syntax & JSON schema 🤖 2026-09-28 08:35:44 +00:00
Dogan Can Bakir dad93f0ed6 Merge pull request #7774 from projectdiscovery/feat/7742-llm-matcher-inputs
feat(llm): compare multiple responses with matcher inputs
2026-09-28 11:30:19 +03:00
dependabot[bot] 3adff6347b chore(deps): bump the modules group with 6 updates (#7787)
Bumps the modules group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.21` | `0.5.22` |
| [github.com/projectdiscovery/interactsh](https://github.com/projectdiscovery/interactsh) | `1.3.1` | `1.4.0` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.51` | `0.1.52` |
| [github.com/projectdiscovery/ratelimit](https://github.com/projectdiscovery/ratelimit) | `0.0.90` | `0.0.91` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.27` | `1.3.28` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.3.1` | `0.3.2` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.21 to 0.5.22
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.21...v0.5.22)

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

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.51 to 0.1.52
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.51...v0.1.52)

Updates `github.com/projectdiscovery/ratelimit` from 0.0.90 to 0.0.91
- [Release notes](https://github.com/projectdiscovery/ratelimit/releases)
- [Commits](https://github.com/projectdiscovery/ratelimit/compare/v0.0.90...v0.0.91)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.27 to 1.3.28
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.27...v1.3.28)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.3.1 to 0.3.2
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.3.1...v0.3.2)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/interactsh
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.52
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/ratelimit
  dependency-version: 0.0.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.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.2
  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-09-28 06:23:44 +00:00
Om 349299c9ab fix(openapi): skip a request whose xml body cannot be encoded
An XML array example has no single root to encode, and the request
still went out with no body or Content-Type. Skip it after the warning,
like the default branch does for content types it cannot build.
2026-09-28 02:30:09 +05:30
Om f76bd78d15 fix(openapi): build request bodies from an example of the empty schema
When a request body had no schema, the generator used the schema that
generateEmptySchemaValue returned as the body value itself. An
application/xml body then hit the unchecked map assertion and nuclei
panicked while loading the spec, a JSON body was sent as
{"type":"object"}, and a text/plain body as a dump of the schema struct.

Generate an example from the empty schema instead, and let the XML
branch take a string example as the body and warn on types it cannot
encode instead of panicking. The empty schema for
application/octet-stream has no example, so that body now gets the
placeholder the octet-stream branch already has for an empty value.
2026-09-28 01:52:46 +05:30
root-Manas cbbf15bc1e fix(openapi): select complete authentication requirements 2026-09-27 18:31:48 +05:30
root-Manas 591a019e83 fix(openapi): report missing credentials and reject empty values 2026-09-27 11:29:42 +05:30
root-Manas 5b4bc8efc8 fix(openapi): validate credentials per operation 2026-09-27 11:06:44 +05:30
root-Manas 8496c48c6f feat(openapi): support OAuth2 and OpenID Connect access tokens 2026-09-27 10:51:06 +05:30
dw1 c5611b9ac4 feat(js): expose SMB auth status & migrate goimpacket (#7780)
* feat(js): expose SMB auth status & migrate goimpacket

Add `SMBClient.Authenticate` to check session setup without accessing
shares. Report success and the server's guest and null-session flags so
callers can distinguish credential authentication from guest or anonymous
access. Return failures as errors and close the session after each call.

Switch to projectdiscovery/[email protected]. Adapt krbforge to upstream's
mandatory <username>.ccache output in the current directory. Validate
the destination against the existing file policy before ticket creation
and return the actual filename. Reject custom output paths and "-", as
upstream no longer supports custom paths or in-memory ticket generation.

Closes #7760
Closes #7752
Fixes #7761

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

* chore: satisfy lint

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

* fix(js): validate execution ID in smb.Authenticate

Return an error when the executionId context value is missing, empty, or
not a string. This prevents the unchecked type assertion from panicking
for missing or incorrectly typed values.

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

---------

Signed-off-by: Dwi Siswanto <[email protected]>
2026-09-26 08:59:56 +07:00
IgorDaniel45 ccd0aaca40 fix compiled cache after metadata load 2026-09-25 10:56:02 -03:00
Mzack9999 77d3b6d314 retry functional 2026-09-25 13:57:31 +02:00
Mr.XandDwi Siswanto c36316de36 chore(deps): bump github.com/projectdiscovery/[email protected] (#7766)
* Bump projectdiscovery/utils to pick up the zstd decoder leak fix

http.NewResponseChain() decompresses Content-Encoding: zstd responses
through utils's wrapDecodeReader(), which builds a *zstd.Decoder and
wraps it in io.NopCloser. The decoder is never closed, and klauspost's
zstd decoder keeps background worker goroutines alive until Close() is
called. When a response's decompressed size exceeds MaxBodyRead, the
LimitReader stops before EOF, the decoder's workers block permanently,
and the goroutines plus their window buffers leak for the life of the
process (projectdiscovery/nuclei#7749). gzip/deflate/brotli don't spawn
goroutines, so only zstd is affected.

The fix (github.com/projectdiscovery/utils@27864e5, PR #783) already
landed on utils's main branch, but no tagged release contains it yet
(latest is v0.11.5), so go.mod still points at v0.11.4. Bump to a
pseudo-version pinned at that commit as the interim step the issue
calls for, to be replaced by a tagged version once utils cuts one.

Verified with the deterministic reproducer from the issue report
(NewResponseChain + Fill against a >cap zstd body, 50 iterations):
goroutine count returns to baseline (delta=0) instead of leaking ~1
goroutine per response.

* chore(deps): bump github.com/projectdiscovery/[email protected].6

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

---------

Signed-off-by: Dwi Siswanto <[email protected]>
Co-authored-by: Dwi Siswanto <[email protected]>
2026-09-25 18:50:59 +07:00
Mzack9999 010ee044b2 fix inputs 2026-09-24 20:00:05 +02:00
Mzack9999 71f9782b2c Merge pull request #7777 from projectdiscovery/fix/7775-sonic-warning
fix(json): keep sonic out of builds it does not support
2026-09-24 19:50:48 +02:00
Mzack9999 536e427c01 Merge pull request #7776 from projectdiscovery/fix/7764-go-1.27
chore(deps): require go 1.27.1
2026-09-24 19:48:35 +02:00
Doğan Can Bakır ae203e7aad test(sdk): ignore the renamed http2 idle connection goroutine on go 1.27 2026-09-24 10:22:32 +03:00
Doğan Can Bakır 2b6b1d3371 fix(json): keep sonic out of builds it does not support 2026-09-24 10:07:28 +03:00
Doğan Can Bakır 3fdd8ca091 chore(deps): require go 1.27.1 for windows overlapped I/O fix 2026-09-24 09:30:27 +03:00
ghost af77e344f0 docs: update syntax & JSON schema 🤖 2026-09-24 06:17:10 +00:00
Doğan Can Bakır 1b244b2330 feat(llm): compare multiple responses with matcher inputs 2026-09-24 09:14:01 +03:00
Dogan Can Bakir fb1a318c6a Merge pull request #7773 from projectdiscovery/dwisiswant0/refactor/enforce-scan-network-restrictions-in-network-helpers
refactor: enforce scan network restrictions in network helpers
2026-09-24 09:13:43 +03:00
Dogan Can Bakir dbc69f7bf0 Merge pull request #7772 from projectdiscovery/feat/7744-llm-prompt-interpolation
feat(llm): interpolate variables and target values in llm prompts
2026-09-24 09:13:07 +03:00
Mzack9999 aed2cb4643 fix literals 2026-09-24 00:50:53 +02:00
Mzack9999 3fb407b345 restore tls 2026-09-23 23:47:09 +02:00
Mzack9999 15e2d88ca5 fix graphql 2026-09-23 21:58:13 +02:00
Mzack9999 c987c2df6a fix quarantine 2026-09-23 21:58:13 +02:00
Mzack9999 4d967cba5c merge dev 2026-09-23 20:43:05 +02:00
Mzack9999 f23992134f merge dev 2026-09-23 20:34:39 +02:00
Mzack9999 213d3badfd merge dev 2026-09-23 20:23:30 +02:00
Mzack9999 dece1c5ea9 fix helpers 2026-09-23 18:57:29 +02:00
Mzack9999 5e5758b7d3 fix interpolation 2026-09-23 14:23:46 +02:00
Mzack9999 06fb71ebf2 fix lint 2026-09-23 14:23:46 +02:00
Dwi Siswanto c50f92fdd3 refactor: enforce scan network restrictions in network helpers
Pass scan options through rendering, variables, matchers, and extractors
so resolve(), public_ip()/publicip(), and jarm() honor -lna and -eh.
These helpers previously performed network I/O outside the scan's network
policy.

Use the scan dialer for HTTP and JARM connections and reject restricted
IP addresses in DNS answers. Validate original and normalized hostnames
to prevent exclusion bypasses through DNS escapes and trailing dots.

Enforce -eh with or without -lna, including in unsigned templates.
Keep local access available without -lna unless excluded, and preserve
configured DNS resolvers, including local ones.

Bind helpers at evaluation time without storing scan state in cached
expressions. Reject network helper calls without initialized scan
context.

Fixes #7769

Signed-off-by: Dwi Siswanto <[email protected]>
2026-09-23 18:53:49 +07:00
Doğan Can Bakır 5b6510fb79 feat(llm): interpolate variables and target values in llm prompts 2026-09-23 12:09:18 +03:00
Dogan Can Bakir a5b59a9a4e Merge pull request #7765 from projectdiscovery/fix/7764-go-1.26.8
chore(deps): require go 1.26.8
2026-09-22 22:11:27 +03:00
hodeswildsmith455-boop 268ea525bd feat: add OrcaRouter as a first-class LLM provider with API key and PKCE login
Signed-off-by: hodeswildsmith455-boop <[email protected]>
2026-09-22 15:17:55 +00:00
Dogan Can Bakir 14283b0e13 Merge pull request #7735 from projectdiscovery/feat/llm-matchers
add llm matchers and extractors (semantic matching)
2026-09-22 16:59:11 +03:00
Doğan Can Bakır 322a40e1db fix(headless): subscribe wait-event before navigating 2026-09-22 16:03:27 +03:00
Dogan Can Bakir aa457f1c44 Merge pull request #7763 from projectdiscovery/feat/websocket-js-module
feat(js): add websocket module
2026-09-22 15:50:41 +03:00
Doğan Can Bakır bd0c398887 test(headless): cover wait-event missing events fired before waiting 2026-09-22 15:46:03 +03:00
Doğan Can Bakır b4a5cdab02 Merge remote-tracking branch 'origin/dev' into feat/llm-matchers
# Conflicts:
#	go.mod
#	go.sum
2026-09-22 15:26:55 +03:00
Doğan Can Bakır 1981edbaac chore(deps): require go 1.26.8 for windows overlapped I/O fix 2026-09-22 15:04:03 +03:00
Dogan Can Bakir 0e2fb326d6 Merge pull request #7759 from projectdiscovery/feat/per-target-profiles
feat(input): per-target profiles via target lines
2026-09-22 14:12:46 +03:00
Doğan Can Bakır ef0faa7d6f fix(js): answer pings between websocket fragments and release closed connections 2026-09-22 13:01:47 +03:00
Doğan Can Bakır e8b53505e5 fix(js): use projectdiscovery/goja import in bindgen template 2026-09-22 12:41:44 +03:00
Doğan Can Bakır 1d6b532bbc docs(js): note which headers websocket ResponseHeaders omits 2026-09-22 11:58:05 +03:00
Doğan Can Bakır b6cea874d3 feat(js): register websocket module with bindings and integration tests 2026-09-22 11:31:32 +03:00
Doğan Can Bakır 36dafa988b feat(js): add websocket module 2026-09-22 11:16:06 +03:00
Mzack9999 d7bdb9c058 Merge pull request #7746 from dwisiswant0/dwisiswant0/fix/dedupe/preserve-input-identity-serialize-indexing
fix(dedupe): preserve input identity & serialize indexing
2026-09-21 21:46:03 +02:00
Doğan Can Bakır f3c12c43eb fix(input): accept only profile IDs in target lines 2026-09-21 18:18:43 +03:00
Doğan Can Bakır 4dad173f0a fix(input): load every template when a plain duplicate lifts a restriction 2026-09-21 18:14:13 +03:00
Doğan Can Bakır 1b557852c4 test(input): cover target lines in list provider 2026-09-21 16:38:46 +03:00
Doğan Can Bakır e5052eb771 docs(input): document per-target profiles 2026-09-21 16:37:56 +03:00
Doğan Can Bakır 56411349d2 fix(input): match multi-word tech names by each word 2026-09-21 16:33:10 +03:00
Doğan Can Bakır 77dda01e67 feat(core): run per-target profiles in engine, clusters and loader 2026-09-21 15:53:54 +03:00
Doğan Can Bakır a7faddf748 feat(input): parse per-target profile lines into a target scope registry 2026-09-21 15:47:11 +03:00
dependabot[bot] d47ace7701 chore(deps): bump the modules group with 8 updates (#7758)
Bumps the modules group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/projectdiscovery/fastdialer](https://github.com/projectdiscovery/fastdialer) | `0.5.20` | `0.5.21` |
| [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go) | `1.3.26` | `1.3.27` |
| [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) | `0.8.23-0.20260910174914-8e1207b367b8` | `0.8.24` |
| [github.com/projectdiscovery/networkpolicy](https://github.com/projectdiscovery/networkpolicy) | `0.1.50` | `0.1.51` |
| [github.com/projectdiscovery/ratelimit](https://github.com/projectdiscovery/ratelimit) | `0.0.90-0.20260912164306-aeeca5645294` | `0.0.90` |
| [github.com/projectdiscovery/utils](https://github.com/projectdiscovery/utils) | `0.11.4-0.20260910160917-83d6b6e62668` | `0.11.4-0.20260911153819-02811729e1f0` |
| [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) | `0.3.0` | `0.3.1` |
| [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck) | `1.3.0` | `1.3.1` |


Updates `github.com/projectdiscovery/fastdialer` from 0.5.20 to 0.5.21
- [Release notes](https://github.com/projectdiscovery/fastdialer/releases)
- [Commits](https://github.com/projectdiscovery/fastdialer/compare/v0.5.20...v0.5.21)

Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.26 to 1.3.27
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](https://github.com/projectdiscovery/retryablehttp-go/compare/v1.3.26...v1.3.27)

Updates `github.com/projectdiscovery/dsl` from 0.8.23-0.20260910174914-8e1207b367b8 to 0.8.24
- [Release notes](https://github.com/projectdiscovery/dsl/releases)
- [Commits](https://github.com/projectdiscovery/dsl/commits/v0.8.24)

Updates `github.com/projectdiscovery/networkpolicy` from 0.1.50 to 0.1.51
- [Release notes](https://github.com/projectdiscovery/networkpolicy/releases)
- [Commits](https://github.com/projectdiscovery/networkpolicy/compare/v0.1.50...v0.1.51)

Updates `github.com/projectdiscovery/ratelimit` from 0.0.90-0.20260912164306-aeeca5645294 to 0.0.90
- [Release notes](https://github.com/projectdiscovery/ratelimit/releases)
- [Commits](https://github.com/projectdiscovery/ratelimit/commits/v0.0.90)

Updates `github.com/projectdiscovery/utils` from 0.11.4-0.20260910160917-83d6b6e62668 to 0.11.4-0.20260911153819-02811729e1f0
- [Release notes](https://github.com/projectdiscovery/utils/releases)
- [Changelog](https://github.com/projectdiscovery/utils/blob/main/CHANGELOG.md)
- [Commits](https://github.com/projectdiscovery/utils/commits)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.3.0 to 0.3.1
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.3.0...v0.3.1)

Updates `github.com/projectdiscovery/cdncheck` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](https://github.com/projectdiscovery/cdncheck/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/fastdialer
  dependency-version: 0.5.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/dsl
  dependency-version: 0.8.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/networkpolicy
  dependency-version: 0.1.51
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/ratelimit
  dependency-version: 0.0.90
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/utils
  dependency-version: 0.11.4-0.20260911153819-02811729e1f0
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.3.1
  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-09-21 06:22:44 +00:00
Bounty Hunter 0270f5f1da Add test case for missing dialers in JS protocol 2026-09-18 21:57:16 +03:00
Bounty Hunter 9cf5459dd9 Fix dialer panics in JS protocol and input providers 2026-09-18 21:45:44 +03:00
dependabot[bot] 37c06098ad chore(deps): bump google.golang.org/grpc (#7754)
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.83.1 to 1.83.2
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.83.1...v1.83.2)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.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-09-18 00:53:21 +00:00
gnuletikandClaude Opus 5 ff7b951bf0 style: drop the redundant embedded Operators selector
staticcheck QF1008: Operators is embedded in Request, so Matchers and
Extractors are reachable directly.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-17 22:18:59 +02:00
gnuletikandClaude Opus 5 4a6aed5eab perf(http): build the full response only when an operator reads it
executeRequest calls respChain.FullResponseString() for every response.
That allocates a copy of headers+body (a make plus two copies) and keeps
it reachable for as long as the event is -- including inside the
interactsh request cache, which holds events until their OAST callback
arrives or the eviction window closes.

Almost nothing reads it. Across the public template corpus only 81 of
11,634 http templates (0.7%) reference "response" or part "all". The rest
match on body or headers, which are already materialised separately.

Gate the build on a scan of the request's own matchers and extractors,
plus the debug and fuzz-stats paths that consume it independently of any
template.

Measured against 4 hosts serving 2MB responses, 4 concurrent runs, with
GOMEMLIMIT at 1.73GiB:

  full profile (73,856 requests)   9,559MB -> 4,760MB peak
  http/vulnerabilities/ subset     7,445MB -> 3,796MB peak

Behaviour is unchanged for part: response, part: body, part: all and a
dsl contains(response, ...) expression, checked against a target whose
token appears only in a response header, so the concatenation is the
only thing that can match it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-17 19:59:43 +02:00
MGreif 228cf4b9a2 Change test 2026-09-16 09:57:18 +02:00
MGreif f5dcb9bf31 Add nil receiver guard and regression test 2026-09-16 09:48:02 +02:00
MGreif b9f971c8fa Add: cb syncing 2026-09-16 09:34:44 +02:00
MGreif 65964f2819 Add back rebase corruptions 2026-09-16 09:14:11 +02:00
MGreif 40715999a5 Change: Rename interface 2026-09-16 09:08:28 +02:00
Doğan Can Bakır 12fd62163b use a process-stable framing marker so llm cache still hits 2026-09-15 15:54:37 +03:00
Dwi Siswanto 748ff1381e fix(dedupe): preserve input identity & serialize indexing
Include Port, Scheme, and URL in the deduplication key so distinct
inputs that redirect to the same matched URL remain separate findings.
Encode field and collection lengths to prevent ambiguous keys, and sort
metadata keys to make hashing deterministic.

Protect the LevelDB lookup and insert with a mutex to prevent
concurrent calls from accepting the same finding more than once.

Version the stored keys and document the upgrade behavior. Existing
report databases may report previous findings once again because old
hashes cannot recover the missing input fields.

Fixes #7745

Signed-off-by: Dwi Siswanto <[email protected]>
2026-09-15 19:20:46 +07:00
Doğan Can Bakır 026a1890b8 Merge remote-tracking branch 'origin/dev' into feat/llm-matchers
# Conflicts:
#	go.mod
#	go.sum
2026-09-15 12:51:00 +03:00
Doğan Can Bakır e14933f765 frame llm response with a random boundary to resist prompt injection 2026-09-15 12:44:43 +03:00
MGreif 223daf2615 Fix: tests 2026-09-15 11:11:12 +02:00
MGreif 63c656f7ee Add: interactsh-client interface for modular design
# Conflicts:
#	pkg/protocols/common/interactsh/interactsh.go
#	pkg/protocols/protocols.go
2026-09-15 11:11:10 +02:00
Mzack9999 d1ad609601 seed fix 2026-09-15 00:03:39 +02:00
Mzack9999 a165326e88 contradiction pruning 2026-09-14 23:25:49 +02:00
Mzack9999 9757c351d0 lossless techfilter 2026-09-14 21:46:15 +02:00
Mzack9999 8cab07405a race fix 2026-09-14 19:48:39 +02:00
Mzack9999 7cfffc9826 cache eligibility 2026-09-14 18:59:49 +02:00
Mzack9999 4c59098740 response cache 2026-09-14 18:11:19 +02:00
Doğan Can Bakır bd639a5aff document llm flags in readmes 2026-09-14 16:00:46 +03:00
Doğan Can Bakır fcba75d191 record model verdict on llm findings and regen schema 2026-09-14 15:28:32 +03:00
Doğan Can Bakır 33067508b1 bump utils to v0.11.4 2026-09-14 15:03:13 +03:00
Doğan Can Bakır d4a40d2b04 Merge remote-tracking branch 'origin/dev' into feat/llm-matchers
# Conflicts:
#	go.mod
#	go.sum
2026-09-14 14:36:46 +03:00
Doğan Can Bakır c063d77d2d run cheap matchers before llm and defer llm extractors 2026-09-14 14:21:04 +03:00
Doğan Can Bakır ddb2edb2e9 harden llm matchers: no negative, confidence floor, scoped client, http only 2026-09-14 14:00:06 +03:00
Doğan Can Bakır fe14cf5d2c reject llm as sole matcher on high or critical templates 2026-09-11 18:26:37 +03:00
Doğan Can Bakır 89db6cf43a add llm extractor and share client with matcher 2026-09-11 18:08:53 +03:00
Doğan Can Bakır a78ea81854 wire llm matcher client, flags, and capability gate 2026-09-11 17:51:19 +03:00
Mzack9999 927f30464e fix race 2026-09-05 17:48:50 +04:00
Mzack9999 bf11a866e1 fix lint 2026-09-05 16:09:50 +04:00
Mzack9999 ca0688f086 clone variables 2026-09-05 15:55:38 +04:00
Mzack9999 8f87aa7440 normalize context 2026-09-05 14:27:44 +04:00
Mzack9999 b00db1d984 cancel listing 2026-09-05 14:17:05 +04:00
Mzack9999 db2b48a3a0 stop enumeration 2026-09-05 13:07:11 +04:00
Mzack9999 e505fb840e restore signature 2026-09-05 12:55:05 +04:00
Mzack9999 7496794e58 honour cancellation 2026-09-05 12:42:01 +04:00
Mzack9999 b996c5d4ce merge dev 2026-09-05 11:59:54 +04:00
IgorDaniel45 eded59972e fix: address compiled cache review feedback 2026-09-02 19:13:00 -03:00
Igor Daniel 4fb773d8b2 fix: isolate cached template execution state 2026-09-02 17:58:59 -03:00
Igor Daniel 9f47943e11 fix: reuse compiled templates in thread-safe engine 2026-09-02 16:49:30 -03:00
Mzack9999 aca9665de0 merge dev 2026-09-02 22:18:26 +04:00
chauhand2463 a1c26f9d98 feat(http): expose TLS key exchange metadata 2026-08-28 09:41:07 +05:30
Mzack9999 034cf04d47 quarantine hosts 2026-08-26 10:37:40 +04:00
gnuletik 841cd851ca fix(core): honour the scan context when acquiring worker slots and JS runtimes
Four acquires in the scan path ignored the context they were given, so a
cancelled scan could not leave until unrelated work finished:

- executeTemplateSpray and executeTemplatesOnTarget acquire a worker slot via
  AdaptiveWaitGroup.Add, which passes context.Background().
- executeHostSpray does the same.
- flow.GetJSRuntime acquires from a process-global goja pool with
  sizedpool.Get(context.TODO()).

Each of these can wait a long time. A worker slot is held for as long as a
template runs, and a JS runtime stays checked out for the whole flow including
any wait on the rate limiter, so the pool saturates under concurrent scans.
A scan whose context is already cancelled then queues for a runtime it only
needs in order to unwind, and its own cancellation cannot break the wait.

Both spray loops already check ctx.Done() at the top of each iteration, so the
acquire was the only place cancellation was invisible.

GetJSRuntime now takes a context and returns an error. This changes an
exported signature; it has one caller in-tree.
2026-08-26 03:15:52 +02:00
prithvee07 59f8657566 Merge pull request #6 from prithvee07/codex/review-the-codebase-r6nml5
Harden markdown report filenames, improve HTTP input parsing/progress, and upgrade dependencies
2026-08-18 22:14:01 +05:30
prithvee07 7a1d539d7e Merge branch 'dev' into codex/review-the-codebase-r6nml5 2026-08-18 21:32:29 +05:30
Claude 03adf593d4 Merge remote-tracking branch 'upstream/dev' into dev
# Conflicts:
#	pkg/input/types/http.go
2026-08-18 15:54:52 +00:00
prithvee07 fd91245001 Merge pull request #7 from prithvee07/claude/project-contribution-work-gwxmho
Fix panic when parsing unsafe HTTP requests with only method
2026-08-18 21:13:06 +05:30
Claude e6f6274e99 Fix index-out-of-range panic parsing unsafe raw requests with no path
readRawRequest() skips the malformed-field guard when unsafe is true,
so a raw request line with a single token (e.g. "GET\n", no path or
HTTP version) falls through to rawRequest.Path = parts[1] and panics
with "index out of range [1] with length 1". Since unsafe mode exists
specifically to tolerate malformed request lines, this is directly
reachable via a template's raw HTTP request block.

Guard the assignment so a one-token request line degrades to an empty
path instead of panicking; the existing case rawrequest.Path == "" in
Parse() already handles an empty path correctly by automerging with
the input URL.
2026-08-18 15:41:34 +00:00
Mzack9999 b6e4a612f7 harden cache 2026-08-15 18:44:01 +04:00
Mzack9999 d45d0b3e41 merge dev 2026-08-15 18:37:35 +04:00
Matin 0316732363 docs: update heading level to preserve outline 2026-08-13 19:59:04 +04:30
hu-matin 960ba0bb7f docs: add image alt tags and fix table of contents links 2026-08-13 19:51:25 +04:30
hu-matin 04c6db7de0 docs: link Persian README in main README 2026-08-13 13:12:29 +04:30
hu-matin caa5dfafea docs: add Persian(FA) translation for README 2026-08-13 12:50:42 +04:30
Mzack9999 5916da0d03 harden filters 2026-08-11 20:10:40 +04:00
Mzack9999 b91ebe9a62 fix ci 2026-08-11 20:01:53 +04:00
Mzack9999 89e5323d4d Merge branch 'dev' into feat-automatic-scan 2026-08-11 19:52:51 +04:00
Mzack9999 dceb85759e tech filter 2026-08-11 19:51:46 +04:00
Mzack9999 f7d051a0eb scan plan 2026-08-11 13:27:56 +04:00
prithvee07 8a3a0f33de fix(input): parse HTTP authority case-insensitively 2026-08-04 11:16:50 +05:30
aviadavi 0f5934cfaa test: assert unsafe raw -H evaluation on the generated wire bytes
The previous test called the resolver directly, so it passed even with the
TryFillCustomHeaders wiring reverted and therefore guarded nothing.

Drive real request generation instead and read UnsafeRawBytes, asserting
the marker is absent, the tag survives, and the user agent still varies
across requests. Confirmed by reverting the fix: the test now fails with
"expression reached the wire unevaluated".
2026-08-03 17:18:04 +03:00
aviadavi bfd48c7740 fix: evaluate -H expressions on the unsafe raw request path too
Unsafe raw requests do not go through setCustomHeaders for the value that
reaches the wire: TryFillCustomHeaders splices the -H line straight into
UnsafeRawBytes, and rawhttp sends those bytes. So a header carrying an
expression was sent with the literal {{...}} marker intact.

Evaluate the -H strings before they are spliced in, and share one
evaluation helper between both paths so they cannot drift.

Verified against a local server with an unsafe: true template:

  before: {{rand_user_agent()}} myprop/value
  after:  Mozilla/5.0 (Macintosh ...) Safari/605.1.15 myprop/value
2026-08-03 17:18:04 +03:00
aviadavi 92f3a55ee7 feat: evaluate DSL helpers in -H values per request
Adds a rand_user_agent() DSL helper and evaluates helper expressions in
global custom header values once per generated request.

This implements the approach proposed in #7446 instead of a dedicated
CLI flag, so attribution tagging needs no new global option:

  -H "User-Agent: {{rand_user_agent()}} myprop/value"

The helper is registered with cacheable=false, otherwise a cached result
would freeze the user agent for the whole run. It is backed by the
existing projectdiscovery/useragent pool that nuclei already uses for UA
randomization, so tagged scans send the same class of user agent as
untagged ones and no new dependency is introduced. The name matches the
slug dsl.FakerFunctions() would expose, so wiring faker later is a
drop in.

Values come from the operator's own -H flag or config file, the same
trust boundary as a template, and response data is never evaluated here.
Values with no {{ }} marker skip evaluation entirely, and compile or
evaluation errors keep the original value so a typo degrades the header
rather than failing the scan. The unsafe raw request path is unchanged.
2026-08-03 17:18:04 +03:00
prithvee07 b19e739961 Merge branch 'projectdiscovery:dev' into dev 2026-08-03 17:03:26 +05:30
prithvee07 b24ade541a Merge pull request #5 from prithvee07/claude/codebase-review-3q1x4d
Add ReadHeaderTimeout to DASTServer's http.Server
2026-08-03 12:01:27 +05:30
Claude 58d3ad6063 Add ReadHeaderTimeout to DASTServer's http.Server
The DAST fuzzing/stats server (-dast-server) constructed its http.Server
with no timeouts, leaving it open to a Slowloris-style slow-header DoS.
Only ReadHeaderTimeout is set (not ReadTimeout/WriteTimeout) since /fuzz
requests can legitimately take a while to process. Same class of fix
already applied to internal/httpapi/apiendpoint.go.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
2026-08-03 06:21:20 +00:00
prithvee07 1ff643d72a Merge branch 'projectdiscovery:dev' into dev 2026-08-03 11:44:22 +05:30
prithvee07 7f2254ae57 Merge pull request #4 from prithvee07/claude/codebase-review-3q1x4d
Fix ParseRawRequest header/host parsing panics and data loss
2026-08-03 11:42:56 +05:30
Claude 5371ac689a Fix ParseRawRequest header/host parsing panics and data loss
ParseRawRequest read the request line and headers positionally: the
second line was always treated as the Host line regardless of content,
and header values were read by skipping exactly one byte past the
colon. This meant:
- a valueless header (e.g. "X-Empty:") panicked with a slice-bounds
  error, taking down the whole scan
- a Host header anywhere but the second line was silently dropped from
  the header map, so header-based preconditions never matched
- "Host:target" (no space after colon) parsed the host as "arget"
- an absolute-form request target, as used by proxy captures
  (GET http://target/p HTTP/1.1), got appended to the Host rather than
  replacing it

Every request-shaped input mode (burp, jsonl, yaml, openapi) goes
through this parser. Parse headers uniformly first, look up Host by
name, trim header values instead of assuming one space, and treat an
absolute-form request target as replacing the authority outright -
matching how pkg/protocols/http/raw.readRawRequest already handles
raw template requests.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
2026-08-03 06:08:52 +00:00
prithvee07 675fcc4ea8 Merge pull request #3 from prithvee07/claude/codebase-review-3q1x4d
Security hardening: Add HTTP API token auth and improve file permissions
2026-08-03 11:28:32 +05:30
Claude a20aa26024 Fix pprof/auth exposure on http-api-endpoint and harden file permissions
The experimental -http-api-endpoint server used http.ListenAndServe(addr,
nil), which serves Go's global DefaultServeMux. Since net/http/pprof is
blank-imported elsewhere in the binary for the separate, opt-in
-enable-pprof feature, enabling -http-api-endpoint silently exposed
/debug/pprof/* (process cmdline, heap/goroutine dumps, CPU profiling) on
the same address. The endpoint's PUT /api/concurrency also had no
authentication. Give the server its own ServeMux, add an optional
-http-api-token flag (mirroring -dast-server-token) with a warning when
unset, and add request timeouts.

Also tighten file/directory permissions (0644->0600, 0755/0775->0750)
across report exporters, the template installer/signer, custom-template
downloaders, and output writers, so scan results and downloaded
templates aren't left world-readable on shared systems.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
2026-08-03 05:54:23 +00:00
prithvee07 06771d7197 Merge pull request #2 from prithvee07/codex/review-the-codebase
Harden Markdown report filenames and update Go dependencies (go1.26.5, go-github v81)

# Markdown and security change report

This file is a standalone, downloadable summary of the Markdown report filename
hardening and Go vulnerability remediation work.

## Executive summary

The change set:

- prevents Markdown report paths from being influenced by directory traversal
  sequences;
- prevents long template IDs and hosts from removing the per-finding UUID;
- includes the selected matcher or extractor name in result filenames;
- truncates filenames without producing invalid UTF-8;
- updates the Go toolchain and vulnerable dependencies; and
- reduces reachable `govulncheck` findings from 23 to one upstream advisory
  with no fixed dependency version.

## Markdown filename hardening

Markdown finding files use this structure:

```text
<template-id>-<host>-<uuid>-<matcher-or-extractor>.md
```

The matcher name is selected when both a matcher and extractor name are
available. Extractor-only findings use the extractor name.

Before a file is written, unsafe filesystem characters, path separators, and
parent-directory references are replaced. The resulting filename is limited to
255 bytes and truncated only at a valid UTF-8 boundary. Prefix truncation does
not remove the UUID or `.md` extension, so findings cannot collide merely
because their long template and host prefixes are identical.

## Dependency remediation

| Component | Previous version | Updated version |
| --- | --- | --- |
| Go toolchain | 1.26.0 | 1.26.5 |
| `golang.org/x/text` | v0.38.0 | v0.39.0 |
| `github.com/yuin/goldmark` | v1.7.13 | v1.7.17 |
| Direct `github.com/google/go-github` use | v30.1.0 | v81.0.0 |

The direct GitHub client migration covers the custom-template downloader and
GitHub reporting tracker. Module metadata was refreshed after the upgrades.

## Vulnerability scan result

The initial source scan reported 23 reachable vulnerabilities. After the
updates, the scan reports only GO-2026-5932.

GO-2026-5932 remains reachable through the latest available
`github.com/projectdiscovery/utils/update` dependency, which transitively uses
`github.com/google/go-github/v30` and `golang.org/x/crypto/openpgp`. The advisory
does not identify a fixed `golang.org/x/crypto` release. Eliminating this final
result requires the upstream update package to migrate away from the older
GitHub client or a replacement of that update subsystem.

Run the vulnerability scan with:

```console
go run golang.org/x/vuln/cmd/govulncheck@latest \
  -db=https://storage.googleapis.com/go-vulndb ./...
```

An exit status indicating GO-2026-5932 is expected until the upstream
dependency is migrated.

## Validation commands

```console
go mod verify
go test ./pkg/reporting/exporters/markdown
go test ./pkg/external/customtemplates -run '^$'
go test ./pkg/reporting/trackers/github
go vet ./pkg/external/customtemplates \
  ./pkg/reporting/trackers/github \
  ./pkg/reporting/exporters/markdown
```

The `-run '^$'` custom-template command checks compilation without running the
network-dependent GitHub download tests.

## Detailed documentation

- [Markdown report filenames](markdown-report-filenames.md)
- [Go vulnerability remediation](security-remediation.md)
2026-08-02 22:56:16 +05:30
prithvee07 efbb48c167 docs: add downloadable change report 2026-08-02 22:55:13 +05:30
prithvee07 c03f5c8df1 Merge pull request #1 from prithvee07/codex/review-the-codebase
Sanitize/truncate markdown report filenames; update GitHub client and dependency versions
2026-08-02 22:41:13 +05:30
prithvee07 865c90e627 fix(deps): remediate reachable Go vulnerabilities 2026-08-02 22:40:26 +05:30
Mzack9999 40bd7f1777 strict probe 2026-07-30 22:34:07 +04:00
Mzack9999 441b37db74 idle conns 2026-07-30 16:01:58 +04:00
Jiho Lee 2766609119 fix(extractors): reject a regex group the pattern cannot produce
ExtractRegex skips any submatch where len(match) < RegexGroup+1, and
FindAllStringSubmatch returns exactly NumSubexp()+1 entries. So a group
index beyond the pattern's capture count can never match: the template
compiles, runs, and silently extracts nothing. An empty result is
indistinguishable from 'the pattern did not match the target', which is
the same silent-failure class as the negative-group case already
rejected two lines above.

Scanning nuclei-templates (13451 files, 2100 regex extractors with
group > 0) finds 4 real instances, and they look like genuine bugs
rather than false positives. The clearest is
smtp-credentials-exposure.yaml, where two patterns share one group: 1:

    regex:
      - smtp_username":".*"                        <- 0 groups
      - <smtp_username>(.*)</smtp_username>       <- 1 group

Against a body containing both forms it extracts only the XML one; the
JSON credential is missed with no indication. That template has been
half-working since it was written.

Validation runs on the cached-regex branch too. The compile path
short-circuits on a cache hit, so checking only the freshly-compiled
branch would let the same pattern through unvalidated as soon as any
other template had already compiled it.

The fuzz harness picks a group index independently of its patterns (its
own defaults include the group-less https?://[^\s"']+), so it now
clamps the group to what the narrowest pattern captures -- otherwise it
would fail at compile time on inputs it deliberately generates, instead
of exercising extraction.

Tests fail without the fix: removing both call sites gives 2 failures,
and removing only the cached-branch check still fails the cache test.
2026-07-26 23:45:05 +09:00
Mzack9999 e30c800e8c graphql fuzz 2026-07-24 17:44:01 +04:00
389 changed files with 34201 additions and 2101 deletions

No files matched your search

+21 -17
View File
@@ -17,6 +17,8 @@ concurrency:
cancel-in-progress: true
jobs:
# lint gates every other job; none of them uses another's output, so they run
# in parallel once it passes instead of queueing behind the unit tests
lint:
name: "Lint"
if: ${{ !endsWith(github.actor, '[bot]') }}
@@ -45,16 +47,6 @@ jobs:
- uses: actions/[email protected]
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- uses: projectdiscovery/actions/free-disk-space@v1
with:
llvm: 'false'
php: 'false'
mongodb: 'false'
mysql: 'false'
misc-packages: 'false'
docker-images: 'false'
tools-cache: 'false'
- run: make build
# The data-race detector is OS-independent, so we only pay its ~2-3x cost
# on ubuntu; windows/macOS run the same suite without instrumentation.
- name: "Unit tests (race)"
@@ -85,7 +77,7 @@ jobs:
sdk:
name: "Run example SDK"
needs: ["tests"]
needs: ["lint"]
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: "${{ github.token }}"
@@ -106,7 +98,7 @@ jobs:
integration:
name: "Integration tests"
needs: ["tests"]
needs: ["lint"]
env:
GITHUB_TOKEN: "${{ github.token }}"
strategy:
@@ -119,6 +111,18 @@ jobs:
- uses: projectdiscovery/actions/setup/python@v1
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
# The suite launches one nuclei process per case, ~175 of them, and the
# binary is around 200MB. This job takes roughly four times longer on
# windows than on linux for the same work; Defender scanning an executable
# on every launch is the leading suspect for the difference.
- name: Exclude the workspace from Defender scanning
if: ${{ matrix.os == 'windows-latest' }}
shell: pwsh
run: |
Add-MpPreference -ExclusionPath "${{ github.workspace }}"
Add-MpPreference -ExclusionPath "$env:USERPROFILE\go"
Add-MpPreference -ExclusionProcess "nuclei.exe"
Add-MpPreference -ExclusionProcess "go.exe"
- run: make integration
env:
PDCP_API_KEY: "${{ secrets.PDCP_API_KEY }}"
@@ -126,7 +130,7 @@ jobs:
functional:
name: "Functional tests"
needs: ["tests"]
needs: ["lint"]
env:
GITHUB_TOKEN: "${{ github.token }}"
strategy:
@@ -147,7 +151,7 @@ jobs:
validate:
name: "Template validate"
needs: ["tests"]
needs: ["lint"]
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: "${{ github.token }}"
@@ -159,7 +163,7 @@ jobs:
codeql:
name: "CodeQL analysis"
needs: ["tests"]
needs: ["lint"]
runs-on: ubuntu-latest
permissions:
actions: read
@@ -201,7 +205,7 @@ jobs:
release:
name: "Release test"
needs: ["tests"]
needs: ["lint"]
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
@@ -210,7 +214,7 @@ jobs:
flamegraph:
name: "Flamegraph"
needs: ["tests"]
needs: ["lint"]
uses: ./.github/workflows/flamegraph.yaml
# perf-regression:
+1 -1
View File
@@ -17,4 +17,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: crate-ci/typos@v1.49.0
- uses: crate-ci/typos@v1.51.1
+2 -2
View File
@@ -94,7 +94,7 @@ test:
# Keep this under the GitHub Actions step timeout (50m) so a hung nuclei process
# is reported by go test instead of the runner killing the job with no test name.
integration:
$(GOTEST) -tags=integration -timeout 40m ./internal/tests/integration
$(GOTEST) -tags=integration -timeout 40m -v ./internal/tests/integration
integration-debug:
$(GOTEST) -tags=integration ./internal/tests/integration -v $(GO_TEST_ARGS) -args $(INTEGRATION_ARGS)
@@ -112,7 +112,7 @@ functional: build
exit 1; \
fi; \
RELEASE_BINARY="$$release_binary" DEV_BINARY="$(PWD)/bin/nuclei" \
$(GOTEST) -tags=functional -timeout 1h ./internal/tests/functional
$(GOTEST) -tags=functional -timeout 1h -v ./internal/tests/functional
tidy:
$(GOMOD) tidy
+29 -9
View File
@@ -1,7 +1,7 @@
![nuclei](/static/nuclei-cover-image.png)
<div align="center">
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
@@ -9,8 +9,9 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
<p align="center">
@@ -110,7 +111,7 @@ Browse the full Nuclei [**`documentation here`**](https://docs.projectdiscovery.
### Installation
`nuclei` requires **go >= 1.24.2** to install successfully. Run the following command to get the repo:
`nuclei` requires **go >= 1.27.3** to install successfully. Run the following command to get the repo:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -145,6 +146,8 @@ TARGET:
-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)
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
@@ -173,6 +176,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -239,9 +250,11 @@ CONFIGURATIONS:
-lna, -restrict-local-network-access blocks connections to the local / private network
-i, -interface string network interface to use for network scan
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -284,6 +297,8 @@ 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)
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
-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)
@@ -308,7 +323,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -327,6 +345,7 @@ DEBUG:
-dresp, -debug-resp show all received responses
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
-pi, -proxy-internal proxy all internal requests
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
-ldf, -list-dsl-function list all supported DSL function signatures
-tlog, -trace-log string file to write sent requests trace log
-elog, -error-log string file to write sent requests error log
@@ -342,10 +361,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
@@ -667,7 +687,7 @@ Thanks to all the amazing [**`community contributors for sending PRs`**](https:/
<br>
<div align="center">
<sub>**`nuclei`** is distributed under [**MIT License**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md)</sub>
</div>
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ _针对安全团队和企业,我们提供了构建在 Nuclei OSS 之上的云
### 安装
`nuclei` 需要 **go >= 1.24.2** 才能成功安装。运行以下命令获取仓库:
`nuclei` 需要 **go >= 1.27.3** 才能成功安装。运行以下命令获取仓库:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Consulta la [**`documentación completa de Nuclei aquí`**](https://docs.project
### Instalación
`nuclei` requiere **go >= 1.24.2** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
`nuclei` requiere **go >= 1.27.3** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+728
View File
@@ -0,0 +1,728 @@
![nuclei](/static/nuclei-cover-image.png)
<div align="center">
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ID.md">`Indonesia`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
<p align="center">
<a href="https://docs.projectdiscovery.io/tools/nuclei/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Documentation-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1ib29rLW9wZW4iPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjwvc3ZnPg==&logoColor=white" alt="Documentation Badge"></a>
&nbsp;&nbsp;
<a href="https://github.com/projectdiscovery/nuclei-templates"><img src="https://img.shields.io/badge/Templates Library-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoaWVsZCI+PHBhdGggZD0iTTIwIDEzYzAgNS0zLjUgNy41LTcuNjYgOC45NWExIDEgMCAwIDEtLjY3LS4wMUM3LjUgMjAuNSA0IDE4IDQgMTNWNmExIDEgMCAwIDEgMS0xYzIgMCA0LjUtMS4yIDYuMjQtMi43MmExLjE3IDEuMTcgMCAwIDEgMS41MiAwQzE0LjUxIDMuODEgMTcgNSAxOSA1YTEgMSAwIDAgMSAxIDF6Ii8+PC9zdmc+&logoColor=white"></a>
&nbsp;&nbsp;
<a href="https://discord.gg/projectdiscovery?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Discord Badge"></a>
<hr>
</p>
<div dir="rtl">
<br>
<br>
**Nuclei یک اسکنر آسیب‌پذیری مدرن و با کارایی بالاست که از قالب‌های ساده بر پایه YAML استفاده می‌کند. این ابزار به شما امکان می‌دهد سناریوهای سفارشی برای شناسایی آسیب‌پذیری‌ها طراحی کنید که شرایط واقعی را شبیه‌سازی می‌کنند و منجر به صفر شدن خطاهای مثبت کاذب (گزارش‌های اشتباه) می‌شوند.**
- فرمت ساده YAML برای ساخت و سفارشی‌سازی قالب‌های آسیب‌پذیری.
- مشارکت توسط هزاران متخصص امنیت جهت مقابله با آسیب‌پذیری‌های نوظهور و رایج.
- کاهش خطاهای مثبت کاذب با شبیه‌سازی مراحل واقعی جهت تایید یک آسیب‌پذیری.
- پردازش موازی فوق‌سریع اسکن‌ها و خوشه‌بندی درخواست‌ها.
- قابلیت ادغام در خطوط لوله CI/CD برای شناسایی آسیب‌پذیری‌ها و تست‌های رگرسیون.
- پشتیبانی از پروتکل‌های متعدد مانند TCP، DNS، HTTP، SSL، WHOIS، جاوااسکریپت، کد و موارد دیگر.
- قابلیت یکپارچه‌سازی با Jira، Splunk، GitHub، Elastic و GitLab.
<br>
<br>
## فهرست مطالب
- [**`شروع کار`**](#شروع-کار)
- [_`۱. ابزار خط فرمان Nuclei`_](#۱-ابزار-خط-فرمان-nuclei)
- [_`۲. نسخه‌های حرفه‌ای و سازمانی`_](#۲-نسخه‌های-حرفه‌ای-و-سازمانی)
- [**`مستندات`**](#مستندات)
- [_`دستورات خط فرمان`_](#دستورات-خط-فرمان)
- [_`اسکن یک هدف واحد`_](#اسکن-یک-هدف)
- [_`اسکن اهداف متعدد`_](#اسکن-چندین-هدف)
- [_`اسکن شبکه`_](#اسکن-شبکه)
- [_`اسکن با قالب سفارشی شما`_](#اسکن-با-استفاده-از-قالب-سفارشی-خودتان)
- [_`اتصال Nuclei به ProjectDiscovery`_](#اتصال-nuclei-به-projectdiscovery)
- [**`قالب‌های Nuclei، جامعه کاربری و پاداش‌ها`**](#قالب‌های-nuclei-جامعه-و-پاداش‌ها-) 💎
- [**`ماموریت ما`**](#ماموریت-ما)
- [**`مشارکت‌کنندگان`**](#مشارکت‌کنندگان-) ❤
- [**`مجوز استفاده`**](#license)
<br>
<br>
## شروع کار
### ۱. ابزار خط فرمان Nuclei
_ابزار Nuclei را روی سیستم خود نصب کنید. کار خود را با دنبال کردن راهنمای نصب در [**`اینجا`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) آغاز کنید. علاوه بر این، ما [**`یک سطح ابری رایگان`**](https://cloud.projectdiscovery.io/sign-up) ارائه می‌دهیم که همراه با محدودیت‌های رایگان ماهانه سخاوتمندانه‌ای عرضه می‌شود:_
- ذخیره‌سازی و بصری‌سازی یافته‌های آسیب‌پذیری شما
- نوشتن و مدیریت قالب‌های Nuclei شما
- دسترسی به آخرین قالب‌های Nuclei
- شناسایی و ذخیره‌سازی اهداف شما
> [!Important]
> |**این پروژه در حال توسعه فعال است**. انتظار تغییرات اساسی در نسخه‌های جدید را داشته باشید. لطفاً قبل از به‌روزرسانی، لیست تغییرات نسخه (Changelog) را بررسی کنید.|
> |:--------------------------------|
> | این پروژه در درجه اول برای استفاده به عنوان یک ابزار خط فرمان (CLI) مستقل ساخته شده است. **اجرای Nuclei به عنوان یک سرویس ممکن است خطرات امنیتی به همراه داشته باشد.** توصیه می‌شود با احتیاط و با در نظر گرفتن تدابیر امنیتی اضافی از آن استفاده کنید. |
<br>
### ۲. نسخه‌های حرفه‌ای و سازمانی
_برای تیم‌های امنیتی و سازمان‌ها، ما یک سرویس میزبانی‌شده در ابر بر پایه نسخه متن‌باز (OSS) ابزار Nuclei ارائه می‌دهیم؛ این سرویس به‌طور دقیق بهینه‌سازی شده است تا به شما کمک کند اسکن‌های آسیب‌پذیری را به‌صورت مداوم، در مقیاس بزرگ و همراه با تیم و گردش‌های کاری فعلی خود اجرا کنید:_
- اسکن‌های ۵۰ برابر سریع‌تر
- اسکن در مقیاس بزرگ با دقت بسیار بالا
- یکپارچه‌سازی با سرویس‌های ابری (AWS، GCP، Azure، Cloudflare، Fastly، Terraform، Kubernetes)
- اتصال به Jira، Slack، Linear، رابط‌های برنامه‌نویسی (APIs) و وب‌هوک‌ها (Webhooks)
- گزارش‌دهی‌های مدیریتی و سازگاری با استانداردهای امنیتی (Compliance)
- به‌علاوه: اسکن بی‌درنگ (Real-time)، پشتیبانی از SAML SSO، پلتفرم منطبق با استاندارد SOC 2 (همراه با گزینه‌های میزبانی در اتحادیه اروپا و آمریکا)، فضاهای کاری مشترک تیمی و موارد دیگر
- ما به‌طور مداوم در حال [**`افزودن قابلیت‌های جدید`**](https://feedback.projectdiscovery.io/changelog) هستیم!
- **ایده‌آل برای:** نفوذگران قانون‌مند (Pentesters)، تیم‌های امنیتی و سازمان‌ها
در صورتی که سازمان بزرگی دارید و نیازمندی‌های شما پیچیده است، می‌توانید در [**`نسخه پرو ثبت‌نام کنید`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) یا [**`با تیم ما گفتگو کنید`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
<br>
<br>
## مستندات
[**`مستندات کامل`**](https://docs.projectdiscovery.io/tools/nuclei/running) ابزار Nuclei را در اینجا مرور کنید. اگر به‌تازگی کار با Nuclei را شروع کرده‌اید، [**`مجموعه ویدیوهای آموزشی پایه‌ای ما در یوتیوب`**](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl) را بررسی کنید.
<div align="center">
<a href="https://www.youtube.com/watch?v=b5qMyQvL1ZA&list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-getting-started.png" width="350px" alt="Nuclei Getting Started Video Tutorial"></a> <a href="https://www.youtube.com/watch?v=nFXygQdtjyw&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-write-your-first-template.png" width="350px" alt="Writing Your First Nuclei Template Video Tutorial"></a>
</div>
<br>
### نصب
برای نصب موفقیت‌آمیز `nuclei` به نسخه **go >= 1.24.2** نیاز است. دستور زیر را برای دریافت مخزن اجرا کنید:
<div dir="ltr">
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
```
</div>
برای اطلاعات بیشتر درباره نحوه نصب nuclei، به آدرس `https://docs.projectdiscovery.io/tools/nuclei/install` مراجعه کنید.
### دستورات خط فرمان
برای نمایش تمامی پارامترها و سوییچ‌های این ابزار:
<div dir="ltr">
```sh
nuclei -h
```
</div>
<details>
  <summary>گسترش تمام فلگ‌های راهنما</summary>
<div dir="ltr">
```yaml
Nuclei is a fast, template based vulnerability scanner focusing
on extensive configurability, massive extensibility and ease of use.
Usage:
./nuclei [flags]
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)
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, 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
-ntv, -new-templates-version string[] run new templates added in specific version
-as, -automatic-scan automatic web scan using wappalyzer technology detection to tags mapping
-t, -templates string[] list of template or template directory to run (comma-separated, file)
-turl, -template-url string[] template url or list containing template urls to run (comma-separated, file)
-ai, -prompt string generate and run template using ai prompt
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
-validate validate the passed templates to nuclei
-nss, -no-strict-syntax disable strict syntax check on templates
-td, -template-display displays the templates content
-tl list all templates matching current filters
-tgl list all available tags
-sign signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable
-code enable loading code protocol-based templates
-dut, -disable-unsigned-templates disable running unsigned templates or templates with mismatched signature
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
-tags string[] templates to run based on tags (comma-separated, file)
-etags, -exclude-tags string[] templates to exclude based on tags (comma-separated, file)
-itags, -include-tags string[] tags to be executed even if they are excluded either by default or configuration
-id, -template-id string[] templates to run based on template ids (comma-separated, file, allow-wildcard)
-eid, -exclude-id string[] templates to exclude based on template ids (comma-separated, file)
-it, -include-templates string[] path to template file or directory to be executed even if they are excluded either by default or configuration
-et, -exclude-templates string[] path to template file or directory to exclude (comma-separated, file)
-em, -exclude-matchers string[] template matchers to exclude in result
-s, -severity value[] templates to run based on severity. Possible values: info, low, medium, high, critical, unknown
-es, -exclude-severity value[] templates to exclude based on severity. Possible values: info, low, medium, high, critical, unknown
-pt, -type value[] templates to run based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
-ept, -exclude-type value[] templates to exclude based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
-tc, -template-condition string[] templates to run based on expression condition
OUTPUT:
-o, -output string output file to write found issues/vulnerabilities
-sresp, -store-resp store all request/response passed through nuclei to output directory
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
-silent display findings only
-nc, -no-color disable output content coloring (ANSI escape codes)
-j, -jsonl write output in JSONL(ines) format
-irr, -include-rr -omit-raw include request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only) [DEPRECATED use -omit-raw] (default true)
-or, -omit-raw omit request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)
-ot, -omit-template omit encoded template in the JSON, JSONL output
-nm, -no-meta disable printing result metadata in cli output
-ts, -timestamp enables printing timestamp in cli output
-rdb, -report-db string nuclei reporting database (always use this to persist report data)
-ms, -matcher-status display match failure status
-me, -markdown-export string directory to export results in markdown format
-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:
-config string path to the nuclei configuration file
-tp, -profile string template profile config file to run
-tpl, -profile-list list community template profiles
-fr, -follow-redirects enable following redirects for http templates
-fhr, -follow-host-redirects follow redirects on the same host
-mr, -max-redirects int max number of redirects to follow for http templates (default 10)
-dr, -disable-redirects disable redirects for http templates
-rc, -report-config string nuclei reporting module configuration file
-H, -header string[] custom header/cookie to include in all http request in header:value format (cli, file)
-V, -var value custom vars in key=value format
-r, -resolvers string file containing resolver list for nuclei
-sr, -system-resolvers use system DNS resolving as error fallback
-dc, -disable-clustering disable clustering of requests
-passive enable passive HTTP response processing mode
-fh2, -force-http2 force http2 connection on requests
-ev, -env-vars enable environment variables to be used in template
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
-ck, -client-key string client key file (PEM-encoded) used for authenticating against scanned hosts
-ca, -client-ca string client certificate authority file (PEM-encoded) used for authenticating against scanned hosts
-sml, -show-match-line show match lines for file templates, works with extractors only
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access allow all templates in the run to read any file on the system (not just payloads)
-lna, -restrict-local-network-access blocks connections to the local / private network
-i, -interface string network interface to use for network scan
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
INTERACTSH:
-iserver, -interactsh-server string interactsh server url for self-hosted instance (default: oast.pro,oast.live,oast.site,oast.online,oast.fun,oast.me)
-itoken, -interactsh-token string authentication token for self-hosted interactsh server
-interactions-cache-size int number of requests to keep in the interactions cache (default 5000)
-interactions-eviction int number of seconds to wait before evicting requests from cache (default 60)
-interactions-poll-duration int number of seconds to wait before each interaction poll request (default 5)
-interactions-cooldown-period int extra time for interaction polling before exiting (default 5)
-ni, -no-interactsh disable interactsh server for OAST testing, exclude OAST based templates
FUZZING:
-ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix)
-fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single)
-fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
-dast enable / run dast (fuzz) nuclei templates
-dts, -dast-server enable dast server mode (live fuzzing)
-dtr, -dast-report write dast scan report to file
-dtst, -dast-server-token string dast server token (optional)
-dtsa, -dast-server-address string dast server address (default "localhost:9055")
-dtp, -dast-proxy enable dast proxy mode (fuzz live traffic proxied through nuclei)
-dtpa, -dast-proxy-address string dast proxy listen address (default "127.0.0.1:9056")
-dtpau, -dast-proxy-auth string dast proxy basic auth in user:pass format (required for non-loopback address)
-dfp, -display-fuzz-points display fuzz points in the output for debugging
-fuzz-param-frequency int frequency of uninteresting parameters for fuzzing before skipping (default 10)
-fa, -fuzz-aggression string fuzzing aggression level controls payload count for fuzz (low, medium, high) (default "low")
-cs, -fuzz-scope string[] in scope url regex to be followed by fuzzer
-cos, -fuzz-out-scope string[] out of scope url regex to be excluded by fuzzer
UNCOVER:
-uc, -uncover enable uncover engine
-uq, -uncover-query string[] uncover search query
-ue, -uncover-engine string[] uncover search engine (shodan,censys,fofa,shodan-idb,quake,hunter,zoomeye,netlas,criminalip,publicwww,hunterhow,google) (default shodan)
-uf, -uncover-field string uncover fields to return (ip,port,host) (default "ip:port")
-ul, -uncover-limit int uncover results to return (default 100)
-ur, -uncover-ratelimit int override ratelimit of engines with unknown ratelimit (default 60 req/min) (default 60)
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)
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
-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)
-hbs, -headless-bulk-size int maximum number of headless hosts to be analyzed in parallel per template (default 10)
-headc, -headless-concurrency int maximum number of headless templates to be executed in parallel (default 10)
-jsc, -js-concurrency int maximum number of javascript runtimes to be executed in parallel (default 120)
-pc, -payload-concurrency int max payload concurrency for each template (default 25)
-prc, -probe-concurrency int http probe concurrency with httpx (default 50)
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
OPTIMIZATIONS:
-timeout int time to wait in seconds before timeout (default 10)
-retries int number of times to retry a failed request (default 1)
-ldp, -leave-default-ports leave default HTTP/HTTPS ports (eg. host:80,host:443)
-mhe, -max-host-error int max errors for a host before skipping from scan (default 30)
-te, -track-error string[] adds given error to max-host-error watchlist (standard, file)
-nmhe, -no-mhe disable skipping host from scan based on errors
-project use a project folder to avoid sending same request multiple times
-project-path string set a specific project path (default "/tmp")
-spm, -stop-at-first-match stop processing HTTP requests after the first match (may break template/workflow logic)
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
-page-timeout int seconds to wait for each page in headless mode (default 20)
-sb, -show-browser show the browser on the screen when running templates with headless mode
-ho, -headless-options string[] start headless chrome with additional options
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
-cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
-lha, -list-headless-action list available headless actions
DEBUG:
-debug show all requests and responses
-dreq, -debug-req show all sent requests
-dresp, -debug-resp show all received responses
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
-pi, -proxy-internal proxy all internal requests
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
-ldf, -list-dsl-function list all supported DSL function signatures
-tlog, -trace-log string file to write sent requests trace log
-elog, -error-log string file to write sent requests error log
-version show nuclei version
-hm, -hang-monitor enable nuclei hang monitoring
-v, -verbose show verbose output
-profile-mem string generate memory (heap) profile & trace files
-vv display templates loaded for scan
-svd, -show-var-dump show variables dump for debugging
-vdl, -var-dump-limit int limit the number of characters displayed in var dump (default 255)
-ep, -enable-pprof enable pprof debugging server
-tv, -templates-version shows the version of the installed nuclei-templates
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
STATISTICS:
-stats display statistics about the running scan
-sj, -stats-json display statistics in JSONL(ines) format
-si, -stats-interval int number of seconds to wait between showing a statistics update (default 5)
-mp, -metrics-port int port to expose nuclei metrics on (default 9092)
-hps, -http-stats enable http status capturing (experimental)
CLOUD:
-auth configure projectdiscovery cloud (pdcp) api key (default true)
-tid, -team-id string upload scan results to given team id (optional) (default "none")
-cup, -cloud-upload upload scan results to pdcp dashboard [DEPRECATED use -dashboard]
-sid, -scan-id string upload scan results to existing scan id (optional)
-sname, -scan-name string scan name to set (optional)
-pd, -dashboard upload / view nuclei results in projectdiscovery cloud (pdcp) UI dashboard
-pdu, -dashboard-upload string upload / view nuclei results file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard
AUTHENTICATION:
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
-ps, -prefetch-secrets prefetch secrets from the secrets file
# NOTE: Headers in secrets files preserve exact casing (useful for case-sensitive APIs)
EXAMPLES:
Run nuclei on single host:
$ nuclei -target example.com
Run nuclei with specific template directories:
$ nuclei -target example.com -t http/cves/ -t ssl
Run nuclei against a list of hosts:
$ nuclei -list hosts.txt
Run nuclei with a JSON output:
$ nuclei -target example.com -json-export output.json
Run nuclei with sorted Markdown outputs (with environment variables):
$ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
Additional documentation is available at: https://docs.projectdiscovery.io/getting-started/running
```
</div>
مستندات تکمیلی در این آدرس در دسترس است: [**`docs.projectdiscovery.io/getting-started/running`**](https://docs.projectdiscovery.io/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
</details>
### اسکن یک هدف
برای انجام یک اسکن سریع روی یک وب‌اپلیکیشن:
<div dir="ltr">
```sh
nuclei -target https://example.com
```
</div>
### اسکن چندین هدف
Nuclei می‌تواند با ارائهٔ فهرستی از اهداف، اسکن گروهی را انجام دهد. می‌توانید از فایلی استفاده کنید که شامل چندین URL است.
<div dir="ltr">
```sh
nuclei -list urls.txt
```
</div>
### اسکن شبکه
این کار کل زیرشبکه را برای مشکلات مرتبط با شبکه، مانند پورت‌های باز یا سرویس‌های پیکربندی نادرست، اسکن می‌کند.
<div dir="ltr">
```sh
nuclei -target 192.168.1.0/24
```
</div>
### اسکن با استفاده از قالب سفارشی خودتان
برای نوشتن و استفاده از قالب خودتان، یک فایل `.yaml` با قوانین مشخص ایجاد کنید، سپس آن را به‌صورت زیر استفاده کنید.
<div dir="ltr">
```sh
nuclei -u https://example.com -t /path/to/your-template.yaml
```
</div>
### اتصال Nuclei به ProjectDiscovery
می‌توانید اسکن‌ها را روی دستگاه خود اجرا کرده و نتایج را برای تحلیل و رفع مشکلات بیشتر، در پلتفرم ابری بارگذاری کنید.
<div dir="ltr">
```sh
nuclei -target https://example.com -dashboard
```
</div>
> [!NOTE]
> این قابلیت کاملاً رایگان است و به هیچ‌گونه اشتراکی نیاز ندارد. برای راهنمایی دقیق‌تر، به [**`مستندات`**](https://docs.projectdiscovery.io/cloud/scanning/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) مراجعه کنید.
<br>
<br>
## قالب‌های Nuclei، جامعه و پاداش‌ها 💎
[**قالب‌های Nuclei**](https://github.com/projectdiscovery/nuclei-templates) بر اساس مفهوم فایل‌های قالب مبتنی بر YAML طراحی شده‌اند که نحوه ارسال و پردازش درخواست‌ها را تعریف می‌کنند. این موضوع امکان توسعه‌پذیری آسان Nuclei را فراهم می‌کند. قالب‌ها با استفاده از YAML نوشته می‌شوند که قالبی ساده و قابل‌خواندن برای انسان ارائه می‌دهد تا بتوان فرآیند اجرا را به‌سرعت تعریف کرد.
**همین حالا به‌صورت آنلاین با ویرایشگر رایگان قالب‌های Nuclei مجهز به هوش مصنوعی ما امتحان کنید؛** [**`اینجا کلیک کنید`**](https://cloud.projectdiscovery.io/templates).
قالب‌های Nuclei روشی ساده و یکپارچه برای شناسایی و گزارش آسیب‌پذیری‌ها ارائه می‌دهند و جزئیات ضروری مانند سطح شدت و روش‌های شناسایی را در بر می‌گیرند. این ابزار متن‌باز که توسط جامعه توسعه داده می‌شود، واکنش به تهدیدات را سرعت می‌بخشد و در دنیای امنیت سایبری به‌طور گسترده شناخته شده است. قالب‌های Nuclei به‌طور فعال توسط هزاران پژوهشگر امنیتی در سراسر جهان توسعه داده می‌شوند. ما دو برنامه برای مشارکت‌کنندگان خود داریم: [**`Pioneers`**](https://projectdiscovery.io/pioneers) و [**`💎 پاداش‌ها`**](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
<p align="right">
<a href="/static/nuclei-templates-teamcity.png" target="_blank"><img src="/static/nuclei-templates-teamcity.png" width="1200px" alt="نمونه قالب Nuclei برای شناسایی پیکربندی نادرست TeamCity" /></a>
</p>
### مثال‌ها
برای مشاهده موارد استفاده و ایده‌ها، به [**مستندات ما**](https://docs.projectdiscovery.io/templates/introduction) مراجعه کنید.
| قالب Nuclei | مورد استفاده |
| ---: | ---: |
| **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** | شناسایی CVEهای شناخته‌شده |
| **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** | شناسایی آسیب‌پذیری‌های Out-of-Band |
| **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** | شناسایی SQL Injection |
| **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** | Cross-Site Scripting (XSS) |
| **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** | رمزهای عبور پیش‌فرض یا ضعیف |
| **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** | فایل‌های محرمانه یا افشای داده‌ها |
| **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** | شناسایی Open Redirect |
| **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** | شناسایی Subdomain Takeover |
| **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** | پیکربندی‌های نادرست امنیتی |
| **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** | پیکربندی‌های ضعیف SSL/TLS |
| **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** | سرویس‌های ابری با پیکربندی نادرست |
| **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** | آسیب‌پذیری‌های اجرای کد از راه دور |
| **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** | حملات Directory Traversal |
| **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** | آسیب‌پذیری‌های File Inclusion |
<br>
<br>
## ماموریت ما
اسکنرهای سنتی آسیب‌پذیری دهه‌ها پیش ساخته شده‌اند. آن‌ها متن‌بسته، فوق‌العاده کند و وابسته به فروشندگان هستند. مهاجمان امروزی، برخلاف گذشته که بهره‌برداری از آسیب‌پذیری‌ها سال‌ها طول می‌کشید، ظرف چند روز پس از انتشار CVEهای جدید، آن‌ها را در سراسر اینترنت به‌صورت گسترده مورد سوءاستفاده قرار می‌دهند. این تغییر، نیازمند رویکردی کاملاً متفاوت برای مقابله با اکسپلویت‌های پرطرفدار در اینترنت است.
ما Nuclei را برای حل این چالش ساختیم. ما کل چارچوب موتور اسکن را متن‌باز و قابل سفارشی‌سازی کردیم تا جامعه امنیتی جهانی بتواند با یکدیگر همکاری کرده و با بردارهای حمله و آسیب‌پذیری‌های پرطرفدار در اینترنت مقابله کند. امروزه Nuclei توسط شرکت‌های Fortune 500، سازمان‌های دولتی و دانشگاه‌ها استفاده و توسعه داده می‌شود.
شما نیز می‌توانید با مشارکت در کد ما، [**`کتابخانه قالب‌ها`**](https://github.com/projectdiscovery/nuclei-templates)، یا [**`پیوستن به تیم ما`**](https://projectdiscovery.io/) مشارکت کنید.
<br>
<br>
## مشارکت‌کنندگان :heart:
از تمام مشارکت‌کنندگان فوق‌العاده [**`جامعه که با ارسال PRها`**](https://github.com/projectdiscovery/nuclei/graphs/contributors) و به‌روز نگه‌داشتن این پروژه از آن حمایت می‌کنند، سپاسگزاریم. :heart:
<p align="left">
<a href="https://github.com/Ice3man543"><img src="https://avatars.githubusercontent.com/u/22318055?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ehsandeep"><img src="https://avatars.githubusercontent.com/u/8293321?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Mzack9999"><img src="https://avatars.githubusercontent.com/u/13421144?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/forgedhallpass"><img src="https://avatars.githubusercontent.com/u/13679401?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/tarunKoyalwar"><img src="https://avatars.githubusercontent.com/u/45962551?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/manuelbua"><img src="https://avatars.githubusercontent.com/u/819314?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/actions-user"><img src="https://avatars.githubusercontent.com/u/65916846?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/dogancanbakir"><img src="https://avatars.githubusercontent.com/u/65292895?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/parrasajad"><img src="https://avatars.githubusercontent.com/u/16835787?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/vzamanillo"><img src="https://avatars.githubusercontent.com/u/10209695?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ShubhamRasal"><img src="https://avatars.githubusercontent.com/u/45902122?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/RamanaReddy0M"><img src="https://avatars.githubusercontent.com/u/90540245?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/LuitelSamikshya"><img src="https://avatars.githubusercontent.com/u/85764322?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/kchason"><img src="https://avatars.githubusercontent.com/u/1111099?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/pmareke"><img src="https://avatars.githubusercontent.com/u/3502075?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/dwisiswant0"><img src="https://avatars.githubusercontent.com/u/25837540?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/xm1k3"><img src="https://avatars.githubusercontent.com/u/73166077?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/5amu"><img src="https://avatars.githubusercontent.com/u/39925709?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ehrishirajsharma"><img src="https://avatars.githubusercontent.com/u/35542790?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/zerodivisi0n"><img src="https://avatars.githubusercontent.com/u/687694?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/geeknik"><img src="https://avatars.githubusercontent.com/u/466878?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/TerminalFi"><img src="https://avatars.githubusercontent.com/u/32599364?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/KaulSe"><img src="https://avatars.githubusercontent.com/u/45340011?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/sullo"><img src="https://avatars.githubusercontent.com/u/1474884?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/wdahlenburg"><img src="https://avatars.githubusercontent.com/u/4451504?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ghost"><img src="https://avatars.githubusercontent.com/u/10137?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Nishan8583"><img src="https://avatars.githubusercontent.com/u/20457968?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jdk2588"><img src="https://avatars.githubusercontent.com/u/985054?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/nothinux"><img src="https://avatars.githubusercontent.com/u/17433202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/CodFrm"><img src="https://avatars.githubusercontent.com/u/22783163?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/CasperGN"><img src="https://avatars.githubusercontent.com/u/5549643?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ankh2054"><img src="https://avatars.githubusercontent.com/u/6784287?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/revblock"><img src="https://avatars.githubusercontent.com/u/72813848?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/cn-kali-team"><img src="https://avatars.githubusercontent.com/u/30642514?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/EndPositive"><img src="https://avatars.githubusercontent.com/u/25148195?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jimen0"><img src="https://avatars.githubusercontent.com/u/6826244?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/xstevens"><img src="https://avatars.githubusercontent.com/u/69216?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mjkim610"><img src="https://avatars.githubusercontent.com/u/17107206?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/organiccrap"><img src="https://avatars.githubusercontent.com/u/376317?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/lu4nx"><img src="https://avatars.githubusercontent.com/u/3006875?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/souvikhazra1"><img src="https://avatars.githubusercontent.com/u/13842393?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/tovask"><img src="https://avatars.githubusercontent.com/u/22732484?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Marmelatze"><img src="https://avatars.githubusercontent.com/u/199681?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/doug-threatmate"><img src="https://avatars.githubusercontent.com/u/127235272?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/yabeow"><img src="https://avatars.githubusercontent.com/u/21117771?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/olearycrew"><img src="https://avatars.githubusercontent.com/u/6044920?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/gano3s"><img src="https://avatars.githubusercontent.com/u/2551605?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/alizmhdi"><img src="https://avatars.githubusercontent.com/u/79321261?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/hackerpain"><img src="https://avatars.githubusercontent.com/u/61242234?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/lc"><img src="https://avatars.githubusercontent.com/u/19563282?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/savushkin-yauheni"><img src="https://avatars.githubusercontent.com/u/5173352?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/74616e696d"><img src="https://avatars.githubusercontent.com/u/97121933?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/edoardottt"><img src="https://avatars.githubusercontent.com/u/35783570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/zt2"><img src="https://avatars.githubusercontent.com/u/7644862?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/moonD4rk"><img src="https://avatars.githubusercontent.com/u/24284231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/wk8"><img src="https://avatars.githubusercontent.com/u/2536231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mikerott"><img src="https://avatars.githubusercontent.com/u/857712?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/toufik-airane"><img src="https://avatars.githubusercontent.com/u/5610269?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/hktalent"><img src="https://avatars.githubusercontent.com/u/18223385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jturner"><img src="https://avatars.githubusercontent.com/u/1825202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/gaby"><img src="https://avatars.githubusercontent.com/u/835733?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/vavkamil"><img src="https://avatars.githubusercontent.com/u/47953210?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/leonjza"><img src="https://avatars.githubusercontent.com/u/1148127?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mionskowski-form3"><img src="https://avatars.githubusercontent.com/u/91873652?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/chenrui333"><img src="https://avatars.githubusercontent.com/u/1580956?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/iamargus95"><img src="https://avatars.githubusercontent.com/u/77744293?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/shashikarsiddharth"><img src="https://avatars.githubusercontent.com/u/60960197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/trypa11"><img src="https://avatars.githubusercontent.com/u/67585616?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Zeokat"><img src="https://avatars.githubusercontent.com/u/1313154?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/alban-stourbe-wmx"><img src="https://avatars.githubusercontent.com/u/159776828?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/anykno"><img src="https://avatars.githubusercontent.com/u/2528207?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ronaudinho"><img src="https://avatars.githubusercontent.com/u/10264710?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/boy-hack"><img src="https://avatars.githubusercontent.com/u/18695984?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/iuliu8899"><img src="https://avatars.githubusercontent.com/u/31680027?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/debasishbsws"><img src="https://avatars.githubusercontent.com/u/65381620?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/denysvitali-niantic"><img src="https://avatars.githubusercontent.com/u/157139422?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/fail-open"><img src="https://avatars.githubusercontent.com/u/72417455?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Xc1Ym"><img src="https://avatars.githubusercontent.com/u/29765332?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/XTeam-Wing"><img src="https://avatars.githubusercontent.com/u/25416365?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Weltolk"><img src="https://avatars.githubusercontent.com/u/40228052?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/tonghuaroot"><img src="https://avatars.githubusercontent.com/u/23011166?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/praetorian-thendrickson"><img src="https://avatars.githubusercontent.com/u/69640071?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/S0obi"><img src="https://avatars.githubusercontent.com/u/4180104?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/skahn007gl"><img src="https://avatars.githubusercontent.com/u/144735608?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/shouichi"><img src="https://avatars.githubusercontent.com/u/99586?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/seb-elttam"><img src="https://avatars.githubusercontent.com/u/111818823?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/AdallomRoy"><img src="https://avatars.githubusercontent.com/u/4046118?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/rotemreiss"><img src="https://avatars.githubusercontent.com/u/9288082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/oscarintherocks"><img src="https://avatars.githubusercontent.com/u/1785821?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/xxcdd"><img src="https://avatars.githubusercontent.com/u/42600601?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/chen2aaron"><img src="https://avatars.githubusercontent.com/u/9978183?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/voidz0r"><img src="https://avatars.githubusercontent.com/u/1032286?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/vince-isec"><img src="https://avatars.githubusercontent.com/u/149686094?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/true13"><img src="https://avatars.githubusercontent.com/u/18207552?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/skhalsa-sigsci"><img src="https://avatars.githubusercontent.com/u/68570441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ShuBo6"><img src="https://avatars.githubusercontent.com/u/41125338?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/seeyarh"><img src="https://avatars.githubusercontent.com/u/16869800?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/securibee"><img src="https://avatars.githubusercontent.com/u/51520913?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/sduc"><img src="https://avatars.githubusercontent.com/u/2879617?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/scottdharvey"><img src="https://avatars.githubusercontent.com/u/25498254?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/rykkard"><img src="https://avatars.githubusercontent.com/u/51889048?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/monitor403"><img src="https://avatars.githubusercontent.com/u/45124775?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mlec1"><img src="https://avatars.githubusercontent.com/u/42201667?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/meme-lord"><img src="https://avatars.githubusercontent.com/u/17912559?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/LazyMaple"><img src="https://avatars.githubusercontent.com/u/12314941?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/llussy"><img src="https://avatars.githubusercontent.com/u/18432966?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/linchizhen"><img src="https://avatars.githubusercontent.com/u/170242051?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/kiokuless"><img src="https://avatars.githubusercontent.com/u/110003596?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Jarnpher553"><img src="https://avatars.githubusercontent.com/u/10233873?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/c-f"><img src="https://avatars.githubusercontent.com/u/35263248?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/hanghuge"><img src="https://avatars.githubusercontent.com/u/166206050?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/galoget"><img src="https://avatars.githubusercontent.com/u/8353133?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/fudancoder"><img src="https://avatars.githubusercontent.com/u/171416994?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/revolunet"><img src="https://avatars.githubusercontent.com/u/124937?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jsoref"><img src="https://avatars.githubusercontent.com/u/2119212?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/MachadoOtto"><img src="https://avatars.githubusercontent.com/u/93268441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jonathanwalker"><img src="https://avatars.githubusercontent.com/u/14978093?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/nHurD"><img src="https://avatars.githubusercontent.com/u/233374?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/jessekelly881"><img src="https://avatars.githubusercontent.com/u/22938931?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/JaneX8"><img src="https://avatars.githubusercontent.com/u/5116641?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/atomiczsec"><img src="https://avatars.githubusercontent.com/u/75549184?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/M-Faheem-Khan"><img src="https://avatars.githubusercontent.com/u/17150767?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/denandz"><img src="https://avatars.githubusercontent.com/u/5291556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/tibbon"><img src="https://avatars.githubusercontent.com/u/82880?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/dany74q"><img src="https://avatars.githubusercontent.com/u/2129762?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/0x123456789"><img src="https://avatars.githubusercontent.com/u/36066426?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/danigoland"><img src="https://avatars.githubusercontent.com/u/15079567?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/ChrisMandich"><img src="https://avatars.githubusercontent.com/u/14286797?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/austintraver"><img src="https://avatars.githubusercontent.com/u/25112463?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/socialsister"><img src="https://avatars.githubusercontent.com/u/155628741?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Anemys"><img src="https://avatars.githubusercontent.com/u/51196227?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/andreangelucci"><img src="https://avatars.githubusercontent.com/u/18552197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/AlexS778"><img src="https://avatars.githubusercontent.com/u/98418121?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/akkuman"><img src="https://avatars.githubusercontent.com/u/7813511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/zrquan"><img src="https://avatars.githubusercontent.com/u/33086594?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/1efty"><img src="https://avatars.githubusercontent.com/u/18194777?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/rsrdesarrollo"><img src="https://avatars.githubusercontent.com/u/5142014?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/razin99"><img src="https://avatars.githubusercontent.com/u/44442082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/MetzinAround"><img src="https://avatars.githubusercontent.com/u/65838556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/vil02"><img src="https://avatars.githubusercontent.com/u/65706193?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mrschyte"><img src="https://avatars.githubusercontent.com/u/8571?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/PeterDaveHello"><img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/parthmalhotra"><img src="https://avatars.githubusercontent.com/u/28601533?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/owenrumney"><img src="https://avatars.githubusercontent.com/u/3049157?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Ovi3"><img src="https://avatars.githubusercontent.com/u/29408109?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/Bisstocuz"><img src="https://avatars.githubusercontent.com/u/42398278?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/daffainfo"><img src="https://avatars.githubusercontent.com/u/36522826?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/mhmdiaa"><img src="https://avatars.githubusercontent.com/u/19687798?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/MiryangJung"><img src="https://avatars.githubusercontent.com/u/48237511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/0xmin"><img src="https://avatars.githubusercontent.com/u/44919834?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/maikthulhu"><img src="https://avatars.githubusercontent.com/u/680830?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/sttlr"><img src="https://avatars.githubusercontent.com/u/40246850?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/iamRjarpan"><img src="https://avatars.githubusercontent.com/u/45498226?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/leoloobeek"><img src="https://avatars.githubusercontent.com/u/8801754?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/KristinnVikar"><img src="https://avatars.githubusercontent.com/u/93918469?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/kant01ne"><img src="https://avatars.githubusercontent.com/u/5072452?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/KeisukeYamashita"><img src="https://avatars.githubusercontent.com/u/23056537?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
<a href="https://github.com/1hehaq"><img src="https://avatars.githubusercontent.com/u/162917546?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
</p>
<br>
<br>
<br>
<div align="center">
<sub>**`nuclei`** تحت [**مجوز MIT**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md) منتشر شده است.</sub>
</div>
</div>
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Telusuri [**`dokumentasi Nuclei lengkap di sini`**](https://docs.projectdiscover
### Instalasi
`nuclei` memerlukan **go >= 1.24.2** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
`nuclei` memerlukan **go >= 1.27.3** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Nuclei の完全な[**`ドキュメントはこちら`**](https://docs.projectdi
### インストール
`nuclei` のインストールには **go >= 1.24.2** が必要です。以下のコマンドを実行してリポジトリを取得してください:
`nuclei` のインストールには **go >= 1.27.3** が必要です。以下のコマンドを実行してリポジトリを取得してください:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Nuclei 전체 [**`문서를 여기에서`**](https://docs.projectdiscovery.io/to
### 설치
`nuclei` 설치에는 **go >= 1.24.2** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
`nuclei` 설치에는 **go >= 1.27.3** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Acesse a [**`documentação completa do Nuclei aqui`**](https://docs.projectdisc
### Instalação
O `nuclei` requer **go >= 1.24.2** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
O `nuclei` requer **go >= 1.27.3** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+20 -6
View File
@@ -9,7 +9,8 @@
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
</div>
@@ -110,7 +111,7 @@ Nuclei'nin tam [**`dokümantasyonuna buradan`**](https://docs.projectdiscovery.i
### Kurulum
`nuclei` yüklemek için **go >= 1.24.2** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
`nuclei` yüklemek için **go >= 1.27.3** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
```sh
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
@@ -173,6 +174,14 @@ TEMPLATES:
-esc, -enable-self-contained enable loading self-contained templates
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
-llm-concurrency int maximum concurrent llm calls (default 4)
-llm-cache cache llm responses within a scan (default true)
FILTERING:
-a, -author string[] templates to run based on authors (comma-separated, file)
@@ -242,6 +251,7 @@ CONFIGURATIONS:
-sip, -source-ip string source ip address to use for network scan
-rsr, -response-size-read int max response size to read in bytes
-rss, -response-size-save int max response size to read in bytes (default 1048576)
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
-hae, -http-api-endpoint string experimental http api endpoint
@@ -308,7 +318,10 @@ OPTIMIZATIONS:
-stream stream mode - start elaborating without sorting the input
-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)
-response-cache reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)
-nh, -no-httpx disable httpx probing for non-url input
-stp, -strict-probe skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)
-tf, -tech-filter opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
@@ -342,10 +355,11 @@ DEBUG:
-hc, -health-check run diagnostic check up
UPDATE:
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
-up, -update update nuclei engine to the latest released version
-ut, -update-templates update nuclei-templates to latest released version
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
-ud, -update-template-dir string custom directory to install / update nuclei-templates
-duc, -disable-update-check disable automatic nuclei/templates update check
HONEYPOT:
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
+134
View File
@@ -1011,6 +1011,7 @@ Part Definitions:
- <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>key_exchange</code> - TLS key exchange group 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
@@ -2475,6 +2476,136 @@ xpath:
```
</div>
<hr />
<div class="dd">
<code>prompt</code> <i>string</i>
</div>
<div class="dt">
Prompt is the natural-language question for an llm matcher. The model
judges the selected response part and returns a verdict; the matcher
fires when the verdict equals Expect with at least MinConfidence.
{{...}} placeholders are interpolated from the template variables,
-var and target. Response derived values are not interpolated, since
the response is attacker controlled.
Examples:
```yaml
prompt: Is this a working admin login form rather than a marketing page?
```
</div>
<hr />
<div class="dd">
<code>expect</code> <i>string</i>
</div>
<div class="dt">
Expect is the verdict that counts as a match for an llm matcher.
Defaults to "yes".
</div>
<hr />
<div class="dd">
<code>options</code> <i>[]string</i>
</div>
<div class="dt">
Options restricts the model to a fixed set of verdicts for an llm
matcher. Defaults to yes/no.
</div>
<hr />
<div class="dd">
<code>min-confidence</code> <i>float64</i>
</div>
<div class="dt">
MinConfidence is the minimum confidence (0-1) an llm verdict needs to
count as a match. Defaults to 0.
</div>
<hr />
<div class="dd">
<code>inputs</code> <i>[]string</i>
</div>
<div class="dt">
Inputs compares several responses instead of a single part, for
checks that are about the difference between responses (blind
boolean injection, user enumeration, IDOR). Each entry is resolved
against the runtime values, so {{body_1}} and {{body_2}} work with
req-condition, and each response is framed separately for the model.
Part is ignored when Inputs is set.
Examples:
```yaml
inputs:
- '{{body_1}}'
- '{{body_2}}'
```
</div>
<hr />
<div class="dd">
<code>max-input-tokens</code> <i>int</i>
</div>
<div class="dt">
MaxInputTokens caps how much of the response part is sent to the model
for an llm matcher, as an approximate token count.
</div>
<hr />
<div class="dd">
<code>allow-sole</code> <i>bool</i>
</div>
<div class="dt">
AllowSole permits an llm matcher to be the only matcher on a high or
critical template. Without it such a template is rejected, so a model is
never the sole arbiter of a severe finding by accident.
</div>
<hr />
@@ -2600,6 +2731,8 @@ Enum Values:
- <code>dsl</code>
- <code>xpath</code>
- <code>llm</code>
</div>
<hr />
@@ -4012,6 +4145,7 @@ Part Definitions:
- <code>issuer_cn</code> - Issuer CN is the common name of the certificate issuer
- <code>issuer_dn</code> - Issuer DN is the distinguished name of the certificate issuer
- <code>issuer_org</code> - Issuer organization is the organization of the certificate issuer
- <code>key_exchange</code> - Key exchange is the TLS key exchange group negotiated in the handshake
- <code>not_after</code> - Timestamp after which the remote cert expires
- <code>not_before</code> - Timestamp before which the certificate is not valid
- <code>probe_status</code> - Probe status indicates if the probe was successful
+2
View File
@@ -32,6 +32,8 @@ AllowdTypes = "AllowdTypes"
ExludedDastTmplStats = "ExludedDastTmplStats"
[default.extend-words]
# SMTP greeting command
HELO = "HELO"
# External package API (fileutil.IsWriteable)
Writeable = "Writeable" # TODO(dwisiswant0): Fix in upstream.
# Variable name used for flow annotations
+21
View File
@@ -244,6 +244,8 @@ func main() {
}
}
// readConfig defines the cli flags and loads them from the command line and
// config files.
func readConfig() *goflags.FlagSet {
var selectedFiles configuration.Selection
// when true updates nuclei binary to latest version
@@ -273,6 +275,8 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
)
flagSet.CreateGroup("target-format", "Target-Format",
@@ -304,6 +308,14 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.EnableSelfContainedTemplates, "enable-self-contained", "esc", false, "enable loading self-contained templates"),
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter)"),
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
flagSet.IntVar(&options.LLMMaxCalls, "llm-max-calls", 5000, "maximum llm calls per scan (0 for unlimited)"),
flagSet.IntVar(&options.LLMConcurrency, "llm-concurrency", 4, "maximum concurrent llm calls"),
flagSet.BoolVar(&options.LLMCache, "llm-cache", true, "cache llm responses within a scan"),
)
flagSet.CreateGroup("filters", "Filtering",
@@ -373,9 +385,11 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.RestrictLocalNetworkAccess, "restrict-local-network-access", "lna", false, "blocks connections to the local / private network"),
flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"),
flagSet.StringVarP(&options.AttackType, "attack-type", "at", "", "type of payload combinations to perform (batteringram,pitchfork,clusterbomb)"),
flagSet.BoolVarP(&options.PayloadRandomOrder, "payload-random-order", "pro", false, "iterate payload combinations in a random order instead of sequentially"),
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.IntVarP(&options.RequestSaveSize, "request-size-save", "rqss", 32*unitutils.Kilo, "max request body size to save in bytes (0 to save the whole body)"),
flagSet.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"),
@@ -422,6 +436,8 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.CreateGroup("rate-limit", "Rate-Limit",
flagSet.IntVarP(&options.RateLimit, "rate-limit", "rl", 150, "maximum number of requests to send per second"),
flagSet.DurationVarP(&options.RateLimitDuration, "rate-limit-duration", "rld", time.Second, "maximum number of requests to send per second"),
flagSet.BoolVar(&options.HostBackoff, "host-backoff", false, "slow down a host that starts blocking (429, repeated 403s, connection failures)"),
flagSet.DurationVar(&options.HostBackoffMax, "host-backoff-max", 30*time.Second, "maximum delay applied to a host that is blocking"),
flagSet.BoolVar(&options.PerHostRateLimit, "per-host-rate-limit", false, "enable per-host rate limiting (global rate limit becomes unlimited when enabled)"),
flagSet.IntVarP(&options.RateLimitMinute, "rate-limit-minute", "rlm", 0, "maximum number of requests to send per minute (DEPRECATED)"),
flagSet.IntVarP(&options.BulkSize, "bulk-size", "bs", 25, "maximum number of hosts to be analyzed in parallel per template"),
@@ -450,7 +466,10 @@ on extensive configurability, massive extensibility and ease of use.`)
scanstrategy.TemplateSpray.String(): goflags.EnumVariable(2),
}),
flagSet.DurationVarP(&options.InputReadTimeout, "input-read-timeout", "irt", time.Duration(3*time.Minute), "timeout on input read"),
flagSet.BoolVar(&options.ResponseCache, "response-cache", false, "reuse identical safe GET/HEAD responses within a scan (lossless: skips requests with auth, cookies, body or custom headers)"),
flagSet.BoolVarP(&options.DisableHTTPProbe, "no-httpx", "nh", false, "disable httpx probing for non-url input"),
flagSet.BoolVarP(&options.StrictProbe, "strict-probe", "stp", false, "skip templates whose target service is unreachable: HTTP/headless on hosts httpx could not confirm and network templates on closed ports (lossless, no raw-input fallback)"),
flagSet.BoolVarP(&options.TechFilter, "tech-filter", "tf", false, "opt-in: fingerprint HTTP targets and skip product/macro-tagged templates that cannot match (off by default; enables scan-scoped GET cache; fail-open; no-op if no tech-bound templates)"),
flagSet.BoolVar(&options.PreflightPortScan, "preflight-portscan", false, "run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)"),
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "disable stdin processing"),
flagSet.BoolVar(&options.EnableHTTPCache, "http-cache", false, "enable HTTP cache (RFC 9111) for HTTP requests"),
@@ -472,6 +491,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.DebugResponse, "debug-resp", "dresp", false, "show all received responses"),
flagSet.StringSliceVarP(&options.Proxy, "proxy", "p", nil, "list of http/socks5 proxy to use (comma separated or file input)", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.ProxyInternal, "proxy-internal", "pi", false, "proxy all internal requests"),
flagSet.StringVarP(&options.ReplayProxy, "replay-proxy", "rp", "", "http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)"),
flagSet.BoolVarP(&options.ListDslSignatures, "list-dsl-function", "ldf", false, "list all supported DSL function signatures"),
flagSet.StringVarP(&options.TraceLogFile, "trace-log", "tlog", "", "file to write sent requests trace log"),
flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"),
@@ -490,6 +510,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.CreateGroup("update", "Update",
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.UpdateTemplatesChannel, "update-templates-channel", "utc", installer.TemplatesChannelStable, "nuclei-templates update channel (stable, latest)"),
flagSet.StringVarP(&options.NewTemplatesDirectory, "update-template-dir", "ud", "", "custom directory to install / update nuclei-templates"),
flagSet.BoolVarP(&disableUpdateCheck, "disable-update-check", "duc", false, "disable automatic nuclei/templates update check"),
)
+95 -102
View File
@@ -1,41 +1,6 @@
module github.com/projectdiscovery/nuclei/v3
go 1.26.0
require (
github.com/andygrunwald/go-jira v1.16.1
github.com/antchfx/htmlquery v1.3.5
github.com/go-playground/validator/v10 v10.26.0
github.com/go-rod/rod v0.116.2
github.com/gobwas/ws v1.4.0
github.com/invopop/jsonschema v0.13.0
github.com/itchyny/gojq v0.12.17
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.73
github.com/olekukonko/tablewriter v1.0.8
github.com/pkg/errors v0.9.1
github.com/projectdiscovery/clistats v0.1.7
github.com/projectdiscovery/fastdialer v0.5.20
github.com/projectdiscovery/hmap v0.0.102
github.com/projectdiscovery/interactsh v1.3.1
github.com/projectdiscovery/rawhttp v0.1.92
github.com/projectdiscovery/retryabledns v1.0.116
github.com/projectdiscovery/retryablehttp-go v1.3.26
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
github.com/segmentio/ksuid v1.0.4
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.10.0
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.58.0
golang.org/x/oauth2 v0.36.0
golang.org/x/text v0.41.0
)
go 1.27.3
require (
carvel.dev/ytt v0.52.0
@@ -48,11 +13,13 @@ require (
github.com/FalconOpsLLC/goexec v0.3.0
github.com/Masterminds/semver/v3 v3.5.0
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/alecthomas/chroma v0.10.0
github.com/alexsnet/go-vnc v0.1.0
github.com/alitto/pond v1.9.2
github.com/andygrunwald/go-jira v1.16.1
github.com/antchfx/htmlquery v1.3.5
github.com/antchfx/xmlquery v1.4.4
github.com/antchfx/xpath v1.3.6
github.com/aws/aws-sdk-go-v2 v1.41.5
@@ -71,19 +38,28 @@ require (
github.com/fullstorydev/grpcurl v1.9.3
github.com/getkin/kin-openapi v0.144.0
github.com/getsops/sops/v3 v3.9.2
github.com/go-echarts/go-echarts/v2 v2.6.0
github.com/go-git/go-git/v5 v5.19.2
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-playground/validator/v10 v10.26.0
github.com/go-rod/rod v0.116.2
github.com/go-sql-driver/mysql v1.10.0
github.com/gobwas/ws v1.4.0
github.com/goccy/go-json v0.10.5
github.com/google/go-github/v30 v30.1.0
github.com/google/go-github/v92 v92.0.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/graphql-go/graphql v0.8.1
github.com/h2non/filetype v1.1.3
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/hdm/jarm-go v0.0.8
github.com/invopop/jsonschema v0.13.0
github.com/itchyny/gojq v0.12.17
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/jhump/protoreflect v1.17.0
github.com/julienschmidt/httprouter v1.3.0
github.com/kitabisa/go-ci v1.0.3
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
github.com/lib/pq v1.12.3
@@ -92,45 +68,72 @@ require (
github.com/maypok86/otter/v2 v2.2.1
github.com/mholt/archives v0.1.5
github.com/microsoft/go-mssqldb v1.9.2
github.com/miekg/dns v1.1.73
github.com/oiweiwei/go-msrpc v1.2.12
github.com/olekukonko/tablewriter v1.0.8
github.com/ory/dockertest/v3 v3.12.0
github.com/pkg/errors v0.9.1
github.com/praetorian-inc/fingerprintx v1.1.15
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8
github.com/projectdiscovery/clistats v0.1.7
github.com/projectdiscovery/dsl v0.8.24
github.com/projectdiscovery/fastdialer v0.6.0
github.com/projectdiscovery/fasttemplate v0.0.2
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
github.com/projectdiscovery/goflags v0.2.1
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
github.com/projectdiscovery/gologger v1.1.73
github.com/projectdiscovery/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/hmap v0.0.102
github.com/projectdiscovery/httpx v1.12.0
github.com/projectdiscovery/interactsh v1.4.1
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
github.com/projectdiscovery/networkpolicy v0.1.50
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294
github.com/projectdiscovery/networkpolicy v0.1.56
github.com/projectdiscovery/ratelimit v0.0.94
github.com/projectdiscovery/rawhttp v0.2.0
github.com/projectdiscovery/rdap v0.9.0
github.com/projectdiscovery/retryabledns v1.0.116
github.com/projectdiscovery/retryablehttp-go v1.3.31
github.com/projectdiscovery/sarif v0.1.0
github.com/projectdiscovery/tlsx v1.4.0
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
github.com/projectdiscovery/uncover v1.2.1
github.com/projectdiscovery/useragent v0.0.109
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668
github.com/projectdiscovery/wappalyzergo v0.3.0
github.com/redis/go-redis/v9 v9.11.0
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5
github.com/projectdiscovery/wappalyzergo v0.3.5
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/redis/go-redis/v9 v9.19.0
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0
github.com/sandrolain/httpcache v1.4.0
github.com/segmentio/ksuid v1.0.4
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/sijms/go-ora/v2 v2.9.0
github.com/spf13/cast v1.10.0
github.com/stretchr/testify v1.12.1
github.com/syndtr/goleveldb v1.0.0
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
github.com/trivago/tgo v1.0.7
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.22.0
golang.org/x/term v0.45.0
google.golang.org/grpc v1.83.1
go.uber.org/multierr v1.11.0
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/net v0.59.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.23.0
golang.org/x/sys v0.48.0
golang.org/x/term v0.46.0
golang.org/x/text v0.42.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.49.0
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
moul.io/http2curl v1.0.0
)
@@ -149,7 +152,6 @@ require (
cloud.google.com/go/storage v1.56.0 // indirect
dario.cat/mergo v1.0.2 // 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
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect
@@ -162,6 +164,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.13.0 // indirect
@@ -204,8 +207,8 @@ require (
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/caddyserver/certmagic v0.25.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/caddyserver/certmagic v0.25.3 // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/censys/censys-sdk-go v0.19.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -216,6 +219,7 @@ require (
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cloudflare/cfssl v1.6.5 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -227,7 +231,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -238,6 +242,8 @@ require (
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/emersion/go-smtp v0.24.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
@@ -247,7 +253,7 @@ require (
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.29.0 // indirect
github.com/gaissmai/bart v0.30.0 // indirect
github.com/geoffgarside/ber v1.2.0 // indirect
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
@@ -257,7 +263,7 @@ require (
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/logr v1.4.4 // 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.22.5 // indirect
@@ -267,6 +273,10 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goburrow/cache v0.1.4 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
@@ -275,6 +285,7 @@ require (
github.com/golang/snappy v1.0.0 // indirect
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
@@ -296,7 +307,6 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/vault/api v1.15.0 // indirect
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // 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
@@ -308,6 +318,7 @@ 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/json-iterator/go v1.1.12 // 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
@@ -318,6 +329,7 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
@@ -326,7 +338,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.1.3 // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
@@ -337,7 +349,7 @@ require (
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
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
@@ -363,22 +375,24 @@ require (
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.2 // indirect
github.com/projectdiscovery/cdncheck v1.3.0 // indirect
github.com/projectdiscovery/cdncheck v1.3.2 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashabaranov/go-openai v1.37.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
@@ -404,6 +418,7 @@ require (
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -415,68 +430,46 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yuin/goldmark v1.7.17 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/mod v0.41.0 // indirect
google.golang.org/api v0.264.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
mellium.im/sasl v0.3.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
)
require (
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/goburrow/cache v0.1.4 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/trivago/tgo v1.0.7
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
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.55.0 // indirect
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/mod v0.39.0 // indirect
golang.org/x/sys v0.47.0
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.49.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
)
require (
github.com/alecthomas/chroma v0.10.0
github.com/go-echarts/go-echarts/v2 v2.6.0
gopkg.in/warnings.v0 v0.1.2 // indirect
)
// https://go.dev/ref/mod#go-mod-file-retract
retract v3.2.0 // retract due to broken js protocol issue
+103 -99
View File
@@ -64,6 +64,8 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34=
code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
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=
@@ -71,8 +73,6 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
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=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
@@ -124,13 +124,12 @@ github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb888350
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 h1:lpSZPcbowbxvKFaFvE1reLTCStezWXcRVk0zzBtUatg=
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d h1:xX93NZHxxrHcmN4nn1owvJdr1E/l1kp1Rv7rMr9ly+U=
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
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.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
@@ -169,6 +168,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY=
github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU=
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
@@ -265,17 +266,16 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
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=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.3 h1:P3akjLPBtV/i6bHC6LbcLjY3KuoOvfiqF8wFHeP5IhY=
github.com/bytedance/sonic v1.15.3/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -310,17 +310,19 @@ github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0V
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
@@ -349,8 +351,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -382,6 +382,10 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -419,8 +423,8 @@ github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1Zky
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.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc=
github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
@@ -466,8 +470,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -585,9 +589,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
github.com/google/go-github/v92 v92.0.0 h1:4vW4RVffwIvoEfIA4RX09mRSB47qQX557/2+HgeWRRg=
github.com/google/go-github/v92 v92.0.0/go.mod h1:w3CH62ZcmRfvW1cdXpyTztSOVMtdjxtKpgo0GouLmjY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
@@ -639,6 +642,8 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -773,6 +778,10 @@ 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/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
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=
@@ -808,8 +817,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -839,8 +848,8 @@ github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -922,14 +931,14 @@ github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kI
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
github.com/projectdiscovery/blackrock v0.0.2 h1:mxXdu0uM8P2L2Qi210COlU8QiICPFW/Rxk5QUhlPO2k=
github.com/projectdiscovery/blackrock v0.0.2/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
github.com/projectdiscovery/cdncheck v1.3.0 h1:/bWdNXIU1P5ASZuW8QA+tAw1FNHhmWlWXk/qiUHyx2c=
github.com/projectdiscovery/cdncheck v1.3.0/go.mod h1:GIAA0SqjDfxzDQjHR/X+bK05BGmUGfsVplP5Zdv/kHA=
github.com/projectdiscovery/cdncheck v1.3.2 h1:I453ZWNBeG0WWIy3wi9412l4PTzorLgebEnqmbcpSfo=
github.com/projectdiscovery/cdncheck v1.3.2/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8 h1:zaAfFHjyLPq9Aoii1mXPzXkITvXyPJTSZo6nNhzC1xI=
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8/go.mod h1:TYqDNtD4hU8bGFgVxPqhYQ9K1z5rMLV/YFyvQgcU5TM=
github.com/projectdiscovery/fastdialer v0.5.20 h1:MlXoVXQERJY4ILqMNYfuoTKVR4Ix4XonJLkFaoHzgM0=
github.com/projectdiscovery/fastdialer v0.5.20/go.mod h1:zdtIbBt5ELNygMEPDSyQ9Nr9KpP9FuAvJwzNIrZZoJA=
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
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=
@@ -938,8 +947,10 @@ github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c h1:s+lLAlr
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c/go.mod h1:rN35/D3lVx2YDeENFFz06uj8j3XIqK1Ym9XcISF5fzg=
github.com/projectdiscovery/goflags v0.2.1 h1:XnSYvsCjUwHbZaaJUZ3YznCJPoXRk/IhTFP9Gecmx6M=
github.com/projectdiscovery/goflags v0.2.1/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/goimpacket v0.1.1-0.20261007205806-6042a8213eeb h1:+L1yW2PblRHSSHQRTmvxLkGDPZnM52N62oeBd2xHKF0=
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb/go.mod h1:bfgULaYf+6pjjcLCekpXIYElnLJbmhvTttxMAbQJvhY=
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c h1:4H/CfVNlue9PIwsCM1IGxOynDI7cir+emXuDvGJTLJs=
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d h1:fqqH9LHpN2WDz9QuxFrhKNxXSRtzk+Sa6jAhbB7tXcQ=
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d/go.mod h1:Ezmbgdaw4EunGGBU4MQViLoGMJc37LA3ip55YV3KeRI=
github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwapyM7ukGU1jFY=
@@ -954,8 +965,8 @@ github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
github.com/projectdiscovery/httpx v1.12.0 h1:NN5iDrw7NjPBDlACdjac4lePTQmZ5aOmgOzptMpP44c=
github.com/projectdiscovery/httpx v1.12.0/go.mod h1:OgtyibNGdu9BrJxRdNKm280Davw1Z4s2FAhfPNNtLB4=
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
github.com/projectdiscovery/interactsh v1.4.1 h1:EGHPJ1rnuxvILWfcoaRgb/Ykz67WiDXABRCSJN666GA=
github.com/projectdiscovery/interactsh v1.4.1/go.mod h1:5g207F6MvIydlKWo4ZzplXkhaCwSq2veuUTNvEFjqps=
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 h1:GUHQYLllWhoca+C4UAT0+TTB1PrUT05klj39V6/KpK8=
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6/go.mod h1:f8P5X5+jEkUqK9z3oNWtSH1g23jMfw3nuEqmtA1jXA4=
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
@@ -966,32 +977,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.50 h1:z/ZMuNAtPH6Q7BlMZ4DLA1ytm+7TYPAdBCNLjNqaV0U=
github.com/projectdiscovery/networkpolicy v0.1.50/go.mod h1:QAAMLSm1YsbGeZTFNGBMtyob28B1VUVblNT56wPhDJc=
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294 h1:d5fkf/st46mg+QpOkzOuFBBCYfV8ljO3pmi8CChwIyE=
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294/go.mod h1:pQg2urKomS0WkMmLCF5HhWyuresBXosh1kIfn1Tlwug=
github.com/projectdiscovery/rawhttp v0.1.92 h1:38GRsVLNllES/dZcEcA6s6P+rYBp34H9mDphMJMDvvg=
github.com/projectdiscovery/rawhttp v0.1.92/go.mod h1:spfXTPjxCj+bfWLynMVB7uOb+LS3PDNAI9HexMEV1Zg=
github.com/projectdiscovery/networkpolicy v0.1.56 h1:M/L421HFuN707oX0tLTzKZY46uRyaKwxAeyi0q8PlxI=
github.com/projectdiscovery/networkpolicy v0.1.56/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
github.com/projectdiscovery/ratelimit v0.0.94 h1:2qciSuUxarn73YGICZeprZtjrwyAH+0iVpdKzKCXoXE=
github.com/projectdiscovery/ratelimit v0.0.94/go.mod h1:GAerX6s+eWRuACedHRv2/xmMtlW+VahZmbRstQh0sS0=
github.com/projectdiscovery/rawhttp v0.2.0 h1:TGiHC5hydL97/zoi0WjEuNrfO6Gjtg9LeXndUf1Cq3M=
github.com/projectdiscovery/rawhttp v0.2.0/go.mod h1:Waz+K5y6r44ElpqAoOfYtY03zrGGZ12nx0yDKq7Ot1E=
github.com/projectdiscovery/rdap v0.9.0 h1:wPhHx5pQ2QI+WGhyNb2PjhTl0NtB39Nk7YFZ9cp8ZGA=
github.com/projectdiscovery/rdap v0.9.0/go.mod h1:zk4yrJFQ2Hy36Aqk+DvotYQxYAeALaCJ5ORySkff36Q=
github.com/projectdiscovery/retryabledns v1.0.116 h1:tK28PJ4tLO7bgM89RpIFjkfTPMSbRegwvg9mN/q6z78=
github.com/projectdiscovery/retryabledns v1.0.116/go.mod h1:r3vH0AKYtJ9fculcqRFrdVYe2ABcHwZeR4C6j3rqCbY=
github.com/projectdiscovery/retryablehttp-go v1.3.26 h1:BkVqMUIVPhx+3vOoo5CxYGNnMqQQYXOedu89XQB1KTg=
github.com/projectdiscovery/retryablehttp-go v1.3.26/go.mod h1:poffUOX+jvS0XbaPCYess221jXvSO7R/QlE7zhs3+9Q=
github.com/projectdiscovery/retryablehttp-go v1.3.31 h1:0hSEZ5+jj6Oo1IL8qrytD36TK/qCxfe543jEaRqSTjs=
github.com/projectdiscovery/retryablehttp-go v1.3.31/go.mod h1:/WEa7YejxTCXEMLCGfGYaC3JsgXgzuqw8rv8cb4buEc=
github.com/projectdiscovery/sarif v0.1.0 h1:O541T+a448nSJsmIMnXXSOeDQEzpnCAYvRfe0eG5h74=
github.com/projectdiscovery/sarif v0.1.0/go.mod h1:LBC+reM3bkI3qIIhE0rZaINaYX6VG+En6u2hHa5mA7E=
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
github.com/projectdiscovery/stringsutil v0.0.2/go.mod h1:EJ3w6bC5fBYjVou6ryzodQq37D5c6qbAYQpGmAy+DC0=
github.com/projectdiscovery/tlsx v1.4.0 h1:OU2mNK6TOB/YiFX3jhmgwOepFWxPoj5rn8npWiCCz74=
github.com/projectdiscovery/tlsx v1.4.0/go.mod h1:WCrUXAHjdKbDNjsu5EbVBS7qvzap3fxi/E85v/Flyx8=
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75 h1:3qJgfWzzwtFXc/vdUIurSzPzyjBH9NQe0wATkZMoQGI=
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75/go.mod h1:8jqB9aHT6U8NkUsENvaSjD/iBEGB4Ta+CbJBMcvzP9c=
github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLvh5+UjzATE=
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668 h1:umwLd2H4q5hfHPdAHSQWdrmBvECZs2qsF9hskvo1jw8=
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668/go.mod h1:DzaUDLQ+Tqsn2nOL47M1mRBZSVyCmwsAlR2E9dxHB4Y=
github.com/projectdiscovery/wappalyzergo v0.3.0 h1:2AsEkjnKgFZK4tjaiNhwC0zfj04rpckSvIOZINeTGyQ=
github.com/projectdiscovery/wappalyzergo v0.3.0/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5 h1:AOspf7ScbwZr8A2YnjCFd4ZJAMSFqzfEaIJIbJPuu10=
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -1018,8 +1029,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
@@ -1046,8 +1057,6 @@ github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQ
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
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=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
@@ -1066,9 +1075,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -1174,9 +1182,8 @@ github.com/vulncheck-oss/go-exploit v1.51.0/go.mod h1:J28w0dLnA6DnCrnBm9Sbt6smX8
github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko=
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb h1:QIW0GHVTSubm3VxkKpn1FnFAgins6N8FiGTvUQwpXzw=
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
@@ -1231,6 +1238,8 @@ 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/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
@@ -1241,16 +1250,17 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk=
github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w=
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 h1:DCz0McWRVJNICkHdu2XpETqeLvPtZXs315OZyUs1BDk=
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77/go.mod h1:aSvf+uTU222mUYq/KQj3oiEU7ajhCZe8RRSLHIoM4EM=
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 h1:B9sBGvtHjiGOOemAh0cCGTzub76DXlxmOh3Qtx3TkRs=
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87/go.mod h1:9O2aSz1l9fUHTFxJMYCoFsZBy6yGlz3nrOp4dHue5qw=
github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo=
github.com/zmap/zgrab2 v0.1.8 h1:PFnXrIBcGjYFec1JNbxMKQuSXXzS+SbqE89luuF4ORY=
github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFnegE=
@@ -1274,26 +1284,28 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
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.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
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/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
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.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
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=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
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 v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/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.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
@@ -1326,10 +1338,9 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1367,8 +1378,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.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/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=
@@ -1416,10 +1427,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1428,7 +1438,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
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.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=
@@ -1447,8 +1456,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1495,7 +1504,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1513,10 +1521,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1528,10 +1535,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1547,10 +1553,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1682,8 +1687,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.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
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=
@@ -1699,7 +1704,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+1 -1
View File
@@ -1,6 +1,6 @@
module nuclei-tools
go 1.26.1
go 1.27.1
tool (
github.com/dvyukov/go-fuzz/go-fuzz
+445
View File
@@ -0,0 +1,445 @@
package runner
import (
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
syncutil "github.com/projectdiscovery/utils/sync"
)
const hostGroupProbeWorkers = 100
// hostGroup is a set of targets that share the same open-port / HTTP signature.
// Used for savings estimates / logging; execution itself is a single spray pass
// with a per-(template,target) filter derived from the same probe data.
type hostGroup struct {
key string
hosts []*contextargs.MetaInput
openPorts map[string]struct{}
httpOK bool
explicitPort string // operator host:port; network dials this, not template defaults
}
type hostProbeResult struct {
input *contextargs.MetaInput
openPorts []string
httpOK bool
explicitPort string
}
// hostReachabilityIndex maps each input to its probed reachability so the
// engine can skip impossible template×target pairs in a single Execute pass.
type hostReachabilityIndex struct {
byInput map[string]hostReachability
}
type hostReachability struct {
openPorts map[string]struct{}
httpOK bool
// explicitPort is the operator-specified port from host:port input (if any).
// Network execution keeps this port (UseNetworkPort does not replace bare
// host:port), so reachability must not require the template's default port.
explicitPort string
}
// Allow reports whether template may produce a finding on input (lossless).
func (idx *hostReachabilityIndex) Allow(t *templates.Template, mi *contextargs.MetaInput) bool {
if idx == nil || t == nil || mi == nil {
return true
}
h, ok := idx.byInput[mi.Input]
if !ok {
return true
}
return templateAllowedOnHost(t, h.httpOK, h.openPorts, h.explicitPort)
}
func templateAllowedOnHost(t *templates.Template, httpOK bool, openPorts map[string]struct{}, explicitPort string) bool {
if t.SelfContained || len(t.Workflows) > 0 || isUniversalTemplate(t) {
return true
}
if isWebTemplate(t) {
return httpOK
}
ports, ok := tcpNetworkOnlyPorts(t)
if !ok {
return true
}
// host:port inputs: dial the operator-chosen port at execution time, not the
// template default (see contextargs.UseNetworkPort). If that port is
// reachable, network templates may run against it.
if explicitPort != "" {
if _, open := openPorts[explicitPort]; open {
return true
}
return false
}
for _, p := range ports {
if _, open := openPorts[p]; open {
return true
}
}
return false
}
// estimateTemplateRequests mirrors core.getRequestCount for scheduling math.
func estimateTemplateRequests(tpls []*templates.Template) int {
count := 0
for _, t := range tpls {
if len(t.Workflows) > 0 {
continue
}
if t.TotalRequests > 0 {
count += t.TotalRequests
continue
}
count++
}
return count
}
func hostGroupKey(openPorts []string, httpOK bool) string {
ports := append([]string(nil), openPorts...)
sort.Strings(ports)
http := "nohttp"
if httpOK {
http = "http"
}
if len(ports) == 0 {
return http + "|-"
}
return http + "|" + strings.Join(ports, ",")
}
func isWebTemplate(t *templates.Template) bool {
return len(t.RequestsHTTP)+len(t.RequestsWithHTTP)+len(t.RequestsHeadless)+len(t.RequestsWebsocket) > 0 &&
len(t.RequestsNetwork)+len(t.RequestsWithTCP)+len(t.RequestsDNS)+len(t.RequestsFile)+
len(t.RequestsSSL)+len(t.RequestsCode)+len(t.RequestsJavascript)+len(t.Workflows) == 0
}
func isUniversalTemplate(t *templates.Template) bool {
if t.SelfContained || len(t.Workflows) > 0 {
return true
}
if _, ok := tcpNetworkOnlyPorts(t); ok {
return false
}
if isWebTemplate(t) {
return false
}
return true
}
// templatesForHostGroup returns templates that can produce findings on this group.
func templatesForHostGroup(all []*templates.Template, g hostGroup) []*templates.Template {
out := make([]*templates.Template, 0, len(all))
for _, t := range all {
if t.SelfContained || len(t.Workflows) > 0 || isUniversalTemplate(t) {
continue
}
if templateAllowedOnHost(t, g.httpOK, g.openPorts, g.explicitPort) {
out = append(out, t)
}
}
return out
}
func partitionTemplates(all []*templates.Template) (web, network, universal, selfContained []*templates.Template) {
for _, t := range all {
switch {
case t.SelfContained:
selfContained = append(selfContained, t)
case len(t.Workflows) > 0:
universal = append(universal, t)
case isWebTemplate(t):
web = append(web, t)
case isUniversalTemplate(t):
universal = append(universal, t)
default:
network = append(network, t)
}
}
return
}
// buildHostReachability probes each target once, then returns an index for
// single-pass filtering plus host groups for logging/estimates.
//
// HTTP reachability prefers the already-populated InputHelper.InputsHTTP map
// (from initializeTemplatesHTTPInput) so we do not re-run httpx.
// When probePorts is false, only explicit input ports are recorded (HTTP-only mode).
func (r *Runner) buildHostReachability(tpls []*templates.Template, httpHelper *input.Helper, probePorts bool) (*hostReachabilityIndex, []hostGroup, error) {
if r.inputProvider == nil {
return nil, nil, fmt.Errorf("no input provider")
}
var portsToProbe []string
if probePorts {
// Only concrete network-template ports. Do not pull 80/443 from HTTP
// templates via portsPopularityFromTemplates — httpOK already comes from
// scheme / InputsHTTP, and spraying web ports across every host inflates
// probe cost and flakes under parallel dial load.
seen := map[string]struct{}{}
for _, t := range tpls {
ps, ok := tcpNetworkOnlyPorts(t)
if !ok {
continue
}
for _, p := range ps {
if _, dup := seen[p]; dup {
continue
}
seen[p] = struct{}{}
portsToProbe = append(portsToProbe, p)
}
}
sort.Strings(portsToProbe)
}
var targets []*contextargs.MetaInput
r.inputProvider.Iterate(func(mi *contextargs.MetaInput) bool {
targets = append(targets, mi.Clone())
return true
})
if len(targets) == 0 {
return &hostReachabilityIndex{byInput: map[string]hostReachability{}}, nil, nil
}
// Only build an httpx client if we still need live HTTP probes.
needLiveHTTPProbe := false
for _, mi := range targets {
if strings.HasPrefix(mi.Input, "http://") || strings.HasPrefix(mi.Input, "https://") {
continue
}
if httpHelper != nil && httpHelper.InputsHTTPProbed {
continue // already probed (hit or miss) during initializeTemplatesHTTPInput
}
needLiveHTTPProbe = true
break
}
var httpClient *httpx.HTTPX
if needLiveHTTPProbe {
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
if dialers != nil {
httpxOptions := httpx.DefaultOptions
if r.options.AliveHttpProxy != "" {
httpxOptions.Proxy = r.options.AliveHttpProxy
} else if r.options.AliveSocksProxy != "" {
httpxOptions.Proxy = r.options.AliveSocksProxy
}
httpxOptions.RetryMax = r.options.Retries
if r.options.Timeout > 0 {
httpxOptions.Timeout = time.Duration(r.options.Timeout) * time.Second
}
httpxOptions.NetworkPolicy = dialers.NetworkPolicy
if c, err := httpx.New(&httpxOptions); err == nil {
httpClient = c
}
}
}
workers := hostGroupProbeWorkers
// Fan out one task per host:port so closed-port timeouts do not serialize per host.
type probeJob struct {
hostIdx int
host string
port string
}
var jobs []probeJob
hostMeta := make([]struct {
mi *contextargs.MetaInput
host string
explicitPort string
}, len(targets))
for i, mi := range targets {
host, explicitPort := hostAndExplicitPort(mi.Target())
if host == "" {
host, explicitPort = hostAndExplicitPort(mi.Input)
}
hostMeta[i].mi = mi
hostMeta[i].host = host
hostMeta[i].explicitPort = explicitPort
if !probePorts {
continue
}
// Always probe the operator-specified port (may differ from template
// defaults). Do not spray unrelated template ports onto host:port inputs.
if explicitPort != "" && isNumericPort(explicitPort) {
jobs = append(jobs, probeJob{hostIdx: i, host: host, port: explicitPort})
continue
}
for _, p := range portsToProbe {
jobs = append(jobs, probeJob{hostIdx: i, host: host, port: p})
}
}
if len(jobs) < workers {
workers = len(jobs)
}
if workers < 1 {
workers = 1
}
openByHost := make([]map[string]struct{}, len(targets))
for i := range openByHost {
openByHost[i] = map[string]struct{}{}
// Only assume explicit ports are open when we are not probing them.
if !probePorts {
if ep := hostMeta[i].explicitPort; ep != "" && isNumericPort(ep) {
openByHost[i][ep] = struct{}{}
}
}
}
if len(jobs) > 0 {
swg, err := syncutil.New(syncutil.WithSize(workers))
if err != nil {
return nil, nil, err
}
var mu sync.Mutex
for _, job := range jobs {
swg.Add()
go func(job probeJob) {
defer swg.Done()
switch r.probe(job.host, job.port) {
case portOpen, portUnknown:
mu.Lock()
openByHost[job.hostIdx][job.port] = struct{}{}
mu.Unlock()
}
}(job)
}
swg.Wait()
}
results := make([]hostProbeResult, len(targets))
for i, meta := range hostMeta {
open := openByHost[i]
ports := make([]string, 0, len(open))
for p := range open {
ports = append(ports, p)
}
sort.Strings(ports)
results[i] = hostProbeResult{
input: meta.mi,
openPorts: ports,
httpOK: resolveHTTPReachability(meta.mi, open, httpHelper, httpClient),
explicitPort: meta.explicitPort,
}
}
idx := &hostReachabilityIndex{byInput: make(map[string]hostReachability, len(results))}
grouped := map[string]*hostGroup{}
order := make([]string, 0)
for _, res := range results {
if res.input == nil {
continue
}
portsSet := make(map[string]struct{}, len(res.openPorts))
for _, p := range res.openPorts {
portsSet[p] = struct{}{}
}
idx.byInput[res.input.Input] = hostReachability{
openPorts: portsSet,
httpOK: res.httpOK,
explicitPort: res.explicitPort,
}
key := hostGroupKey(res.openPorts, res.httpOK)
if res.explicitPort != "" {
// Keep explicit host:port inputs in their own estimate bucket so
// network-template fan-out is not attributed to bare hosts.
key = key + "|ep:" + res.explicitPort
}
g, ok := grouped[key]
if !ok {
g = &hostGroup{
key: key,
openPorts: portsSet,
httpOK: res.httpOK,
explicitPort: res.explicitPort,
}
grouped[key] = g
order = append(order, key)
}
g.hosts = append(g.hosts, res.input)
}
out := make([]hostGroup, 0, len(order))
for _, key := range order {
out = append(out, *grouped[key])
}
return idx, out, nil
}
func resolveHTTPReachability(mi *contextargs.MetaInput, open map[string]struct{}, helper *input.Helper, client *httpx.HTTPX) bool {
if mi == nil {
return false
}
if strings.HasPrefix(mi.Input, "http://") || strings.HasPrefix(mi.Input, "https://") {
return true
}
// Reuse httpx results from initializeTemplatesHTTPInput when available.
if helper != nil && helper.InputsHTTP != nil {
if probed, ok := helper.InputsHTTP.Get(mi.Input); ok && len(probed) > 0 {
return true
}
// Empty/skipped probe map (e.g. MultiFormat) must not force non-HTTP.
if !helper.InputsHTTPProbed {
// fall through to live probe / open ports
} else {
// Map was built for all inputs: absence means not HTTP.
return false
}
}
if client != nil {
return utils.ProbeURL(mi.Input, client) != ""
}
if _, ok := open["80"]; ok {
return true
}
if _, ok := open["443"]; ok {
return true
}
return false
}
// estimateGroupedExecutions returns baseline and filtered template×host counts.
func estimateGroupedExecutions(all []*templates.Template, groups []hostGroup, hostCount int) (baseline, filtered int) {
baseline = estimateTemplateRequests(all) * hostCount
_, _, universal, selfContained := partitionTemplates(all)
once := append([]*templates.Template{}, selfContained...)
once = append(once, universal...)
filtered = estimateTemplateRequests(once) * hostCount
for _, g := range groups {
filtered += estimateTemplateRequests(templatesForHostGroup(all, g)) * len(g.hosts)
}
return baseline, filtered
}
// countReachabilityStats returns concrete network template count and distinct numeric ports to probe.
func countReachabilityStats(tpls []*templates.Template) (concreteNetwork int, portsToProbe int) {
ports := map[string]struct{}{}
for _, t := range tpls {
ps, ok := tcpNetworkOnlyPorts(t)
if !ok {
continue
}
concreteNetwork++
for _, p := range ps {
ports[p] = struct{}{}
}
}
return concreteNetwork, len(ports)
}
+217
View File
@@ -0,0 +1,217 @@
package runner
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/stretchr/testify/require"
)
func TestHostGroupKey(t *testing.T) {
// Lexicographic sort: "443" < "80".
require.Equal(t, "http|443,80", hostGroupKey([]string{"443", "80"}, true))
require.Equal(t, "nohttp|22", hostGroupKey([]string{"22"}, false))
require.Equal(t, "nohttp|-", hostGroupKey(nil, false))
}
func TestTemplatesForHostGroup(t *testing.T) {
web := &templates.Template{
ID: "web",
RequestsHTTP: []*http.Request{{}},
}
ftp := &templates.Template{
ID: "ftp",
RequestsNetwork: []*network.Request{{
Port: "21",
}},
}
ssh := &templates.Template{
ID: "ssh",
RequestsNetwork: []*network.Request{{
Port: "22",
}},
}
all := []*templates.Template{web, ftp, ssh}
webGroup := hostGroup{
key: "http|80,443",
httpOK: true,
openPorts: map[string]struct{}{"80": {}, "443": {}},
}
got := templatesForHostGroup(all, webGroup)
require.Len(t, got, 1)
require.Equal(t, "web", got[0].ID)
ftpGroup := hostGroup{
key: "nohttp|21",
httpOK: false,
openPorts: map[string]struct{}{"21": {}},
}
got = templatesForHostGroup(all, ftpGroup)
require.Len(t, got, 1)
require.Equal(t, "ftp", got[0].ID)
sshGroup := hostGroup{
key: "nohttp|22",
httpOK: false,
openPorts: map[string]struct{}{"22": {}},
}
got = templatesForHostGroup(all, sshGroup)
require.Len(t, got, 1)
require.Equal(t, "ssh", got[0].ID)
}
func TestPartitionTemplates(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
ftp := &templates.Template{ID: "ftp", RequestsNetwork: []*network.Request{{Port: "21"}}}
self := &templates.Template{ID: "self", SelfContained: true}
dynNet := &templates.Template{
ID: "dyn",
RequestsNetwork: []*network.Request{{Port: ""}},
}
w, n, u, s := partitionTemplates([]*templates.Template{web, ftp, self, dynNet})
require.Len(t, w, 1)
require.Equal(t, "web", w[0].ID)
require.Len(t, n, 1)
require.Equal(t, "ftp", n[0].ID)
require.Len(t, u, 1)
require.Equal(t, "dyn", u[0].ID)
require.Len(t, s, 1)
require.Equal(t, "self", s[0].ID)
}
func TestEstimateGroupedVsBaseline(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}, TotalRequests: 1}
ftp := &templates.Template{ID: "ftp", RequestsNetwork: []*network.Request{{Port: "21"}}, TotalRequests: 1}
ssh := &templates.Template{ID: "ssh", RequestsNetwork: []*network.Request{{Port: "22"}}, TotalRequests: 1}
all := []*templates.Template{web, ftp, ssh}
groups := []hostGroup{
{key: "http|80", httpOK: true, openPorts: map[string]struct{}{"80": {}}, hosts: metaInputs("h1", "h2")},
{key: "nohttp|21", httpOK: false, openPorts: map[string]struct{}{"21": {}}, hosts: metaInputs("h3")},
{key: "nohttp|22", httpOK: false, openPorts: map[string]struct{}{"22": {}}, hosts: metaInputs("h4")},
}
baseline, filtered := estimateGroupedExecutions(all, groups, 4)
require.Equal(t, 12, baseline)
// web*2 + ftp*1 + ssh*1 = 4
require.Equal(t, 4, filtered)
require.Equal(t, 8, baseline-filtered)
}
func TestHostReachabilityAllow(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
ftp := &templates.Template{ID: "ftp", RequestsNetwork: []*network.Request{{Port: "21"}}}
redis := &templates.Template{ID: "redis", RequestsNetwork: []*network.Request{{Port: "6379"}}}
idx := &hostReachabilityIndex{byInput: map[string]hostReachability{
"h1": {httpOK: true, openPorts: map[string]struct{}{"80": {}}},
"h2": {httpOK: false, openPorts: map[string]struct{}{"21": {}}},
"127.0.0.1:19637": {httpOK: false, openPorts: map[string]struct{}{"19637": {}}, explicitPort: "19637"},
}}
require.True(t, idx.Allow(web, &contextargs.MetaInput{Input: "h1"}))
require.False(t, idx.Allow(web, &contextargs.MetaInput{Input: "h2"}))
require.False(t, idx.Allow(ftp, &contextargs.MetaInput{Input: "h1"}))
require.True(t, idx.Allow(ftp, &contextargs.MetaInput{Input: "h2"}))
require.True(t, idx.Allow(web, &contextargs.MetaInput{Input: "unknown"})) // lossless
// Explicit host:port must allow network templates even when template default
// port (6379) differs from the operator-specified port (19637).
require.True(t, idx.Allow(redis, &contextargs.MetaInput{Input: "127.0.0.1:19637"}))
require.False(t, idx.Allow(web, &contextargs.MetaInput{Input: "127.0.0.1:19637"}))
}
func TestTemplateAllowedOnHostUniversal(t *testing.T) {
dnsish := &templates.Template{ID: "dns", RequestsDNS: nil} // no concrete network ports
// force universal via empty network dynamic port
dyn := &templates.Template{ID: "dyn", RequestsNetwork: []*network.Request{{Port: ""}}}
require.True(t, templateAllowedOnHost(dyn, false, map[string]struct{}{}, ""))
_ = dnsish
self := &templates.Template{ID: "self", SelfContained: true}
require.True(t, templateAllowedOnHost(self, false, nil, ""))
}
func TestTemplateAllowedOnHostExplicitPort(t *testing.T) {
redis := &templates.Template{ID: "redis", RequestsNetwork: []*network.Request{{Port: "6379"}}}
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
open := map[string]struct{}{"19637": {}}
require.True(t, templateAllowedOnHost(redis, false, open, "19637"))
require.False(t, templateAllowedOnHost(redis, false, open, "")) // bare host: need 6379
require.False(t, templateAllowedOnHost(web, false, open, "19637"))
// Closed explicit port: openPorts empty => network templates denied.
require.False(t, templateAllowedOnHost(redis, false, map[string]struct{}{}, "19637"))
}
func TestTemplatesForHostGroupExplicitPort(t *testing.T) {
redis := &templates.Template{ID: "redis", RequestsNetwork: []*network.Request{{Port: "6379"}}}
ftp := &templates.Template{ID: "ftp", RequestsNetwork: []*network.Request{{Port: "21"}}}
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
g := hostGroup{
httpOK: false,
openPorts: map[string]struct{}{"19637": {}},
explicitPort: "19637",
}
got := templatesForHostGroup([]*templates.Template{redis, ftp, web}, g)
require.Len(t, got, 2)
ids := map[string]bool{got[0].ID: true, got[1].ID: true}
require.True(t, ids["redis"] && ids["ftp"])
}
func TestCountReachabilityStats(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
redis := &templates.Template{ID: "redis", RequestsNetwork: []*network.Request{{Port: "6379"}}}
pg := &templates.Template{ID: "pg", RequestsNetwork: []*network.Request{{Port: "5432,5432"}}}
dyn := &templates.Template{ID: "dyn", RequestsNetwork: []*network.Request{{Port: ""}}}
concrete, ports := countReachabilityStats([]*templates.Template{web, redis, pg, dyn})
require.Equal(t, 2, concrete)
require.Equal(t, 2, ports) // 6379 + 5432
}
func TestResolveHTTPReachability(t *testing.T) {
require.True(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "http://web"}, nil, nil, nil))
require.True(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "https://web"}, nil, nil, nil))
require.False(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "redis1"}, map[string]struct{}{}, nil, nil))
require.True(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "host"}, map[string]struct{}{"80": {}}, nil, nil))
require.True(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "host"}, map[string]struct{}{"443": {}}, nil, nil))
}
func TestResolveHTTPReachabilitySkippedProbeFailOpen(t *testing.T) {
// MultiFormat leaves an empty InputsHTTP map without probing — must not
// force non-HTTP when open ports still suggest HTTP.
helper := &input.Helper{InputsHTTPProbed: false}
require.True(t, resolveHTTPReachability(&contextargs.MetaInput{Input: "host"}, map[string]struct{}{"80": {}}, helper, nil))
}
func TestTemplatesForHostGroupSkipsUniversal(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}}
univ := &templates.Template{ID: "univ", RequestsNetwork: []*network.Request{{Port: ""}}}
g := hostGroup{httpOK: true, openPorts: map[string]struct{}{"80": {}}}
got := templatesForHostGroup([]*templates.Template{web, univ}, g)
require.Len(t, got, 1)
require.Equal(t, "web", got[0].ID)
}
func TestEstimateGroupedIncludesUniversalsOnce(t *testing.T) {
web := &templates.Template{ID: "web", RequestsHTTP: []*http.Request{{}}, TotalRequests: 1}
univ := &templates.Template{ID: "univ", RequestsNetwork: []*network.Request{{Port: ""}}, TotalRequests: 1}
groups := []hostGroup{
{httpOK: true, openPorts: map[string]struct{}{"80": {}}, hosts: metaInputs("h1")},
}
baseline, filtered := estimateGroupedExecutions([]*templates.Template{web, univ}, groups, 1)
require.Equal(t, 2, baseline)
// universal counted once for all hosts + web for http group
require.Equal(t, 2, filtered)
}
func metaInputs(hosts ...string) []*contextargs.MetaInput {
out := make([]*contextargs.MetaInput, 0, len(hosts))
for _, h := range hosts {
mi := contextargs.NewMetaInput()
mi.Input = h
out = append(out, mi)
}
return out
}
+7 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
"github.com/projectdiscovery/hmap/store/hybrid"
"github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
@@ -68,12 +69,15 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
}
swg.Add()
go func(input *contextargs.MetaInput) {
go func(target *contextargs.MetaInput) {
defer swg.Done()
if result := utils.ProbeURL(input.Input, httpxClient); result != "" {
// a pinned target is probed through its ip, and each ip keeps its
// own result, as servers of one host may serve different schemes
ctx := contextargs.WithCustomIP(context.Background(), target)
if result := utils.ProbeURLWithContext(ctx, target.Input, httpxClient); result != "" {
count.Add(1)
_ = hm.Set(input.Input, []byte(result))
_ = hm.Set(input.ProbeKey(target.Input, target.CustomIP), []byte(result))
}
}(value)
return true
+24 -19
View File
@@ -39,25 +39,30 @@ func GetAuthTmplStore(opts *types.Options, catalog catalog.Catalog, execOpts *pr
}
tmpls = append(tmpls, data...)
}
opts.Templates = tmpls
opts.Workflows = nil
opts.RemoteTemplateDomainList = nil
opts.TemplateURLs = nil
opts.WorkflowURLs = nil
opts.ExcludedTemplates = nil
opts.Tags = nil
opts.ExcludeTags = nil
opts.IncludeTemplates = nil
opts.Authors = nil
opts.Severities = nil
opts.ExcludeSeverities = nil
opts.IncludeTags = nil
opts.IncludeIds = nil
opts.ExcludeIds = nil
opts.Protocols = nil
opts.ExcludeProtocols = nil
opts.IncludeConditions = nil
cfg := loader.NewConfig(opts, catalog, execOpts)
// The auth store only wants the secret-file templates. Copy first so
// this scoping cannot clear Templates, ExcludeTags, or the other filters
// on the caller's options. The CLI already copies; the SDK passes the
// engine options directly.
scoped := opts.Copy()
scoped.Templates = tmpls
scoped.Workflows = nil
scoped.RemoteTemplateDomainList = nil
scoped.TemplateURLs = nil
scoped.WorkflowURLs = nil
scoped.ExcludedTemplates = nil
scoped.Tags = nil
scoped.ExcludeTags = nil
scoped.IncludeTemplates = nil
scoped.Authors = nil
scoped.Severities = nil
scoped.ExcludeSeverities = nil
scoped.IncludeTags = nil
scoped.IncludeIds = nil
scoped.ExcludeIds = nil
scoped.Protocols = nil
scoped.ExcludeProtocols = nil
scoped.IncludeConditions = nil
cfg := loader.NewConfig(scoped, catalog, execOpts)
cfg.StoreId = loader.AuthStoreId
store, err := loader.New(cfg)
if err != nil {
+56
View File
@@ -0,0 +1,56 @@
package runner
import (
"context"
"time"
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
utilsllm "github.com/projectdiscovery/utils/llm"
)
// llmMatcherClient adapts the shared provider layer to the minimal interface
// the matchers package needs, keeping matchers free of any provider SDK import.
type llmMatcherClient struct {
client *utilsllm.Client
model string
}
// Model names the model behind a verdict so it can be recorded in the finding.
func (a *llmMatcherClient) Model() string {
return a.model
}
func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON bool) (string, error) {
return a.client.Complete(ctx, utilsllm.Request{
Prompt: prompt,
Format: utilsllm.Format{JSON: asJSON},
})
}
// configureLLM builds the scan's llm client from options. It returns nil unless
// -llm is set, so nothing reaches a model until the user opts in. The API key is
// read from the environment by the provider layer, never from options.
//
// The client is returned rather than installed globally so two scans in one
// process cannot overwrite each other's provider, cache, budget, or timeout.
func configureLLM(options *types.Options) (llmclient.Client, error) {
if !options.EnableLLM {
return nil, nil
}
client, err := utilsllm.New(utilsllm.Config{
Provider: options.LLMProvider,
BaseURL: options.LLMBaseURL,
Model: options.LLMModel,
Timeout: time.Duration(options.LLMTimeout) * time.Second,
Cache: options.LLMCache,
MaxCalls: options.LLMMaxCalls,
MaxConcurrency: options.LLMConcurrency,
})
if err != nil {
return nil, err
}
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
}
+20 -3
View File
@@ -20,6 +20,7 @@ import (
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
@@ -56,10 +57,18 @@ func ConfigureOptions() error {
return nil
}
// readsTargetsFromStdin reports whether targets are read from a stdin pipe.
// Targets given with -u, -l, or uncover take its place: under cron, CI or an
// agent stdin is often a pipe nothing writes to or closes, and reading it
// would hold the scan until it does.
func readsTargetsFromStdin(options *types.Options, stdinIsPipe bool) bool {
hasOtherTargets := len(options.Targets) > 0 || options.TargetsFilePath != "" || options.Uncover || len(options.UncoverQuery) > 0
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets
}
// ParseOptions parses the command line flags provided by a user
func ParseOptions(options *types.Options) {
// Check if stdin pipe was given
options.Stdin = !options.DisableStdin && fileutil.HasStdin()
options.Stdin = readsTargetsFromStdin(options, fileutil.HasStdin())
// Read the inputs from env variables that not passed by flag.
readEnvInputVars(options)
@@ -141,7 +150,7 @@ func ParseOptions(options *types.Options) {
}
}
// validateOptions validates the configuration options passed
// ValidateOptions validates the configuration options passed
func ValidateOptions(options *types.Options) error {
if err := validateOptions.Struct(options); err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
@@ -167,10 +176,18 @@ func ValidateOptions(options *types.Options) error {
if options.ShouldFollowHTTPRedirects() && options.DisableRedirects {
return errors.New("both follow redirects and disable redirects specified")
}
switch options.UpdateTemplatesChannel {
case "", installer.TemplatesChannelStable, installer.TemplatesChannelLatest:
default:
return fmt.Errorf("invalid templates update channel %q, must be %q or %q", options.UpdateTemplatesChannel, installer.TemplatesChannelStable, installer.TemplatesChannelLatest)
}
// loading the proxy server list from file or cli and test the connectivity
if err := loadProxyServers(options); err != nil {
return err
}
if err := validateReplayProxy(options.ReplayProxy); err != nil {
return err
}
if options.Validate {
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
}
+31
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
@@ -59,3 +60,33 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
})
}
}
// TestValidateOptionsUpdateTemplatesChannel checks that only known templates
// update channels are accepted.
func TestValidateOptionsUpdateTemplatesChannel(t *testing.T) {
for _, channel := range []string{"", installer.TemplatesChannelStable, installer.TemplatesChannelLatest} {
require.NoError(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: channel}), "channel %q", channel)
}
require.ErrorContains(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: "nightly"}), `invalid templates update channel "nightly"`)
}
func TestReadsTargetsFromStdin(t *testing.T) {
for _, tc := range []struct {
name string
options *types.Options
stdinIsPipe bool
want bool
}{
{"piped stdin alone", &types.Options{}, true, true},
{"no pipe", &types.Options{}, false, false},
{"disabled", &types.Options{DisableStdin: true}, true, false},
{"targets given with -u", &types.Options{Targets: goflags.StringSlice{"example.com"}}, true, false},
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
{"uncover enabled", &types.Options{Uncover: true}, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
})
}
}
+104
View File
@@ -0,0 +1,104 @@
package runner
import (
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
)
// planScope narrows the templates run on each target by the scan plan filters
// (strict-probe reachability, tech filter), on top of the per-target profiles
// in base. It is a protocols.TargetScope, so the engine skips, counts and
// narrows clusters for these pairs the same way it does for profiles.
type planScope struct {
base protocols.TargetScope
allow func(*templates.Template, *contextargs.MetaInput) bool
// byPath maps a template path to its template. A path shared by several
// templates, or empty, is left out, so such templates are never filtered.
byPath map[string]*templates.Template
mu sync.RWMutex
selections map[planKey]*planSelection
}
// planKey identifies one selection: the target and the profile selection it
// narrows. Profile selections are comparable by contract.
type planKey struct {
input string
ip string
base protocols.TemplateSelection
}
// planSelection is the selection of one target.
type planSelection struct {
base protocols.TemplateSelection
scope *planScope
input *contextargs.MetaInput
}
var _ protocols.TargetScope = (*planScope)(nil)
func newPlanScope(base protocols.TargetScope, allow func(*templates.Template, *contextargs.MetaInput) bool, tpls []*templates.Template) *planScope {
byPath := make(map[string]*templates.Template, len(tpls))
shared := make(map[string]struct{})
for _, t := range tpls {
if t == nil || t.Path == "" {
continue
}
if _, ok := byPath[t.Path]; ok {
shared[t.Path] = struct{}{}
continue
}
byPath[t.Path] = t
}
for path := range shared {
delete(byPath, path)
}
return &planScope{
base: base,
allow: allow,
byPath: byPath,
selections: make(map[planKey]*planSelection),
}
}
// For returns the selection for input. It is the same value for the same
// input, so the engine can group targets by selection.
func (s *planScope) For(input *contextargs.MetaInput) protocols.TemplateSelection {
var base protocols.TemplateSelection
if s.base != nil {
base = s.base.For(input)
}
if input == nil {
return base
}
key := planKey{input: input.Input, ip: input.CustomIP, base: base}
s.mu.RLock()
sel, ok := s.selections[key]
s.mu.RUnlock()
if ok {
return sel
}
s.mu.Lock()
defer s.mu.Unlock()
if sel, ok := s.selections[key]; ok {
return sel
}
sel = &planSelection{base: base, scope: s, input: input.Clone()}
s.selections[key] = sel
return sel
}
// Allows reports whether the profiles and the plan filters select the template.
func (p *planSelection) Allows(templatePath string) bool {
if p.base != nil && !p.base.Allows(templatePath) {
return false
}
t, ok := p.scope.byPath[templatePath]
if !ok {
return true
}
return p.scope.allow(t, p.input)
}
+52
View File
@@ -0,0 +1,52 @@
package runner
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/stretchr/testify/require"
)
// profileScope selects by path, like per-target profiles do.
type profileScope struct{ sel *profileSelection }
func (p profileScope) For(*contextargs.MetaInput) protocols.TemplateSelection { return p.sel }
type profileSelection struct{ allowed map[string]bool }
func (p *profileSelection) Allows(path string) bool { return p.allowed[path] }
func TestPlanScope(t *testing.T) {
web := &templates.Template{ID: "web", Path: "/t/web.yaml"}
net := &templates.Template{ID: "net", Path: "/t/net.yaml"}
dupA := &templates.Template{ID: "dup-a", Path: "/t/dup.yaml"}
dupB := &templates.Template{ID: "dup-b", Path: "/t/dup.yaml"}
// the filter drops the network template on host-a only
allow := func(tpl *templates.Template, mi *contextargs.MetaInput) bool {
return mi.Input != "host-a" || tpl.ID == "web"
}
input := func(raw string) *contextargs.MetaInput {
mi := contextargs.NewMetaInput()
mi.Input = raw
return mi
}
a, b := input("host-a"), input("host-b")
scope := newPlanScope(nil, allow, []*templates.Template{web, net, dupA, dupB})
selA := scope.For(a)
require.Same(t, selA, scope.For(input("host-a")), "one selection per target, so the engine can group targets")
require.True(t, web.SelectedBy(selA))
require.False(t, net.SelectedBy(selA))
require.True(t, dupA.SelectedBy(selA), "a path shared by several templates is never filtered")
require.True(t, net.SelectedBy(scope.For(b)))
require.True(t, (&templates.Template{Path: "/t/unknown.yaml"}).SelectedBy(selA), "unknown templates are never filtered")
// The plan narrows the profiles, never widens them.
profiles := profileScope{sel: &profileSelection{allowed: map[string]bool{"/t/net.yaml": true}}}
scope = newPlanScope(profiles, allow, []*templates.Template{web, net})
require.False(t, web.SelectedBy(scope.For(b)), "a template the profile leaves out stays out")
require.True(t, net.SelectedBy(scope.For(b)))
require.False(t, net.SelectedBy(scope.For(a)))
}
+42 -4
View File
@@ -14,9 +14,25 @@ import (
)
const (
HTTP_PROXY_ENV = "HTTP_PROXY"
HTTP_PROXY_ENV = "HTTP_PROXY"
HTTPS_PROXY_ENV = "HTTPS_PROXY"
)
var proxyEnvVars = []string{
"HTTP_PROXY",
"HTTPS_PROXY",
"http_proxy",
"https_proxy",
"ALL_PROXY",
"all_proxy",
}
func setProxyEnv(proxyURL string) {
for _, env := range proxyEnvVars {
_ = os.Setenv(env, proxyURL)
}
}
// loadProxyServers load list of proxy servers from file or comma separated
func loadProxyServers(options *types.Options) error {
if len(options.Proxy) == 0 {
@@ -53,15 +69,37 @@ func loadProxyServers(options *types.Options) error {
return errkit.Wrapf(err, "failed to parse proxy got %v", err)
}
if options.ProxyInternal {
_ = os.Setenv(HTTP_PROXY_ENV, proxyURL.String())
setProxyEnv(proxyURL.String())
}
switch proxyURL.Scheme {
case proxyutils.HTTP, proxyutils.HTTPS:
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
if options.Logger != nil {
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
}
options.AliveHttpProxy = proxyURL.String()
case proxyutils.SOCKS5:
options.AliveSocksProxy = proxyURL.String()
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
if options.Logger != nil {
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
}
}
return nil
}
// validateReplayProxy rejects a -replay-proxy value the replay client cannot dial
func validateReplayProxy(raw string) error {
if raw == "" {
return nil
}
proxyURL, err := url.Parse(raw)
if err != nil {
return errkit.Wrapf(err, "invalid replay proxy %q", raw)
}
switch proxyURL.Scheme {
case proxyutils.HTTP, proxyutils.HTTPS, proxyutils.SOCKS5:
if proxyURL.Host != "" {
return nil
}
}
return fmt.Errorf("invalid replay proxy %q: want an http, https or socks5 URL", raw)
}
+97
View File
@@ -0,0 +1,97 @@
package runner
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestValidateReplayProxy(t *testing.T) {
for _, valid := range []string{"", "http://127.0.0.1:8080", "https://proxy.example:8443", "socks5://127.0.0.1:1080"} {
require.NoError(t, validateReplayProxy(valid), valid)
}
for _, invalid := range []string{"127.0.0.1:8080", "ftp://127.0.0.1:21", "http://", "://bad"} {
require.Error(t, validateReplayProxy(invalid), invalid)
}
}
type envState struct {
val string
set bool
}
func saveAndRestoreProxyEnv(t *testing.T) {
t.Helper()
original := make(map[string]envState, len(proxyEnvVars))
for _, env := range proxyEnvVars {
val, set := os.LookupEnv(env)
original[env] = envState{val: val, set: set}
_ = os.Unsetenv(env)
}
t.Cleanup(func() {
for env, state := range original {
if state.set {
_ = os.Setenv(env, state.val)
} else {
_ = os.Unsetenv(env)
}
}
})
}
func TestSetProxyEnv(t *testing.T) {
saveAndRestoreProxyEnv(t)
testProxy := "http://127.0.0.1:8080"
setProxyEnv(testProxy)
for _, env := range proxyEnvVars {
require.Equal(t, testProxy, os.Getenv(env), "expected %s to be set", env)
}
}
func TestLoadProxyServers_ProxyInternal(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
t.Run("proxy internal enabled sets all env vars", func(t *testing.T) {
saveAndRestoreProxyEnv(t)
options := &types.Options{
Proxy: goflags.StringSlice{server.URL},
ProxyInternal: true,
Timeout: 5,
Logger: &gologger.Logger{},
}
err := loadProxyServers(options)
require.NoError(t, err)
for _, env := range proxyEnvVars {
require.Equal(t, server.URL, os.Getenv(env), "expected %s to be set", env)
}
})
t.Run("proxy internal disabled does not set env vars", func(t *testing.T) {
saveAndRestoreProxyEnv(t)
options := &types.Options{
Proxy: goflags.StringSlice{server.URL},
ProxyInternal: false,
Timeout: 5,
Logger: &gologger.Logger{},
}
err := loadProxyServers(options)
require.NoError(t, err)
for _, env := range proxyEnvVars {
require.Empty(t, os.Getenv(env), "expected %s not to be set", env)
}
})
}
+199
View File
@@ -0,0 +1,199 @@
package runner
import (
"context"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
sliceutil "github.com/projectdiscovery/utils/slice"
)
type probeResult int
const (
portClosed probeResult = iota // connection refused / host unreachable
portOpen // connect succeeded
portUnknown // timeout / filtered — treat as possibly open
)
const reachabilityProbeTimeout = 750 * time.Millisecond
// strictProbeEnabled reports whether the lossless reachability prune
// applies to this run. It is limited to standard target inputs; request-shaped
// (DAST / multi-format) inputs are never pruned, because probing a request
// template as a live service is meaningless and could drop coverage.
func (r *Runner) strictProbeEnabled() bool {
return r.options.StrictProbe &&
r.inputProvider != nil &&
r.inputProvider.InputType() != provider.MultiFormatInputProvider
}
// noHTTPServiceReachable reports whether NO target actually speaks HTTP(S).
//
// When it returns true, web templates (HTTP/headless/websocket) can be excluded
// losslessly: a port that does not serve HTTP has no HTTP application, so no
// HTTP template can produce a real finding there. Unlike a bare TCP-open check,
// this catches the open-but-non-HTTP case (e.g. redis on 6379): the port is
// open, but it is not an HTTP service, so HTTP templates are pure waste.
//
// It reuses nuclei's own httpx probe (the same one used for input routing), so
// the determination is consistent with the engine and respects the network
// policy (the probe dials only through the policy-configured httpx client).
// Conservative: an explicit http/https target, any scheme that responds, or the
// inability to probe safely all keep web templates in place.
// Skipped when -no-httpx is set so we do not probe or prune against user intent.
func (r *Runner) noHTTPServiceReachable() bool {
if r.inputProvider == nil || r.options.DisableHTTPProbe {
return false
}
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
if dialers == nil {
return false // cannot probe within policy — keep web templates
}
httpxOptions := httpx.DefaultOptions
if r.options.AliveHttpProxy != "" {
httpxOptions.Proxy = r.options.AliveHttpProxy
} else if r.options.AliveSocksProxy != "" {
httpxOptions.Proxy = r.options.AliveSocksProxy
}
httpxOptions.RetryMax = r.options.Retries
if r.options.Timeout > 0 {
httpxOptions.Timeout = time.Duration(r.options.Timeout) * time.Second
}
httpxOptions.NetworkPolicy = dialers.NetworkPolicy
client, err := httpx.New(&httpxOptions)
if err != nil {
return false // cannot probe — keep web templates
}
anyHTTP := false
r.inputProvider.Iterate(func(mi *contextargs.MetaInput) bool {
if strings.HasPrefix(mi.Input, "http://") || strings.HasPrefix(mi.Input, "https://") {
anyHTTP = true
return false
}
if utils.ProbeURL(mi.Input, client) != "" {
anyHTTP = true
return false
}
return true
})
return !anyHTTP
}
// tcpNetworkOnlyPorts returns the concrete numeric TCP ports of a single-protocol
// network template, or ok=false if the template is ineligible for port pruning.
func tcpNetworkOnlyPorts(t *templates.Template) ([]string, bool) {
if len(t.RequestsNetwork)+len(t.RequestsWithTCP) == 0 {
return nil, false
}
// must be network-only (no other protocol request could still be reachable)
otherReqs := len(t.RequestsHTTP) + len(t.RequestsWithHTTP) + len(t.RequestsHeadless) +
len(t.RequestsDNS) + len(t.RequestsFile) + len(t.RequestsSSL) +
len(t.RequestsCode) + len(t.RequestsJavascript) + len(t.Workflows)
if otherReqs > 0 {
return nil, false
}
reqs := make([]*network.Request, 0, len(t.RequestsNetwork)+len(t.RequestsWithTCP))
reqs = append(reqs, t.RequestsNetwork...)
reqs = append(reqs, t.RequestsWithTCP...)
var ports []string
for _, req := range reqs {
for _, addr := range req.Address {
if strings.Contains(strings.ToLower(addr), "udp://") {
return nil, false // UDP: a TCP probe cannot prove unreachability
}
}
pp := splitPorts(req.Port)
if len(pp) == 0 {
return nil, false // empty/dynamic port (uses input port) — keep
}
for _, p := range pp {
if !isNumericPort(p) {
return nil, false // service name or template var — keep
}
ports = append(ports, p)
}
}
if len(ports) == 0 {
return nil, false
}
return sliceutil.Dedupe(ports), true
}
func isNumericPort(p string) bool {
n, err := strconv.Atoi(p)
return err == nil && n > 0 && n < 65536
}
// hostAndExplicitPort splits a target into host and explicit port (port empty if
// the target is a bare host).
func hostAndExplicitPort(input string) (string, string) {
if strings.Contains(input, "://") {
if u, err := url.Parse(input); err == nil && u.Hostname() != "" {
return u.Hostname(), u.Port()
}
}
if host, port, err := net.SplitHostPort(input); err == nil {
return host, port
}
return input, ""
}
// probe classifies reachability of host:port WITHIN the scan's network policy.
// Security first: it never dials a target the policy forbids and never bypasses
// the sandbox — all probing goes through the execution's fastdialer, which
// enforces the same DenyList (‑lna, exclude-targets, private/metadata ranges,
// DNS-rebind protection) as the scan itself.
//
// It reconciles security with losslessness by mapping every uncertain case to
// portUnknown ("keep, don't dial"): a policy-denied host, a missing dialer, or a
// timeout all leave templates in place. Pruning happens only on a definitive,
// policy-permitted, refused connection.
func (r *Runner) probe(host, port string) probeResult {
if !protocolstate.IsHostAllowed(r.options.ExecutionId, host) {
// Policy denies this host: do not dial (security), do not prune (lossless).
return portUnknown
}
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
if dialers == nil || dialers.Fastdialer == nil {
// No policy-enforcing dialer available: never fall back to a raw dial.
return portUnknown
}
return classifyDial(dialers.Fastdialer.Dial, net.JoinHostPort(host, port), reachabilityProbeTimeout)
}
type dialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
// classifyDial performs one connect via the supplied (policy-enforcing) dialer.
func classifyDial(dial dialFunc, addr string, timeout time.Duration) probeResult {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := dial(ctx, "tcp", addr)
if err == nil {
_ = conn.Close()
return portOpen
}
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return portUnknown
}
msg := strings.ToLower(err.Error())
// Only definitive negatives prune. DNS blips / temporary dialer errors must
// stay unknown so mixed-fleet probing under load remains lossless.
if strings.Contains(msg, "refused") || strings.Contains(msg, "unreachable") ||
strings.Contains(msg, "no route to host") || strings.Contains(msg, "network is unreachable") {
return portClosed
}
return portUnknown
}
+133
View File
@@ -0,0 +1,133 @@
package runner
import (
"context"
"net"
"testing"
"time"
httpproto "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
jsproto "github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
)
func TestHostAndExplicitPort(t *testing.T) {
cases := []struct{ in, host, port string }{
{"127.0.0.1", "127.0.0.1", ""},
{"127.0.0.1:6379", "127.0.0.1", "6379"},
{"http://example.com:8080", "example.com", "8080"},
{"https://example.com", "example.com", ""},
}
for _, c := range cases {
h, p := hostAndExplicitPort(c.in)
if h != c.host || p != c.port {
t.Errorf("hostAndExplicitPort(%q) = (%q,%q), want (%q,%q)", c.in, h, p, c.host, c.port)
}
}
}
func TestIsNumericPort(t *testing.T) {
for _, p := range []string{"80", "6379", "65535", "1"} {
if !isNumericPort(p) {
t.Errorf("isNumericPort(%q) = false, want true", p)
}
}
for _, p := range []string{"ftp", "{{Port}}", "0", "70000", "", "80a"} {
if isNumericPort(p) {
t.Errorf("isNumericPort(%q) = true, want false", p)
}
}
}
// TestTCPNetworkOnlyPorts locks in the losslessness guards: only single-protocol
// network templates with concrete numeric TCP ports are eligible for pruning.
func TestTCPNetworkOnlyPorts(t *testing.T) {
netTmpl := func(addr []string, port string) *templates.Template {
return &templates.Template{RequestsNetwork: []*network.Request{{Address: addr, Port: port}}}
}
t.Run("concrete tcp port eligible", func(t *testing.T) {
ports, ok := tcpNetworkOnlyPorts(netTmpl([]string{"{{Hostname}}"}, "6379,6380"))
if !ok || len(ports) != 2 {
t.Fatalf("want ok with 2 ports, got ok=%v ports=%v", ok, ports)
}
})
t.Run("tls address still tcp", func(t *testing.T) {
if _, ok := tcpNetworkOnlyPorts(netTmpl([]string{"tls://{{Hostname}}"}, "6379")); !ok {
t.Fatal("tls:// should remain eligible (still TCP)")
}
})
t.Run("udp address rejected", func(t *testing.T) {
if _, ok := tcpNetworkOnlyPorts(netTmpl([]string{"udp://{{Hostname}}"}, "161")); ok {
t.Fatal("udp:// must be ineligible — a TCP probe cannot prove UDP unreachability")
}
})
t.Run("service-name port rejected", func(t *testing.T) {
if _, ok := tcpNetworkOnlyPorts(netTmpl([]string{"{{Hostname}}"}, "ftp")); ok {
t.Fatal("service-name port must be ineligible")
}
})
t.Run("dynamic port rejected", func(t *testing.T) {
if _, ok := tcpNetworkOnlyPorts(netTmpl([]string{"{{Hostname}}"}, "{{Port}}")); ok {
t.Fatal("template-var port must be ineligible")
}
})
t.Run("empty port rejected", func(t *testing.T) {
if _, ok := tcpNetworkOnlyPorts(netTmpl([]string{"{{Hostname}}"}, "")); ok {
t.Fatal("empty port (uses input port) must be ineligible")
}
})
t.Run("mixed protocol rejected", func(t *testing.T) {
mixed := netTmpl([]string{"{{Hostname}}"}, "6379")
mixed.RequestsHTTP = []*httpproto.Request{{}}
if _, ok := tcpNetworkOnlyPorts(mixed); ok {
t.Fatal("template with an HTTP request must be ineligible")
}
})
t.Run("javascript protocol rejected", func(t *testing.T) {
js := &templates.Template{RequestsJavascript: []*jsproto.Request{{}}}
if _, ok := tcpNetworkOnlyPorts(js); ok {
t.Fatal("javascript template must be ineligible (opaque dial protocol)")
}
})
}
func TestClassifyDial(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Skip("cannot listen:", err)
}
defer func() { _ = ln.Close() }()
d := &net.Dialer{}
if got := classifyDial(d.DialContext, ln.Addr().String(), reachabilityProbeTimeout); got != portOpen {
t.Errorf("classifyDial(open) = %v, want portOpen", got)
}
// an unused low port on loopback should refuse quickly
if got := classifyDial(d.DialContext, "127.0.0.1:1", reachabilityProbeTimeout); got != portClosed {
t.Errorf("classifyDial(closed) = %v, want portClosed", got)
}
}
func TestClassifyDialDNSFailureIsUnknown(t *testing.T) {
// NXDOMAIN / resolution failure must not prune (lossless under load).
dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
return nil, &net.OpError{Op: "dial", Net: network, Err: &net.DNSError{Err: "no such host", Name: "no-such-host.invalid", IsNotFound: true}}
}
got := classifyDial(dial, "no-such-host.invalid:6379", reachabilityProbeTimeout)
if got != portUnknown {
t.Fatalf("classifyDial(dns-fail) = %v, want portUnknown", got)
}
}
func TestClassifyDialTimeoutIsUnknown(t *testing.T) {
// A dropped connect runs into the timeout and must not prune.
dial := func(ctx context.Context, network, addr string) (net.Conn, error) {
<-ctx.Done()
return nil, &net.OpError{Op: "dial", Net: network, Err: ctx.Err()}
}
got := classifyDial(dial, "192.0.2.1:65535", 50*time.Millisecond)
if got != portUnknown {
t.Fatalf("classifyDial(timeout) = %v, want portUnknown", got)
}
}
+250 -13
View File
@@ -3,9 +3,12 @@ package runner
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"slices"
"sort"
"strings"
"sync/atomic"
@@ -42,10 +45,13 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
scanplan "github.com/projectdiscovery/nuclei/v3/pkg/core/plan"
"github.com/projectdiscovery/nuclei/v3/pkg/core/techfilter"
"github.com/projectdiscovery/nuclei/v3/pkg/external/customtemplates"
fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
parsers "github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/projectfile"
@@ -54,6 +60,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
@@ -63,14 +70,18 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
"github.com/projectdiscovery/retryablehttp-go"
ptrutil "github.com/projectdiscovery/utils/ptr"
stringsutil "github.com/projectdiscovery/utils/strings"
)
var (
@@ -83,7 +94,7 @@ var (
// Runner is a client for running the enumeration process.
type Runner struct {
output output.Writer
interactsh *interactsh.Client
interactsh interactsh.IClient
options *types.Options
projectFile *projectfile.ProjectFile
catalog catalog.Catalog
@@ -109,6 +120,7 @@ type Runner struct {
httpApiEndpoint *httpapi.Server
fuzzStats *fuzzStats.Tracker
dastServer *server.DASTServer
llmClient llmclient.Client
}
// New creates a new client for running the enumeration process.
@@ -122,6 +134,12 @@ func New(options *types.Options) (*Runner, error) {
return nil, fmt.Errorf("initialize nuclei configuration: %w", err)
}
llmClient, err := configureLLM(options)
if err != nil {
return nil, fmt.Errorf("configure llm: %w", err)
}
runner.llmClient = llmClient
if options.HealthCheck {
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
os.Exit(0)
@@ -151,7 +169,9 @@ func New(options *types.Options) (*Runner, error) {
tm := &installer.TemplateManager{
CustomTemplates: ctm,
DisablePublicTemplates: options.PublicTemplateDisableDownload,
Channel: options.UpdateTemplatesChannel,
}
templatesVersion := config.DefaultConfig.TemplateVersion
if err := tm.FreshInstallIfNotExists(); err != nil {
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
}
@@ -168,7 +188,12 @@ func New(options *types.Options) (*Runner, error) {
if options.UpdateTemplates {
// we automatically check for updates unless explicitly disabled
// this print statement is only to inform the user that there are no updates
if !config.DefaultConfig.NeedsTemplateUpdate() {
noUpdates := !config.DefaultConfig.NeedsTemplateUpdate()
if tm.Channel == installer.TemplatesChannelLatest {
// the latest channel updates without a new release
noUpdates = config.DefaultConfig.TemplateVersion == templatesVersion
}
if noUpdates {
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
}
@@ -213,7 +238,7 @@ func New(options *types.Options) (*Runner, error) {
runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
var httpclient *retryablehttp.Client
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
if options.ProxyInternal && (options.AliveHttpProxy != "" || options.AliveSocksProxy != "") {
var err error
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
@@ -412,13 +437,7 @@ func New(options *types.Options) (*Runner, error) {
opts.HTTPClient = httpclient
}
if opts.HTTPClient == nil {
httpOpts := retryablehttp.DefaultOptionsSingle
httpOpts.Timeout = 20 * time.Second // for stability reasons
if options.Timeout > 20 {
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
}
// 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)
opts.HTTPClient = newInteractshHTTPClient(options)
}
interactshClient, err := interactsh.New(opts)
@@ -657,6 +676,7 @@ func (r *Runner) RunEnumeration() error {
executorOpts := &protocols.ExecutorOptions{
Output: r.output,
Options: r.options,
LLMClient: r.llmClient,
Progress: r.progress,
Catalog: r.catalog,
IssuesClient: r.issuesClient,
@@ -675,6 +695,12 @@ func (r *Runner) RunEnumeration() error {
DoNotCache: r.options.DoNotCacheTemplates,
Logger: r.Logger,
}
// Attach scan-scoped HTTP response cache only for opt-in -tf, and only
// before templates compile so request executers share the same pointer.
// Default scans leave this nil (no behavioral change).
if r.options.UsesResponseCache() {
executorOpts.HTTPResponseCache = httprespcache.New()
}
if config.DefaultConfig.IsDebugArgEnabled(config.DebugExportURLPattern) {
// Go StdLib style experimental/debug feature switch
@@ -717,6 +743,10 @@ func (r *Runner) RunEnumeration() error {
executorOpts.HostErrorsCache = cache
}
if r.options.HostBackoff {
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
}
executorEngine := core.New(r.options)
executorEngine.SetExecuterOptions(executorOpts)
@@ -728,6 +758,28 @@ func (r *Runner) RunEnumeration() error {
// If using input-file flags, only load http fuzzing based templates.
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
// Lossless reachability prune: if no web port is reachable on any target,
// exclude web-protocol templates at load time — they cannot connect, so
// cannot produce a finding. Reuses the standard protocol-type exclusion on
// the loader config only, so shared options stay unchanged across runs.
if r.strictProbeEnabled() && r.noHTTPServiceReachable() {
loaderConfig.ExcludeProtocols = append(slices.Clone(loaderConfig.ExcludeProtocols),
templateTypes.HTTPProtocol, templateTypes.HeadlessProtocol, templateTypes.WebsocketProtocol)
r.Logger.Info().Msgf("reachability prune: no HTTP service on any target; excluding web-protocol templates (lossless)")
}
profiles := r.targetProfiles()
if profiles != nil {
if r.options.AutomaticScan {
return errors.New("per-target profiles cannot be combined with automatic scan")
}
loaderConfig.TargetFilter = profiles.LoadFilter()
}
if r.hasPinnedTargets() {
if err := pinnedTargetConflict(r.options); err != nil {
return err
}
}
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
@@ -772,6 +824,15 @@ func (r *Runner) RunEnumeration() error {
if err := store.Load(); err != nil {
return err
}
// Each template is parsed once per scan, so the compiled cache only holds
// pre-compilation snapshots that nothing reads after loading.
if parser, ok := r.parser.(*templates.Parser); ok {
parser.PurgeCompiled()
}
if profiles != nil {
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
executorOpts.TargetScope = profiles
}
// TODO: remove below functions after v3 or update warning messages
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
@@ -818,6 +879,15 @@ func (r *Runner) RunEnumeration() error {
return errors.Wrap(err, "could not probe http input")
}
executorOpts.InputHelper.InputsHTTP = inputHelpers
executorOpts.InputHelper.InputsHTTPProbed = r.inputProvider.InputType() != provider.MultiFormatInputProvider
// Under strict-probe, skip web templates per-input for targets that
// probed as non-HTTP (lossless), instead of falling back to raw URL.
executorOpts.InputHelper.StrictProbe = r.strictProbeEnabled()
if r.strictProbeEnabled() {
r.Logger.Info().Msgf("Strict probe enabled: hosts without httpx confirmation are skipped for HTTP/headless templates")
}
} else if r.strictProbeEnabled() && r.options.DisableHTTPProbe {
r.Logger.Warning().Msgf("strict-probe has no effect with -no-httpx (httpx probing is disabled)")
}
inputCount := int(r.inputProvider.Count())
@@ -966,7 +1036,7 @@ func (r *Runner) executeSmartWorkflowInput(executorOpts *protocols.ExecutorOptio
if err != nil {
return nil, errors.Wrap(err, "could not create automatic scan service")
}
if err := service.Execute(); err != nil {
if err := service.Execute(context.Background()); err != nil {
return nil, errors.Wrap(err, "could not execute automatic scan")
}
result := &atomic.Bool{}
@@ -992,15 +1062,157 @@ func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine)
return nil, errors.New("no templates provided for scan")
}
// pass input provider to engine
// TODO: this should be not necessary after r.hmapInputProvider is removed + refactored
if r.inputProvider == nil {
return nil, errors.New("no input provider found")
}
return r.executeTemplatesWithScanPlan(engine, finalTemplates)
}
// executeTemplatesWithScanPlan chooses spray strategy and optionally attaches a
// reachability / tech filter, then runs a single Execute pass. Wall clock must
// match or beat baseline spray; probing is skipped when its cost would dominate.
func (r *Runner) executeTemplatesWithScanPlan(engine *core.Engine, finalTemplates []*templates.Template) (*atomic.Bool, error) {
defer r.attachScanPlan(engine, finalTemplates)()
results := engine.ExecuteScanWithOpts(context.Background(), finalTemplates, r.inputProvider, r.options.DisableClustering)
return results, nil
}
// attachScanPlan applies the scan plan to engine for one scan. It returns a
// func that undoes the per-scan state once the scan is done.
func (r *Runner) attachScanPlan(engine *core.Engine, finalTemplates []*templates.Template) func() {
var undo []func()
cleanup := func() {
for i := len(undo) - 1; i >= 0; i-- {
undo[i]()
}
}
hostCount := int(r.inputProvider.Count())
baselineReqs := estimateTemplateRequests(finalTemplates) * hostCount
concreteNet, portsToProbe := countReachabilityStats(finalTemplates)
techBound := techfilter.CountTechBound(finalTemplates)
planIn := scanplan.Input{
Hosts: hostCount,
Templates: len(finalTemplates),
Requests: baselineReqs,
BulkSize: r.options.BulkSize,
Stream: r.options.Stream,
StrictProbe: r.strictProbeEnabled(),
PortsToProbe: portsToProbe,
ConcreteNetworkTemplates: concreteNet,
TechFilter: r.options.TechFilter,
TechBoundTemplates: techBound,
}
p := scanplan.Decide(planIn)
// Resolve -ss auto before Execute so the engine placeholder is unused.
if stringsutil.EqualFoldAny(r.options.ScanStrategy, scanstrategy.Auto.String(), "") {
r.options.ScanStrategy = p.Strategy
}
var httpHelper *input.Helper
if opts := engine.ExecuterOptions(); opts != nil {
httpHelper = opts.InputHelper
}
var reachFilter func(*templates.Template, *contextargs.MetaInput) bool
var techFilterFn func(*templates.Template, *contextargs.MetaInput) bool
var techIdx *techReachabilityIndex
filteredReqs := baselineReqs
if p.BuildReachability {
idx, groups, err := r.buildHostReachability(finalTemplates, httpHelper, p.ProbePorts)
if err != nil {
r.Logger.Warning().Msgf("could not build host reachability index, continuing unfiltered: %s", err)
} else {
baseline, filtered := estimateGroupedExecutions(finalTemplates, groups, hostCount)
filteredReqs = filtered
saved := baseline - filtered
pct := 0.0
if baseline > 0 {
pct = 100 * float64(saved) / float64(baseline)
}
for _, g := range groups {
r.Logger.Info().Msgf("host-group %s: hosts=%d templates=%d", g.key, len(g.hosts), len(templatesForHostGroup(finalTemplates, g)))
}
r.Logger.Info().Msgf(
"host-groups: groups=%d baseline_exec=%d filtered_exec=%d saved=%d (%.1f%%) mode=single-pass",
len(groups), baseline, filtered, saved, pct,
)
if p.UseReachabilityFilter {
reachFilter = idx.Allow
}
}
}
if p.UseTechFilter {
var err error
var cache *httprespcache.Cache
if opts := engine.ExecuterOptions(); opts != nil {
if opts.HTTPResponseCache == nil {
opts.HTTPResponseCache = httprespcache.New()
}
cache = opts.HTTPResponseCache
}
techIdx, err = r.buildTechReachability(finalTemplates, httpHelper, cache)
if err != nil {
r.Logger.Warning().Msgf("could not build tech filter index, continuing without tech-filter: %s", err)
if cache != nil {
cache.Disable()
}
techIdx = nil
} else {
base, techFiltered := estimateTechFilteredExecutions(finalTemplates, techIdx, hostCount)
// When stacked with reachability, take the more aggressive (lower) estimate
// as a lower bound for progress; actual AND filter may skip more.
if techFiltered < filteredReqs {
filteredReqs = techFiltered
}
saved := base - techFiltered
pct := 0.0
if base > 0 {
pct = 100 * float64(saved) / float64(base)
}
r.Logger.Info().Msgf(
"tech-filter: baseline_exec=%d filtered_exec=%d saved=%d (%.1f%%) bound_templates=%d",
base, techFiltered, saved, pct, techBound,
)
techFilterFn = techIdx.Allow
}
} else if opts := engine.ExecuterOptions(); opts != nil && opts.HTTPResponseCache != nil && !r.options.ResponseCache {
// -tf set but planner skipped (e.g. no tech-bound templates): do not
// alter request paths via an idle response cache. Keep it when the user
// asked for -response-cache explicitly.
opts.HTTPResponseCache.Disable()
}
p.ApplyFiltered(filteredReqs)
if opts := engine.ExecuterOptions(); opts != nil {
// The filters narrow the templates per target through the same
// TargetScope as per-target profiles, on top of them. The engine then
// skips the pairs, counts progress and narrows clusters as it does for
// profiles.
if filter := composeTemplateFilters(reachFilter, techFilterFn); filter != nil {
profiles := opts.TargetScope
opts.TargetScope = newPlanScope(profiles, filter, finalTemplates)
undo = append(undo, func() { opts.TargetScope = profiles })
}
if opts.HTTPResponseCache != nil {
undo = append(undo, func() {
hits, misses, stores := opts.HTTPResponseCache.Stats()
r.Logger.Info().Msgf("http-response-cache: hits=%d misses=%d stores=%d", hits, misses, stores)
opts.HTTPResponseCache = nil
})
}
}
r.Logger.Info().Msgf("scan-plan: strategy=%s reach=%v tech=%v expected_req=%d reason=%s",
r.options.ScanStrategy, p.UseReachabilityFilter, p.UseTechFilter, p.ExpectedRequests, p.Reason)
return cleanup
}
// displayExecutionInfo prints parser stats, version info, and scan counts.
func (r *Runner) displayExecutionInfo(store *loader.Store) {
// Display parser stats for templates loaded into the store.
@@ -1209,3 +1421,28 @@ func init() {
HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false)
EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false)
}
func newInteractshHTTPClient(options *types.Options) *retryablehttp.Client {
httpOpts := retryablehttp.DefaultOptionsSingle
httpOpts.Timeout = 20 * time.Second // for stability reasons
if options.Timeout > 20 {
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
}
if dialers := protocolstate.GetDialersWithId(options.ExecutionId); dialers != nil && dialers.Fastdialer != nil {
// The pooled default keeps ProxyFromEnvironment and the TLS config that
// INTERACTSH_TLS_VERIFY edits. Nuclei only exports HTTP_PROXY when -pi
// is set, and that path uses the pooled client, so -proxy without -pi
// still dials the interactsh server directly.
transport := retryablehttp.DefaultReusePooledTransport()
transport.DialContext = dialers.Fastdialer.Dial
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialers.Fastdialer.DialTLS(ctx, network, addr)
}
httpOpts.HttpClient = &http.Client{
Transport: transport,
Timeout: httpOpts.Timeout,
}
}
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
return retryablehttp.NewClient(httpOpts)
}
+54
View File
@@ -1,13 +1,18 @@
package runner
import (
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
@@ -143,3 +148,52 @@ func TestWalkReflectStructHandlesNestedStructs(t *testing.T) {
require.Equal(t, "2", testStruct.Struct.B)
require.Equal(t, "true", testStruct.Struct.C)
}
func TestNewInteractshHTTPClient(t *testing.T) {
// ProxyFromEnvironment reads the environment once per process. Set it
// before the first request. NO_PROXY keeps the local dial direct.
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
t.Setenv(key, "")
}
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
options := &types.Options{
ExecutionId: "test-interactsh-client-dialer",
Timeout: 35,
}
require.NoError(t, protocolstate.Init(options))
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
client := newInteractshHTTPClient(options)
require.NotNil(t, client)
require.Equal(t, 35*time.Second, client.HTTPClient.Timeout)
transport, ok := client.HTTPClient.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.Proxy)
require.NotNil(t, transport.DialContext)
require.NotNil(t, transport.DialTLSContext)
require.NotNil(t, transport.TLSClientConfig, "INTERACTSH_TLS_VERIFY edits this config")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)
resp, err := client.HTTPClient.Get(srv.URL)
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode)
require.NoError(t, resp.Body.Close())
host := srv.Listener.Addr().(*net.TCPAddr).IP.String()
dialers := protocolstate.GetDialersWithId(options.ExecutionId)
require.Equal(t, host, dialers.Fastdialer.GetDialedIP(host))
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
require.NoError(t, err)
proxyURL, err := transport.Proxy(req)
require.NoError(t, err)
require.NotNil(t, proxyURL)
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
}
+40
View File
@@ -0,0 +1,40 @@
package runner
import (
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
// AttachScanPlan applies -strict-probe and -tech-filter to an SDK engine for
// one scan, the same way the CLI runner does. It returns a func that undoes the
// per-scan state; call it once the scan is done. When neither option is set it
// does nothing, so default SDK scans keep their behavior.
func AttachScanPlan(opts *types.Options, inputProvider provider.InputProvider, logger *gologger.Logger, engine *core.Engine, tpls []*templates.Template) func() {
if opts == nil || engine == nil || inputProvider == nil || (!opts.StrictProbe && !opts.TechFilter) {
return func() {}
}
if logger == nil {
logger = gologger.DefaultLogger
}
// The SDK engine is reused across scans, so keep the options and the
// response cache it owns as they were before this scan.
strategy := opts.ScanStrategy
executerOpts := engine.ExecuterOptions()
var responseCache *httprespcache.Cache
if executerOpts != nil {
responseCache = executerOpts.HTTPResponseCache
}
r := &Runner{options: opts, inputProvider: inputProvider, Logger: logger}
cleanup := r.attachScanPlan(engine, tpls)
return func() {
cleanup()
opts.ScanStrategy = strategy
if executerOpts != nil {
executerOpts.HTTPResponseCache = responseCache
}
}
}
+44
View File
@@ -0,0 +1,44 @@
package runner
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
// Without -strict-probe or -tech-filter the SDK hook changes nothing.
func TestAttachScanPlanNoop(t *testing.T) {
opts := types.DefaultOptions()
opts.ScanStrategy = "auto"
engine := core.New(opts)
engine.SetExecuterOptions(&protocols.ExecutorOptions{Options: opts})
ip := provider.NewSimpleInputProviderWithUrls(opts.ExecutionId, "http://127.0.0.1:1")
AttachScanPlan(opts, ip, nil, engine, nil)()
require.Equal(t, "auto", opts.ScanStrategy)
require.Nil(t, engine.ExecuterOptions().TargetScope)
}
// The SDK engine is reused across scans, so the hook must leave its options
// and its response cache as they were before the scan.
func TestAttachScanPlanRestoresEngineState(t *testing.T) {
opts := types.DefaultOptions()
opts.ScanStrategy = "auto"
opts.TechFilter = true
cache := httprespcache.New()
engine := core.New(opts)
engine.SetExecuterOptions(&protocols.ExecutorOptions{Options: opts, HTTPResponseCache: cache})
ip := provider.NewSimpleInputProviderWithUrls(opts.ExecutionId, "http://127.0.0.1:1")
cleanup := AttachScanPlan(opts, ip, nil, engine, nil)
require.NotEqual(t, "auto", opts.ScanStrategy, "the plan resolves the auto strategy for the scan")
cleanup()
require.Equal(t, "auto", opts.ScanStrategy)
require.Same(t, cache, engine.ExecuterOptions().HTTPResponseCache)
require.Nil(t, engine.ExecuterOptions().TargetScope)
}
+41
View File
@@ -0,0 +1,41 @@
package runner
import (
"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
// targetProfiles returns the per-target profiles read from input, or nil when
// no target has one.
func (r *Runner) targetProfiles() *targetprofile.Registry {
provider, ok := r.inputProvider.(interface {
TargetProfiles() *targetprofile.Registry
})
if !ok || !provider.TargetProfiles().Scoped() {
return nil
}
return provider.TargetProfiles()
}
// hasPinnedTargets reports whether a target line pinned a target to an ip.
func (r *Runner) hasPinnedTargets() bool {
provider, ok := r.inputProvider.(interface {
PinnedTargets() bool
})
return ok && provider.PinnedTargets()
}
// pinnedTargetConflict returns an error for an option that cannot run with
// targets pinned to an ip.
func pinnedTargetConflict(options *types.Options) error {
switch {
case options.AutomaticScan:
return errors.New("targets pinned to an ip cannot be combined with automatic scan")
case options.AliveHttpProxy != "" || options.AliveSocksProxy != "":
return errors.New("targets pinned to an ip cannot be combined with a proxy, as the proxy resolves the host itself")
case options.ReplayProxy != "":
return errors.New("targets pinned to an ip cannot be combined with -replay-proxy, as the replay proxy resolves the host itself")
}
return nil
}
+27
View File
@@ -0,0 +1,27 @@
package runner
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestPinnedTargetConflict(t *testing.T) {
tests := []struct {
name string
options *types.Options
wantErr bool
}{
{name: "no conflicting option", options: &types.Options{}},
{name: "automatic scan", options: &types.Options{AutomaticScan: true}, wantErr: true},
{name: "http proxy", options: &types.Options{AliveHttpProxy: "http://127.0.0.1:8080"}, wantErr: true},
{name: "socks proxy", options: &types.Options{AliveSocksProxy: "socks5://127.0.0.1:1080"}, wantErr: true},
{name: "replay proxy", options: &types.Options{ReplayProxy: "http://127.0.0.1:8080"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.wantErr, pinnedTargetConflict(tt.options) != nil)
})
}
}
+283
View File
@@ -0,0 +1,283 @@
package runner
import (
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/core/techfilter"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/retryablehttp-go"
syncutil "github.com/projectdiscovery/utils/sync"
unitutils "github.com/projectdiscovery/utils/unit"
wappalyzer "github.com/projectdiscovery/wappalyzergo"
)
const (
techFilterWorkers = 50
techFilterMaxBody = 1 * unitutils.Mega
techFilterHTTPTimeout = 5 * time.Second
techFilterMaxRedirects = 5
)
// techReachabilityIndex filters template×target pairs by product/macro tags.
type techReachabilityIndex struct {
byInput map[string]techfilter.HostProfile // keyed by normalizeInputKey
// boundTags memoizes template ID -> product tags. It is filled lazily from
// the template execution goroutines, so every access needs the mutex.
tagsMu sync.RWMutex
boundTags map[string][]string
}
func normalizeInputKey(raw string) string {
return strings.TrimRight(strings.TrimSpace(raw), "/")
}
// Allow is fail-open: missing profile or unbound template => true.
func (idx *techReachabilityIndex) Allow(t *templates.Template, mi *contextargs.MetaInput) bool {
if idx == nil || t == nil || mi == nil {
return true
}
profile, ok := idx.lookup(mi.Input)
if !ok {
return true
}
return techfilter.AllowTags(profile, idx.tagsFor(t))
}
func (idx *techReachabilityIndex) tagsFor(t *templates.Template) []string {
if t == nil {
return nil
}
idx.tagsMu.RLock()
tags, ok := idx.boundTags[t.ID]
idx.tagsMu.RUnlock()
if ok {
return tags
}
tags = techfilter.TemplateProductTags(t)
idx.tagsMu.Lock()
defer idx.tagsMu.Unlock()
if idx.boundTags == nil {
idx.boundTags = make(map[string][]string)
}
// Another goroutine may have won the race; keep one slice per template ID.
if existing, ok := idx.boundTags[t.ID]; ok {
return existing
}
idx.boundTags[t.ID] = tags
return tags
}
func (idx *techReachabilityIndex) lookup(raw string) (techfilter.HostProfile, bool) {
if idx == nil || idx.byInput == nil {
return techfilter.HostProfile{}, false
}
if p, ok := idx.byInput[normalizeInputKey(raw)]; ok {
return p, true
}
if p, ok := idx.byInput[raw]; ok {
return p, true
}
return techfilter.HostProfile{}, false
}
// buildTechReachability fingerprints HTTP(S) targets and builds a tag index.
// When cache is non-nil, fingerprint GET responses seed it so matching template
// GETs pay no extra RTT.
func (r *Runner) buildTechReachability(tpls []*templates.Template, httpHelper *input.Helper, cache *httprespcache.Cache) (*techReachabilityIndex, error) {
_ = tpls
wapp, err := wappalyzer.New()
if err != nil {
return nil, err
}
// Redirects must be followed. A site that answers "/" with a 302 to its real
// entry point (WordPress -> /wp-admin/install.php, any app -> /login) returns
// an empty body, so the probe would detect nothing and every template bound
// to the undetected product would be pruned. Same-host only, so the probe
// never leaves the target authority.
httpclient, err := httpclientpool.Get(r.options, &httpclientpool.Configuration{
DisableCookie: true,
ResponseHeaderTimeout: techFilterHTTPTimeout,
RedirectFlow: httpclientpool.FollowSameHostRedirect,
MaxRedirects: techFilterMaxRedirects,
}, "")
if err != nil {
return nil, err
}
idx := &techReachabilityIndex{
byInput: make(map[string]techfilter.HostProfile),
boundTags: make(map[string][]string),
}
var mu sync.Mutex
sg, err := syncutil.New(syncutil.WithSize(techFilterWorkers))
if err != nil {
return nil, err
}
fingerprinted := 0
eligible := 0
r.inputProvider.Iterate(func(mi *contextargs.MetaInput) bool {
if mi == nil || mi.Input == "" {
return true
}
fpURL := fingerprintURLForInput(mi.Input, httpHelper)
if fpURL == "" {
return true
}
eligible++
sg.Add()
go func(inputKey, url string) {
defer sg.Done()
profile := fingerprintTarget(wapp, httpclient, url, cache)
key := normalizeInputKey(inputKey)
mu.Lock()
idx.byInput[key] = profile
if profile.HasTags() {
fingerprinted++
}
mu.Unlock()
}(mi.Input, fpURL)
return true
})
sg.Wait()
gologger.Info().Msgf("tech-filter: fingerprinted=%d/%d hosts with tags (eligible=%d)", fingerprinted, len(idx.byInput), eligible)
return idx, nil
}
// fingerprintURLForInput returns an absolute http(s) URL to fingerprint, or "".
// Bare hosts reuse InputsHTTP probed URLs when available.
func fingerprintURLForInput(raw string, helper *input.Helper) string {
if looksLikeHTTPTarget(raw) {
return raw
}
if helper != nil && helper.InputsHTTP != nil {
if probed, ok := helper.InputsHTTP.Get(raw); ok && len(probed) > 0 {
return string(probed)
}
}
return ""
}
func looksLikeHTTPTarget(raw string) bool {
lower := strings.ToLower(raw)
return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://")
}
func fingerprintTarget(wapp *wappalyzer.Wappalyze, client *retryablehttp.Client, rawURL string, cache *httprespcache.Cache) techfilter.HostProfile {
// Prefer a trailing-slash form that matches typical {{BaseURL}}/ template URLs.
seedURL := rawURL
if !strings.HasSuffix(seedURL, "/") {
seedURL = seedURL + "/"
}
req, err := retryablehttp.NewRequest(http.MethodGet, seedURL, nil)
if err != nil {
return techfilter.HostProfile{}
}
// Same user agent the templates will send, so the response seeded below is
// reachable from the cache instead of being stored under a key nothing hits.
req.Header.Set("User-Agent", httpProtocol.StableUserAgent())
resp, err := client.Do(req)
if err != nil {
return techfilter.HostProfile{}
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(io.LimitReader(resp.Body, techFilterMaxBody))
if err != nil {
return techfilter.HostProfile{}
}
if cache != nil {
seedFingerprintResponse(cache, req, resp, body, seedURL)
}
info := wapp.FingerprintWithInfo(resp.Header, body)
products := make(map[string][]string, len(info))
for name, app := range info {
products[name] = append([]string(nil), app.Categories...)
}
return techfilter.ProfileFromFingerprint(products)
}
// seedFingerprintResponse stores the probe response under the URL it actually
// belongs to, so templates can reuse the RTT.
//
// The probe follows redirects, so resp may describe a different URL than the one
// requested. Seeding it under the requested URL would hand every template that
// asks for that URL the redirect target's status, headers and body instead of
// the redirect itself, which silently changes what they match on.
func seedFingerprintResponse(cache *httprespcache.Cache, req *retryablehttp.Request, resp *http.Response, body []byte, seedURL string) {
finalURL := seedURL
if resp != nil && resp.Request != nil && resp.Request.URL != nil {
finalURL = resp.Request.URL.String()
}
if finalURL == seedURL {
cache.SeedHTTP(req.Request, resp, body, techFilterMaxBody)
return
}
finalReq := req.Request.Clone(req.Context())
finalReq.URL = resp.Request.URL
finalReq.Host = ""
cache.SeedHTTP(finalReq, resp, body, techFilterMaxBody)
}
// estimateTechFilteredExecutions counts template×host pairs that survive the tech filter.
func estimateTechFilteredExecutions(tpls []*templates.Template, idx *techReachabilityIndex, hostCount int) (baseline, filtered int) {
baseline = estimateTemplateRequests(tpls) * hostCount
if idx == nil || len(idx.byInput) == 0 {
return baseline, baseline
}
filtered = 0
for _, t := range tpls {
reqs := t.TotalRequests
if reqs <= 0 {
reqs = 1
}
if len(t.Workflows) > 0 {
continue
}
tags := idx.tagsFor(t)
for _, profile := range idx.byInput {
if techfilter.AllowTags(profile, tags) {
filtered += reqs
}
}
missing := hostCount - len(idx.byInput)
if missing > 0 {
filtered += reqs * missing
}
}
return baseline, filtered
}
// composeTemplateFilters ANDs optional filters; nil filters are ignored.
func composeTemplateFilters(filters ...func(*templates.Template, *contextargs.MetaInput) bool) func(*templates.Template, *contextargs.MetaInput) bool {
var active []func(*templates.Template, *contextargs.MetaInput) bool
for _, f := range filters {
if f != nil {
active = append(active, f)
}
}
if len(active) == 0 {
return nil
}
return func(t *templates.Template, mi *contextargs.MetaInput) bool {
for _, f := range active {
if !f(t, mi) {
return false
}
}
return true
}
}
+33
View File
@@ -0,0 +1,33 @@
package runner
import (
"sync"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/core/techfilter"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
)
// tagsFor is filled lazily from the template execution goroutines. Run it under
// -race to catch the concurrent map access that used to crash whole scans.
func TestTechReachabilityIndexConcurrentTagsFor(t *testing.T) {
idx := &techReachabilityIndex{
byInput: map[string]techfilter.HostProfile{},
boundTags: map[string][]string{},
}
var wg sync.WaitGroup
for i := 0; i < 64; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < 64; j++ {
tpl := &templates.Template{ID: string(rune('a' + (i+j)%26))}
_ = idx.tagsFor(tpl)
_ = idx.Allow(tpl, &contextargs.MetaInput{Input: "http://x/"})
}
}(i)
}
wg.Wait()
}
+70
View File
@@ -0,0 +1,70 @@
package runner
import (
"net/http"
"net/url"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/retryablehttp-go"
)
func mustReq(t *testing.T, raw string) *retryablehttp.Request {
t.Helper()
req, err := retryablehttp.NewRequest(http.MethodGet, raw, nil)
if err != nil {
t.Fatalf("request: %v", err)
}
return req
}
func respFor(raw string, status int) *http.Response {
u, _ := url.Parse(raw)
return &http.Response{
StatusCode: status,
Status: http.StatusText(status),
Header: http.Header{"Server": []string{"probe"}},
Request: &http.Request{Method: http.MethodGet, URL: u},
}
}
// The probe follows redirects. The body it comes back with belongs to the
// redirect target, so it must never be stored under the requested URL: a
// template asking for "/" must still see the redirect, not the target page.
func TestSeedFingerprintResponseAfterRedirect(t *testing.T) {
const seedURL = "http://example.test/"
const finalURL = "http://example.test/wp-admin/install.php"
c := httprespcache.New()
req := mustReq(t, seedURL)
req.Header.Set("User-Agent", "probe-ua")
seedFingerprintResponse(c, req, respFor(finalURL, 200), []byte("install page"), seedURL)
lookup := req.Request.Clone(req.Context())
if got := c.Get(httprespcache.KeyFromRequest(lookup), "template", lookup); got != nil {
t.Fatal("the redirect target must not be cached under the requested URL")
}
finalLookup := req.Request.Clone(req.Context())
finalLookup.URL, _ = url.Parse(finalURL)
finalLookup.Host = ""
if got := c.Get(httprespcache.KeyFromRequest(finalLookup), "template", finalLookup); got == nil {
t.Fatal("the response must be cached under the URL it belongs to")
}
}
// Without a redirect the response does describe the requested URL, so seeding
// it there is what saves the round trip.
func TestSeedFingerprintResponseWithoutRedirect(t *testing.T) {
const seedURL = "http://example.test/"
c := httprespcache.New()
req := mustReq(t, seedURL)
req.Header.Set("User-Agent", "probe-ua")
seedFingerprintResponse(c, req, respFor(seedURL, 200), []byte("home"), seedURL)
lookup := req.Request.Clone(req.Context())
if got := c.Get(httprespcache.KeyFromRequest(lookup), "template", lookup); got == nil {
t.Fatal("a non-redirected response must be cached under the requested URL")
}
}
+21
View File
@@ -0,0 +1,21 @@
package runner
import (
"testing"
"github.com/projectdiscovery/hmap/store/hybrid"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/stretchr/testify/require"
)
func TestFingerprintURLForInput(t *testing.T) {
require.Equal(t, "http://a/", fingerprintURLForInput("http://a/", nil))
require.Equal(t, "", fingerprintURLForInput("example.com", nil))
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
require.NoError(t, err)
t.Cleanup(func() { _ = hm.Close() })
require.NoError(t, hm.Set("example.com", []byte("https://example.com")))
helper := &input.Helper{InputsHTTP: hm, InputsHTTPProbed: true}
require.Equal(t, "https://example.com", fingerprintURLForInput("example.com", helper))
}
+6 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
@@ -51,7 +52,7 @@ type NucleiExecutorOptions struct {
Catalog catalog.Catalog
IssuesClient reporting.Client
RateLimiter *ratelimit.Limiter
Interactsh *interactsh.Client
Interactsh interactsh.IClient
ProjectFile *projectfile.ProjectFile
Browser *browserEngine.Browser
FuzzStatsDB *stats.Tracker
@@ -106,6 +107,10 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
executorOpts.HostErrorsCache = cache
}
if opts.Options.HostBackoff {
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
}
executorEngine := core.New(opts.Options)
executorEngine.SetExecuterOptions(executorOpts)
@@ -5,6 +5,7 @@ package functional_test
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
@@ -280,6 +281,12 @@ func trimMatchingQuotes(value string) string {
return value
}
func TestIsRetryableFunctionalError(t *testing.T) {
if isRetryableFunctionalError(nil) || isRetryableFunctionalError(fmt.Errorf("load mismatch")) {
t.Fatal("non-exit errors should not retry")
}
}
func TestSplitFunctionalArgs(t *testing.T) {
t.Run("quoted values", func(t *testing.T) {
args := splitFunctionalArgs(`{{binary}} -tags "cve,exposure" -author "geeknik,pdteam" -severity high,critical`)
@@ -338,6 +345,29 @@ func runFunctionalCase(args []string, debug bool) error {
return fmt.Errorf("functional runners are not initialized")
}
const attempts = 2
var last error
for attempt := 1; attempt <= attempts; attempt++ {
last = compareFunctionalLoad(args, debug)
if last == nil {
return nil
}
if !isRetryableFunctionalError(last) {
return last
}
}
return last
}
func isRetryableFunctionalError(err error) bool {
if err == nil {
return false
}
var exitErr *exec.ExitError
return errors.As(err, &exitErr)
}
func compareFunctionalLoad(args []string, debug bool) error {
var (
releaseOutput string
currentOutput string
+5 -3
View File
@@ -18,9 +18,11 @@ import (
)
const (
dbName = "test"
dbRepository = "mongo"
dbTag = "8"
dbName = "test"
dbRepository = "mongo"
// 8.x refuses to start on kernels 6.19 through 7.0.13 (SERVER-125742).
// The GitHub Ubuntu image is in that range. 7.0 stays up.
dbTag = "7.0"
dbPort = "27017/tcp"
mongoDatabaseReadyTimeout = 3 * time.Minute
mongoServerSelectionDelay = time.Second
+67
View File
@@ -6,6 +6,7 @@ package integration_test
import (
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
@@ -83,6 +84,8 @@ var httpTestcases = []integrationCase{
{Path: "protocols/http/annotation-timeout.yaml", TestCase: &annotationTimeout{}},
{Path: "protocols/http/custom-attack-type.yaml", TestCase: &customAttackType{}},
{Path: "protocols/http/get-all-ips.yaml", TestCase: &scanAllIPS{}},
{Path: "protocols/http/target-line-ip.yaml", TestCase: &httpTargetLineIP{}},
{Path: "protocols/http/target-line-ip-pipeline.yaml", TestCase: &httpTargetLineIP{}},
{Path: "protocols/http/get-without-scheme.yaml", TestCase: &httpGetWithoutScheme{}},
{Path: "protocols/http/cl-body-without-header.yaml", TestCase: &httpCLBodyWithoutHeader{}},
{Path: "protocols/http/cl-body-with-header.yaml", TestCase: &httpCLBodyWithHeader{}},
@@ -1606,6 +1609,70 @@ func (h *scanAllIPS) Execute(filePath string) error {
return expectResultsCount(got, 1)
}
// target lines pin a hostname that does not resolve to the test servers
type httpTargetLineIP struct{}
// Execute executes a test case and returns an error if occurred
func (h *httpTargetLineIP) Execute(filePath string) error {
serve := func(addr, name string) (*httptest.Server, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "host=%s server=%s", r.Host, name)
}))
_ = ts.Listener.Close()
ts.Listener = listener
ts.Start()
return ts, nil
}
one, err := serve("127.0.0.1:0", "one")
if err != nil {
return err
}
defer one.Close()
_, port, err := net.SplitHostPort(one.Listener.Addr().String())
if err != nil {
return err
}
lines := []string{fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.1"}`, port)}
want := []string{"one"}
// the same host pinned to a second ip, where 127.0.0.2 is available
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
defer two.Close()
lines = append(lines, fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.2"}`, port))
want = append(want, "two")
}
targets, err := os.CreateTemp("", "target-lines-*.txt")
if err != nil {
return err
}
defer func() { _ = os.Remove(targets.Name()) }()
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
return err
}
_ = targets.Close()
got, err := testutils.RunNucleiBareArgsAndGetResults(debug, nil, "-t", filePath, "-l", targets.Name())
if err != nil {
return err
}
if err := expectResultsCount(got, len(want)); err != nil {
return err
}
joined := strings.Join(got, "\n")
for _, name := range want {
if !strings.Contains(joined, name) {
return fmt.Errorf("no result from server %q: %v", name, got)
}
}
return nil
}
// ensure that ip|host are handled without http|https scheme
type httpGetWithoutScheme struct{}
+26 -6
View File
@@ -73,17 +73,27 @@ func TestMain(m *testing.M) {
templatesDir := filepath.Join(homeDir, config.NucleiTemplatesDirName)
config.DefaultConfig.SetTemplatesDir(templatesDir)
baseEnv := []string{
"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"),
}
// cases run nuclei with -duc, so the official templates some of them read
// are installed once here rather than by whichever cases start first, which
// queued them behind each other's downloads inside their own timeouts
if err := installTemplates(binaryPath, baseEnv); err != nil {
fmt.Fprintln(os.Stderr, err)
_ = os.RemoveAll(tempDir)
os.Exit(1)
}
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.WithBaseEnv(baseEnv...),
)
testutils.SetDefaultRunner(runner)
@@ -124,6 +134,16 @@ func buildNucleiBinary(repoRoot, binaryPath string) error {
return nil
}
func installTemplates(binaryPath string, env []string) error {
cmd := exec.Command(binaryPath, "-update-templates", "-no-color")
cmd.Env = append(os.Environ(), env...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to install nuclei templates: %w\n%s", err, strings.TrimSpace(string(output)))
}
return nil
}
func isDebugMode() bool {
for _, envVar := range []string{"DEBUG", "ACTIONS_STEP_DEBUG", "ACTIONS_RUNNER_DEBUG", "RUNNER_DEBUG"} {
if envTruthy(envVar) {
@@ -5,10 +5,12 @@ package integration_test
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"net"
"os"
"strings"
"time"
"github.com/go-pg/pg/v10"
@@ -16,6 +18,7 @@ import (
"github.com/ory/dockertest/v3"
dc "github.com/ory/dockertest/v3/docker"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
osutils "github.com/projectdiscovery/utils/os"
"github.com/projectdiscovery/utils/reader"
"go.uber.org/multierr"
@@ -34,9 +37,12 @@ var jsTestcases = []integrationCase{
{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/target-line-ip.yaml", TestCase: &javascriptTargetLineIP{}},
{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/websocket-echo.yaml", TestCase: &javascriptWebSocketEcho{}},
{Path: "protocols/javascript/websocket-denied.yaml", TestCase: &javascriptWebSocketDenied{}},
{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{}},
@@ -465,6 +471,87 @@ func (j *networkMultiStep) Execute(filePath string) error {
return expectResultsCount(results, 1)
}
// target lines pin a hostname that does not resolve, so the javascript
// connection reaches a server only through the pinned ip
type javascriptTargetLineIP struct{}
func (j *javascriptTargetLineIP) Execute(filePath string) error {
serve := func(addr, banner string) (net.Listener, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
_, _ = conn.Write([]byte(banner))
_ = conn.Close()
}
}()
return listener, nil
}
one, err := serve("127.0.0.1:0", "one")
if err != nil {
return err
}
defer func() { _ = one.Close() }()
_, port, err := net.SplitHostPort(one.Addr().String())
if err != nil {
return err
}
want := map[string]string{"127.0.0.1": "one"}
// the same host pinned to a second ip, where 127.0.0.2 is available
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
defer func() { _ = two.Close() }()
want["127.0.0.2"] = "two"
}
lines := make([]string, 0, len(want))
for ip := range want {
lines = append(lines, fmt.Sprintf(`{"target": "pinned.test:%s", "ip": %q}`, port, ip))
}
targets, err := os.CreateTemp("", "target-lines-*.txt")
if err != nil {
return err
}
defer func() { _ = os.Remove(targets.Name()) }()
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
return err
}
_ = targets.Close()
if err := ensureSignedIntegrationTemplates(); err != nil {
return err
}
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), "-t", filePath, "-l", targets.Name(), "-jsonl")
if err != nil {
return err
}
got := make(map[string]string)
for _, result := range results {
var event output.ResultEvent
if !strings.HasPrefix(result, "{") || json.Unmarshal([]byte(result), &event) != nil {
continue
}
got[event.IP] = strings.Join(event.ExtractedResults, ",")
}
if len(got) != len(want) {
return fmt.Errorf("got results for %d ips, want %d: %v", len(got), len(want), got)
}
for ip, banner := range want {
if got[ip] != banner {
return fmt.Errorf("ip %s answered %q, want %q", ip, got[ip], banner)
}
}
return nil
}
func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expectedNumbers ...int) error {
if len(expectedNumbers) == 0 {
expectedNumbers = []int{1}
@@ -0,0 +1,74 @@
//go:build integration
// +build integration
package integration_test
import (
"fmt"
"net"
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
// startWebSocketEchoServer starts a WebSocket server on a random loopback port
// that accepts the "chat" subprotocol, greets, then echoes every message.
func startWebSocketEchoServer() (string, func(), error) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", nil, fmt.Errorf("websocket listen: %w", err)
}
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upgrader := ws.HTTPUpgrader{Protocol: func(protocol string) bool { return protocol == "chat" }}
conn, _, _, err := upgrader.Upgrade(r, w)
if err != nil {
return
}
defer func() { _ = conn.Close() }()
if err := wsutil.WriteServerText(conn, []byte("welcome")); err != nil {
return
}
for {
payload, op, err := wsutil.ReadClientData(conn)
if err != nil {
return
}
if err := wsutil.WriteServerMessage(conn, op, payload); err != nil {
return
}
}
})}
go func() { _ = srv.Serve(lis) }()
return lis.Addr().String(), func() { _ = srv.Close() }, nil
}
// javascriptWebSocketEcho exercises the nuclei/websocket library end to end
// against a real WebSocket server.
type javascriptWebSocketEcho struct{}
func (j *javascriptWebSocketEcho) Execute(filePath string) error {
address, stop, err := startWebSocketEchoServer()
if err != nil {
return err
}
defer stop()
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
// javascriptWebSocketDenied asserts the nuclei/websocket library refuses a host
// on the exclude list (network-policy enforcement) before connecting.
type javascriptWebSocketDenied struct{}
func (j *javascriptWebSocketDenied) 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)
}
@@ -6,6 +6,8 @@ package integration_test
import (
"fmt"
"net"
"os"
"path/filepath"
"testing"
"time"
@@ -44,6 +46,38 @@ func TestNetwork(t *testing.T) {
}
})
t.Run("PinnedTargetLine", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
if err == nil && string(data) == "PING" {
_, _ = conn.Write([]byte("PONG"))
}
})
defer server.Close()
_, port, err := net.SplitHostPort(server.URL)
if err != nil {
t.Fatal(err)
}
targets := filepath.Join(t.TempDir(), "targets.txt")
line := fmt.Sprintf(`{"target": "pinned.test:%s", "ip": "127.0.0.1"}`, port)
if err := os.WriteFile(targets, []byte(line), 0o600); err != nil {
t.Fatal(err)
}
// pinned.test does not resolve: the network request reaches the server
// only through the pinned ip
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, nil, "-t", "protocols/network/basic.yaml", "-l", targets)
if err != nil {
t.Fatalf("pinned network request failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("Hex", func(t *testing.T) {
errState := &capturedError{}
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
@@ -0,0 +1,25 @@
id: target-line-ip-pipeline
info:
name: Target Line IP Pipeline
author: pdteam
severity: info
http:
- raw:
- |
GET / HTTP/1.1
Host: {{Hostname}}
pipeline: true
pipeline-concurrent-connections: 1
pipeline-requests-per-connection: 1
matchers:
- type: word
words:
- "host=pinned.test:"
extractors:
- type: regex
group: 1
regex:
- "server=([a-z]+)"
@@ -0,0 +1,20 @@
id: target-line-ip
info:
name: Target Line IP
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
matchers:
- type: word
words:
- "host=pinned.test:"
extractors:
- type: regex
group: 1
regex:
- "server=([a-z]+)"
@@ -0,0 +1,25 @@
id: javascript-target-line-ip
info:
name: JavaScript Target Line IP
author: pdteam
severity: info
javascript:
- code: |
var m = require("nuclei/net");
var conn = m.Open("tcp", address);
conn.SetTimeout(timeout);
conn.RecvString(3)
args:
address: "{{Host}}:{{Port}}"
Host: "{{Host}}"
Port: 5431
timeout: 3
extractors:
- type: regex
part: response
regex:
- "[a-z]+"
@@ -0,0 +1,34 @@
id: websocket-denied
info:
name: WebSocket Network Policy Denial
author: pdteam
severity: info
description: |
Asserts the nuclei/websocket javascript library refuses a host on the
exclude list before connecting.
javascript:
- code: |
const websocket = require('nuclei/websocket');
let out = "";
try {
const client = new websocket.Client('ws://203.0.113.10/socket');
client.Connect();
} 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,33 @@
id: websocket-echo
info:
name: WebSocket Echo
author: pdteam
severity: info
description: |
Exercises the nuclei/websocket javascript library end to end: dial through
the network policy, negotiate a subprotocol, read the server greeting and
round-trip a text message.
javascript:
- code: |
const websocket = require('nuclei/websocket');
const opts = new websocket.Options();
opts.Protocols = ['chat'];
const client = new websocket.Client('ws://' + address + '/echo', opts);
const greeting = client.Receive();
client.Send('nuclei-websocket');
const echo = client.Receive();
client.Close();
greeting + ' ' + client.Protocol + ' ' + echo;
args:
address: "{{Host}}:{{Port}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- "success == true"
- type: word
part: response
words:
- "welcome chat nuclei-websocket"
@@ -24,6 +24,6 @@ http:
matchers:
- type: dsl
dsl:
- contains(body,'home') # check for http string
- location == '/home' # the docs host redirects here
- blogid == 'cname' # check for cname (extracted information from dns response)
condition: and
@@ -24,7 +24,7 @@ http:
matchers:
- type: dsl
dsl:
- contains(http_body,'home') # check for http string
- http_location == '/home' # the docs host redirects here
- cname_filtered == 'cname' # check for cname (extracted information from dns response)
- ssl_subject_cn == 'docs.projectdiscovery.io'
condition: and
@@ -20,7 +20,7 @@ http:
matchers:
- type: dsl
dsl:
- contains(http_body,'home') # check for http string
- http_location == '/home' # the docs host redirects here
- trim_suffix(dns_cname,'.vercel-dns.com') == 'cname' # check for cname (extracted information from dns response)
- ssl_subject_cn == 'docs.projectdiscovery.io'
condition: and
+7
View File
@@ -261,6 +261,9 @@ func (r *Runner) ArgsResults(debug bool, args ...string) ([]string, error) {
output, err := r.runCommand(false, "", func(cmd *exec.Cmd) {
cmd.Args = append(cmd.Args[:1], append(append([]string{}, args...), r.ExtraArgs...)...)
cmd.Env = r.buildEnv(nil)
if r.DisableAutoUpdate {
cmd.Args = append(cmd.Args, "-duc")
}
if debug {
cmd.Args = append(cmd.Args, "-debug")
cmd.Stderr = os.Stderr
@@ -324,6 +327,10 @@ func (r *Runner) LoadedTemplates(binaryPath string, debug bool, args []string) (
}
if err != nil {
out := strings.TrimSpace(string(data))
if out != "" {
return "", fmt.Errorf("%w: %s", err, out)
}
return "", err
}
+40 -37
View File
@@ -40,43 +40,46 @@ func Cleanup(options *types.Options) {
// DefaultOptions is the default options structure for nuclei during mocking.
var DefaultOptions = &types.Options{
Debug: false,
DebugRequests: false,
DebugResponse: false,
Silent: false,
Verbose: false,
NoColor: true,
UpdateTemplates: false,
JSONL: false,
OmitRawRequests: false,
EnableProgressBar: false,
TemplateList: false,
Stdin: false,
StopAtFirstMatch: false,
NoMeta: false,
Project: false,
MetricsPort: 0,
BulkSize: 25,
TemplateThreads: 10,
Timeout: 5,
Retries: 1,
RateLimit: 150,
RateLimitDuration: time.Second,
ProbeConcurrency: 50,
ProjectPath: "",
Severities: severity.Severities{},
Targets: []string{},
TargetsFilePath: "",
Output: "",
Proxy: []string{},
TraceLogFile: "",
Templates: []string{},
ExcludedTemplates: []string{},
CustomHeaders: []string{},
InteractshURL: "https://oast.fun",
InteractionsCacheSize: 5000,
InteractionsEviction: 60,
InteractionsCoolDownPeriod: 5,
Debug: false,
DebugRequests: false,
DebugResponse: false,
Silent: false,
Verbose: false,
NoColor: true,
UpdateTemplates: false,
JSONL: false,
OmitRawRequests: false,
EnableProgressBar: false,
TemplateList: false,
Stdin: false,
StopAtFirstMatch: false,
NoMeta: false,
Project: false,
MetricsPort: 0,
BulkSize: 25,
TemplateThreads: 10,
Timeout: 5,
Retries: 1,
RateLimit: 150,
RateLimitDuration: time.Second,
ProbeConcurrency: 50,
ProjectPath: "",
Severities: severity.Severities{},
Targets: []string{},
TargetsFilePath: "",
Output: "",
Proxy: []string{},
TraceLogFile: "",
Templates: []string{},
ExcludedTemplates: []string{},
CustomHeaders: []string{},
InteractshURL: "https://oast.fun",
InteractionsCacheSize: 5000,
InteractionsEviction: 60,
// Closing an interactsh client sleeps for the cooldown, waiting on callbacks
// that a unit test never receives, so every test generating an oast url paid
// five seconds for nothing.
InteractionsCoolDownPeriod: 0,
InteractionsPollDuration: 5,
GitHubTemplateRepo: []string{},
GitHubToken: "",
+71
View File
@@ -0,0 +1,71 @@
package nuclei_test
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/stretchr/testify/require"
)
func TestThreadSafeCachedFlowPreservesPrecondition(t *testing.T) {
for _, positive := range []bool{false, true} {
name := "negative"
if positive {
name = "positive"
}
t.Run(name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "flow.yaml")
require.NoError(t, os.WriteFile(path, []byte(`id: cached-flow
info:
name: Cached flow
author: pdteam
severity: info
flow: http(1) && http(2)
http:
- method: GET
path: ["{{BaseURL}}/precondition"]
matchers:
- type: status
status: [200]
internal: true
- method: GET
path: ["{{BaseURL}}/check"]
matchers:
- type: status
status: [404]
`), 0o600))
var checks, findings atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/check" {
checks.Add(1)
}
if positive && r.URL.Path == "/precondition" {
w.WriteHeader(200)
return
}
http.NotFound(w, r)
}))
defer server.Close()
engine, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(), nuclei.DisableUpdateCheck())
require.NoError(t, err)
defer engine.Close()
engine.GlobalResultCallback(func(*output.ResultEvent) { findings.Add(1) })
for call := int64(1); call <= 3; call++ {
require.NoError(t, engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL}, nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{path}})))
expected := int64(0)
if positive {
expected = call
}
require.Equal(t, expected, checks.Load(), "check requests after call %d", call)
require.Equal(t, expected, findings.Load(), "findings after call %d", call)
}
})
}
}
+41 -1
View File
@@ -173,6 +173,16 @@ func WithResponseReadSize(responseReadSize int) NucleiSDKOptions {
}
}
// EnableResponseCache reuses identical safe GET/HEAD responses within one scan.
// It is off by default: a template that depends on state or timing could see a
// different result.
func EnableResponseCache() NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.ResponseCache = true
return nil
}
}
// WithGlobalRateLimit sets global rate (i.e all hosts combined) limit options
// Deprecated: will be removed in favour of WithGlobalRateLimitCtx in next release
func WithGlobalRateLimit(maxTokens int, duration time.Duration) NucleiSDKOptions {
@@ -367,7 +377,9 @@ func UseOutputWriter(writer OutputWriter) NucleiSDKOptions {
}
}
// StatsWriter
// StatsWriter receives scan statistics. A writer that also implements
// progress.TaskTracker is told how many template executions the scan runs
// and how many are done.
type StatsWriter progress.Progress
// UseStatsWriter allows setting a custom stats writer
@@ -754,3 +766,31 @@ func WithTemporaryDirectory(parentDir string) NucleiSDKOptions {
return nil
}
}
// WithPerHostRateLimit applies the rate limit per host instead of globally.
func WithPerHostRateLimit(enabled bool) NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.PerHostRateLimit = enabled
return nil
}
}
// EnableStrictProbe skips templates whose target service is unreachable: web
// templates on hosts that do not speak HTTP and network templates on closed
// ports. It is the SDK form of -strict-probe.
func EnableStrictProbe() NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.StrictProbe = true
return nil
}
}
// EnableTechFilter fingerprints HTTP targets and skips product-bound templates
// that cannot match them. It also turns on the response cache, which the
// fingerprint requests seed. It is the SDK form of -tech-filter.
func EnableTechFilter() NucleiSDKOptions {
return func(e *NucleiEngine) error {
e.opts.TechFilter = true
return nil
}
}
+61 -1
View File
@@ -8,9 +8,15 @@ import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
// Update checks are disabled explicitly so this pins corrupt-file rejection on
// its own. The ignore file is read after init's update block, which may replace
// a corrupt file via UpdateIgnoreFile — with update checks left on, this test
// would assert rejection or repair depending on whether the process could reach
// the network.
func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, config.NucleiIgnoreFileName)
@@ -21,8 +27,62 @@ func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
cfg.SetTemplatesDir(root)
engine, err := NewNucleiEngineCtx(context.Background())
engine, err := NewNucleiEngineCtx(context.Background(), DisableUpdateCheck())
require.Nil(t, engine)
require.ErrorContains(t, err, "error parsing")
require.ErrorContains(t, err, strconv.Quote(path))
}
// TestIgnoreFileSurvivesAuthTemplateStore pins two init orderings.
//
// The ignore file is read after the update block, so a file this same call
// creates is the one that becomes the deny-list. GetAuthTmplStore also runs
// during init whenever SecretsFile is set. It must scope a copy of the
// options: the caller's Templates and ExcludeTags stay put, and the ignore
// file is still applied on top.
//
// A static-only secrets file is enough to build that store.
func TestIgnoreFileSurvivesAuthTemplateStore(t *testing.T) {
root := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(root, config.NucleiIgnoreFileName),
[]byte("tags:\n - dos\nfiles:\n - dns/soa-detect.yaml\n"),
0o600,
))
cfg := config.DefaultConfig
oldRoot := cfg.TemplatesDirectory
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
cfg.SetTemplatesDir(root)
secrets := filepath.Join(root, "secrets.yaml")
require.NoError(t, os.WriteFile(secrets, []byte(`static:
- type: header
domains:
- example.com
headers:
- key: x-api-key
value: not-a-real-secret
`), 0o600))
opts := types.DefaultOptions()
opts.SecretsFile = []string{secrets}
opts.Templates = []string{"http/kept.yaml"}
opts.ExcludeTags = []string{"custom"}
engine, err := NewNucleiEngineCtx(context.Background(),
WithOptions(opts),
DisableUpdateCheck(),
)
require.NoError(t, err)
t.Cleanup(engine.Close)
require.Equal(t, []string{"http/kept.yaml"}, []string(engine.Options().Templates),
"the auth store must not replace the caller's template list")
require.Contains(t, []string(engine.Options().ExcludeTags), "custom",
"the caller's own excluded tags must survive auth-store setup")
require.Contains(t, []string(engine.Options().ExcludeTags), "dos",
"the .nuclei-ignore deny-list must be applied after auth-store setup")
require.Contains(t, []string(engine.Options().ExcludedTemplates), "dns/soa-detect.yaml",
"the files section must be applied too")
}
+9 -5
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/logrusorgru/aurora/v4"
"github.com/projectdiscovery/nuclei/v3/internal/runner"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/core"
@@ -12,6 +13,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/utils/errkit"
@@ -44,11 +46,7 @@ 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,
DoNotCache: opts.DoNotCacheTemplates,
}
if base.interactshClient != nil {
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
@@ -64,6 +62,11 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
opts.RateLimitDuration = time.Second
}
u.executerOpts.RateLimiter = utils.GetRateLimiter(ctx, opts.RateLimit, opts.RateLimitDuration)
// Each execution gets its own response cache, created before its
// templates compile.
if opts.UsesResponseCache() && u.executerOpts.HTTPResponseCache == nil {
u.executerOpts.HTTPResponseCache = httprespcache.New()
}
return u, nil
}
@@ -244,6 +247,7 @@ func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, t
engine := core.New(tmpEngine.opts)
engine.SetExecuterOptions(unsafeOpts.executerOpts)
defer runner.AttachScanPlan(tmpEngine.opts, inputProvider, tmpEngine.opts.Logger, engine, store.Templates())()
_ = engine.ExecuteScanWithOpts(ctx, store.Templates(), inputProvider, false)
engine.WorkPool().Wait()
+88
View File
@@ -2,10 +2,14 @@ package nuclei
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
@@ -68,3 +72,87 @@ http:
require.NotNil(t, cached)
}
}
func TestThreadSafeExecuteUsesSharedCompiledCache(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "template.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-shared-cache
info:
name: Thread safe shared cache
author: pdteam
severity: info
tags: thread-safe-shared-cache
http:
- method: GET
path:
- "{{BaseURL}}"
matchers:
- type: word
words:
- "thread-safe-shared-cache"
`), 0o600))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("thread-safe-shared-cache"))
}))
t.Cleanup(server.Close)
engine, err := NewThreadSafeNucleiEngineCtx(context.Background())
require.NoError(t, err)
t.Cleanup(engine.Close)
const executions = 4
var waitGroup sync.WaitGroup
errors := make([]error, executions)
for i := range executions {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
errors[i] = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-shared-cache"}}),
WithResultCallback(func(*output.ResultEvent) {}),
)
}()
}
waitGroup.Wait()
for _, err := range errors {
require.NoError(t, err)
}
require.Equal(t, 1, engine.eng.GetParser().CompiledCount())
}
func TestThreadSafeExecuteHonorsDisableTemplateCache(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "template.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-disable-cache
info:
name: Thread safe disable cache
author: pdteam
severity: info
tags: thread-safe-disable-cache
http:
- method: GET
path:
- "{{BaseURL}}"
`), 0o600))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
t.Cleanup(server.Close)
engine, err := NewThreadSafeNucleiEngineCtx(context.Background(), DisableTemplateCache())
require.NoError(t, err)
t.Cleanup(engine.Close)
err = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-disable-cache"}}),
)
require.NoError(t, err)
require.Equal(t, 0, engine.eng.GetParser().CompiledCount())
}
+120
View File
@@ -0,0 +1,120 @@
package nuclei_test
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/stretchr/testify/require"
)
func TestScanPlanOptions(t *testing.T) {
ne, err := nuclei.NewNucleiEngineCtx(context.Background(),
nuclei.DisableUpdateCheck(),
nuclei.EnableStrictProbe(),
nuclei.EnableTechFilter(),
nuclei.EnableResponseCache(),
)
require.NoError(t, err)
defer ne.Close()
opts := ne.Options()
require.True(t, opts.StrictProbe)
require.True(t, opts.TechFilter)
require.True(t, opts.ResponseCache)
}
// sdkCacheTemplates writes two templates that both GET /same. The second also
// GETs /other, so request clustering cannot merge them into one request.
func sdkCacheTemplates(t *testing.T) []string {
t.Helper()
dir := t.TempDir()
a := `id: sdk-cache-a
info:
name: SDK Cache A
author: test
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/same"
matchers:
- type: word
words:
- "cached-body"
`
b := `id: sdk-cache-b
info:
name: SDK Cache B
author: test
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/same"
- "{{BaseURL}}/other"
matchers:
- type: word
words:
- "cached-body"
`
pa, pb := filepath.Join(dir, "a.yaml"), filepath.Join(dir, "b.yaml")
require.NoError(t, os.WriteFile(pa, []byte(a), 0o600))
require.NoError(t, os.WriteFile(pb, []byte(b), 0o600))
return []string{pa, pb}
}
// With the SDK options for the scan plan set, both templates still run and
// match, and the response cache sends /same once. Without them it is sent
// twice.
func TestSDKScanPlanAndResponseCache(t *testing.T) {
var sameHits atomic.Int32
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/same" {
sameHits.Add(1)
time.Sleep(200 * time.Millisecond)
}
_, _ = w.Write([]byte("cached-body"))
}))
defer ts.Close()
tpls := sdkCacheTemplates(t)
run := func(extra ...nuclei.NucleiSDKOptions) map[string]bool {
sameHits.Store(0)
opts := append([]nuclei.NucleiSDKOptions{
nuclei.DisableUpdateCheck(),
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: tpls}),
}, extra...)
ne, err := nuclei.NewNucleiEngineCtx(context.Background(), opts...)
require.NoError(t, err)
defer ne.Close()
ne.Options().RestrictLocalNetworkAccess = false
ne.Options().Retries = 0
ne.LoadTargets([]string{ts.URL}, false)
var mu sync.Mutex
matched := map[string]bool{}
require.NoError(t, ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
matched[event.TemplateID] = true
}))
return matched
}
matched := run(nuclei.EnableResponseCache(), nuclei.EnableStrictProbe(), nuclei.EnableTechFilter())
require.Equal(t, map[string]bool{"sdk-cache-a": true, "sdk-cache-b": true}, matched)
require.Equal(t, int32(1), sameHits.Load(), "the response cache must send /same once")
matched = run()
require.Equal(t, map[string]bool{"sdk-cache-a": true, "sdk-cache-b": true}, matched)
require.Equal(t, int32(2), sameHits.Load(), "without the cache /same is sent by each template")
}
+21 -4
View File
@@ -9,6 +9,7 @@ import (
"sync"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/internal/runner"
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
metadataindex "github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
@@ -70,7 +71,7 @@ type NucleiEngine struct {
// unexported core fields
ctx context.Context
interactshClient *interactsh.Client
interactshClient interactsh.IClient
catalog catalog.Catalog
rateLimiter *ratelimit.Limiter
store *loader.Store
@@ -151,6 +152,9 @@ func (e *NucleiEngine) GetTemplates() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
if e.store == nil {
return nil
}
return e.store.Templates()
}
@@ -159,6 +163,9 @@ func (e *NucleiEngine) GetWorkflows() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
if e.store == nil {
return nil
}
return e.store.Workflows()
}
@@ -251,7 +258,7 @@ func (e *NucleiEngine) closeInternal() {
if e.hostErrCache != nil {
e.hostErrCache.Close()
}
if e.executerOpts.RateLimiter != nil {
if e.executerOpts != nil && e.executerOpts.RateLimiter != nil {
e.executerOpts.RateLimiter.Stop()
}
if e.rateLimiter != nil {
@@ -297,9 +304,11 @@ func (e *NucleiEngine) Close() {
// enable matcher-status option if you expect this callback to be called for all results regardless if it matched or not
func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...func(event *output.ResultEvent)) error {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
if err := e.LoadAllTemplates(); err != nil {
return err
}
}
if len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0 {
if e.store == nil || (len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0) {
return ErrNoTemplatesAvailable
}
if e.inputProvider.Count() == 0 {
@@ -323,6 +332,10 @@ func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...f
wg.Add(1)
go func() {
defer wg.Done()
// A response cached by an earlier scan of this engine is never served
// to this one.
e.executerOpts.HTTPResponseCache.Reset()
defer runner.AttachScanPlan(e.opts, e.inputProvider, e.opts.Logger, e.engine, templatesAndWorkflows)()
_ = e.engine.ExecuteScanWithOpts(ctx, templatesAndWorkflows, e.inputProvider, false)
}()
@@ -378,6 +391,10 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
}
}
if err := e.init(ctx); err != nil {
// init may already own interactsh, a browser, a temp dir, and
// reporting. Close is safe on a partial engine: executerOpts is
// checked, and protocolinit.Close is a no-op before Init.
e.Close()
return nil, err
}
return e, nil
+41 -8
View File
@@ -33,6 +33,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httprespcache"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
nucleiUtils "github.com/projectdiscovery/nuclei/v3/pkg/utils"
@@ -44,6 +45,9 @@ import (
func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
mockoutput := testutils.NewMockOutputWriter(e.opts.OmitTemplate)
mockoutput.WriteCallback = func(event *output.ResultEvent) {
if (event.Error != "" || !event.MatcherStatus) && !e.opts.MatcherStatus {
return
}
if len(e.resultCallbacks) > 0 {
for _, callback := range e.resultCallbacks {
if callback != nil {
@@ -184,10 +188,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
return err
}
if err := e.loadIgnoreFile(); err != nil {
return err
}
if e.opts.Parser != nil {
if op, ok := e.opts.Parser.(*templates.Parser); ok {
e.parser = op
@@ -211,7 +211,7 @@ func (e *NucleiEngine) init(ctx context.Context) error {
_ = protocolinit.Init(e.opts)
}
if e.opts.ProxyInternal && e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "" {
if e.opts.ProxyInternal && (e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "") {
httpclient, err := httpclientpool.Get(e.opts, &httpclientpool.Configuration{}, "")
if err != nil {
return err
@@ -354,6 +354,12 @@ func (e *NucleiEngine) init(ctx context.Context) error {
e.executerOpts.Options.Parser = op
}
// Templates compile against e.executerOpts, so the response cache must
// exist before they load. Each scan resets it.
if e.opts.UsesResponseCache() && e.executerOpts.HTTPResponseCache == nil {
e.executerOpts.HTTPResponseCache = httprespcache.New()
}
e.engine = core.New(e.opts)
e.engine.SetExecuterOptions(e.executerOpts)
@@ -382,6 +388,26 @@ func (e *NucleiEngine) init(ctx context.Context) error {
}
}
// Read .nuclei-ignore LAST, once init has finished managing that file and
// the options it writes into.
//
// It must come after the update block above: init creates or replaces the
// ignore file there (UpdateIgnoreFile), so reading earlier reads a file this
// same call is about to write. On a host without a pre-existing ignore file
// — a fresh install, or any container whose filesystem was reset — the read
// found nothing, warned, and left the engine with an empty deny-list for its
// whole lifetime, because ExcludeTags is never re-read after init. Reading
// afterwards also makes the corrupt-file check recoverable rather than fatal:
// a malformed local file that IgnoreFileNeedsUpdate replaces no longer fails
// the scan.
//
// GetAuthTmplStore scopes a copy of the options, so it no longer clears
// the engine filters. This read is still what installs the deny-list, and
// it has to follow the update block above.
if err := e.loadIgnoreFile(); err != nil {
return err
}
return nil
}
@@ -389,7 +415,10 @@ type syncOnce struct {
sync.Once
}
var updateCheckInstance = &syncOnce{}
var (
updateCheckInstance = &syncOnce{}
templateUpdateInstance = &syncOnce{}
)
// processUpdateCheckResults processes update check results
func (e *NucleiEngine) processUpdateCheckResults() error {
@@ -398,8 +427,12 @@ func (e *NucleiEngine) processUpdateCheckResults() error {
if e.onUpdateAvailableCallback != nil {
e.onUpdateAvailableCallback(config.DefaultConfig.LatestNucleiTemplatesVersion)
}
tm := installer.TemplateManager{}
err = tm.UpdateIfOutdated()
})
if !e.disableTemplatesAutoUpgrade {
templateUpdateInstance.Do(func() {
tm := installer.TemplateManager{}
err = tm.UpdateIfOutdated()
})
}
return err
}
+172
View File
@@ -2,13 +2,20 @@ package nuclei_test
import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
@@ -155,3 +162,168 @@ func TestIgnoreFileFilesSectionIsApplied(t *testing.T) {
require.Contains(t, ne.Options().ExcludedTemplates, "http/fuzzing/wordpress-themes-detect.yaml",
"files section should reach ExcludedTemplates")
}
func TestSDKCustomPortTemplateDoesNotPoisonBaseHost(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprint(w, "server-ok")
}))
defer ts.Close()
// Allocate and immediately close a TCP listener to obtain a guaranteed closed local port.
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
closedPort := ln.Addr().(*net.TCPAddr).Port
require.NoError(t, ln.Close())
tplDir := t.TempDir()
closedPortTpl := fmt.Sprintf(`id: closed-port-parallel-probe
info:
name: Closed Port Parallel Probe
author: test
severity: info
http:
- method: GET
path:
- "http://{{Hostname}}:%d/probe?p={{val}}"
threads: 2
payloads:
val:
- "1"
- "2"
matchers:
- type: word
words:
- "never-matches"
`, closedPort)
healthyTpl := `id: healthy-base-target-match
info:
name: Healthy Base Target Match
author: test
severity: high
http:
- method: GET
path:
- "{{BaseURL}}/"
matchers:
- type: word
words:
- "server-ok"
`
closedPortPath := filepath.Join(tplDir, "01-closed-port.yaml")
healthyPath := filepath.Join(tplDir, "02-healthy.yaml")
require.NoError(t, os.WriteFile(closedPortPath, []byte(closedPortTpl), 0o600))
require.NoError(t, os.WriteFile(healthyPath, []byte(healthyTpl), 0o600))
for run := 1; run <= 3; run++ {
ne, err := nuclei.NewNucleiEngineCtx(
context.Background(),
nuclei.DisableUpdateCheck(),
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
Templates: []string{closedPortPath, healthyPath},
}),
nuclei.WithConcurrency(nuclei.Concurrency{
TemplateConcurrency: 1,
HostConcurrency: 1,
HeadlessHostConcurrency: 1,
HeadlessTemplateConcurrency: 1,
JavascriptTemplateConcurrency: 1,
TemplatePayloadConcurrency: 2,
ProbeConcurrency: 1,
}),
)
require.NoError(t, err)
ne.Options().RestrictLocalNetworkAccess = false
ne.Options().Timeout = 2
ne.Options().Retries = 0
ne.LoadTargets([]string{ts.URL}, false)
var (
mu sync.Mutex
matched []string
violations []*output.ResultEvent
)
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
if !event.MatcherStatus || event.Error != "" {
violations = append(violations, event)
}
matched = append(matched, event.TemplateID)
})
ne.Close()
require.NoError(t, err, "run %d failed", run)
require.Empty(t, violations, "ExecuteCallbackWithCtx must not emit non-matching or skip events when MatcherStatus is false")
require.Equal(t, []string{"healthy-base-target-match"}, matched, "run %d should consistently match healthy base target after custom-port failure", run)
}
}
func TestSDKSkipEventsRespectMatcherStatus(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
closedAddr := ln.Addr().String()
require.NoError(t, ln.Close())
tplDir := t.TempDir()
for i := 1; i <= 3; i++ {
tpl := fmt.Sprintf(`id: unreachable-check-%d
info:
name: Unreachable Check %d
author: test
severity: info
http:
- method: GET
path:
- "{{BaseURL}}/path-%d"
matchers:
- type: word
words:
- "ok"
`, i, i, i)
require.NoError(t, os.WriteFile(filepath.Join(tplDir, fmt.Sprintf("0%d-tpl.yaml", i)), []byte(tpl), 0o600))
}
runScan := func(matcherStatus bool) []*output.ResultEvent {
ne, err := nuclei.NewNucleiEngineCtx(
context.Background(),
nuclei.DisableUpdateCheck(),
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
Templates: []string{tplDir},
}),
nuclei.WithConcurrency(nuclei.Concurrency{
TemplateConcurrency: 1,
HostConcurrency: 1,
HeadlessHostConcurrency: 1,
HeadlessTemplateConcurrency: 1,
JavascriptTemplateConcurrency: 1,
TemplatePayloadConcurrency: 1,
ProbeConcurrency: 1,
}),
)
require.NoError(t, err)
ne.Options().RestrictLocalNetworkAccess = false
ne.Options().Timeout = 1
ne.Options().Retries = 0
ne.Options().MatcherStatus = matcherStatus
ne.LoadTargets([]string{"http://" + closedAddr}, false)
var (
mu sync.Mutex
events []*output.ResultEvent
)
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
mu.Lock()
defer mu.Unlock()
events = append(events, event)
})
ne.Close()
require.NoError(t, err)
return events
}
require.Empty(t, runScan(false), "skip/error events must not be emitted when MatcherStatus is false")
require.NotEmpty(t, runScan(true), "skip/error events should be emitted when MatcherStatus is true")
}
+3
View File
@@ -18,6 +18,9 @@ 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"),
// go 1.27 moved the bundled http2 transport out of net/http, which renamed
// the same idle connection goroutine
goleak.IgnoreAnyFunction("net/http/internal/http2.(*ClientConn).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
+60 -2
View File
@@ -386,6 +386,21 @@
"type": "boolean",
"title": "use case insensitive extract",
"description": "use case insensitive extract"
},
"prompt": {
"type": "string",
"title": "llm extraction prompt",
"description": "Natural language instruction describing what to extract; supports {{...}} interpolation of variables and target values"
},
"schema": {
"$ref": "#/$defs/map[string]string",
"title": "llm extraction schema",
"description": "Fields to extract mapped to their type"
},
"max-input-tokens": {
"type": "integer",
"title": "max llm input tokens",
"description": "Approximate cap on response tokens sent to the model"
}
},
"additionalProperties": false,
@@ -401,7 +416,8 @@
"kval",
"xpath",
"json",
"dsl"
"dsl",
"llm"
],
"title": "type of the extractor",
"description": "Type of the extractor"
@@ -1159,6 +1175,47 @@
"title": "xpath queries to match in response",
"description": "xpath are the XPath queries that will be evaluated against the response part of nuclei matching rules"
},
"prompt": {
"type": "string",
"title": "llm prompt",
"description": "Natural language question the model answers about the response part; supports {{...}} interpolation of variables and target values"
},
"expect": {
"type": "string",
"title": "expected llm verdict",
"description": "Verdict that counts as a match; defaults to yes"
},
"options": {
"items": {
"type": "string"
},
"type": "array",
"title": "allowed llm verdicts",
"description": "Fixed set of verdicts the model may return; defaults to yes and no"
},
"min-confidence": {
"type": "number",
"title": "minimum llm confidence",
"description": "Minimum confidence between 0 and 1 for an llm verdict to match"
},
"inputs": {
"items": {
"type": "string"
},
"type": "array",
"title": "llm inputs to compare",
"description": "Responses to compare instead of a single part; each entry supports {{...}} interpolation"
},
"max-input-tokens": {
"type": "integer",
"title": "max llm input tokens",
"description": "Approximate cap on response tokens sent to the model"
},
"allow-sole": {
"type": "boolean",
"title": "allow llm as sole matcher",
"description": "Permit an llm matcher to be the only matcher on a high or critical template"
},
"encoding": {
"type": "string",
"enum": [
@@ -1198,7 +1255,8 @@
"status",
"size",
"dsl",
"xpath"
"xpath",
"llm"
],
"title": "type of the matcher",
"description": "Type of the matcher"
+44
View File
@@ -0,0 +1,44 @@
package authx
import (
"context"
"net/http"
"strings"
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
)
type cookieAuthContextKey struct{}
// IsAuthCookie reports whether an auth strategy installed the named cookie.
// HTTP cookie reuse must preserve these explicit credentials over jar values.
func IsAuthCookie(ctx context.Context, name string) bool {
names, _ := ctx.Value(cookieAuthContextKey{}).(map[string]struct{})
_, ok := names[name]
return ok
}
func markCookieAuth(req *http.Request, names []string) {
if len(names) == 0 {
return
}
existing, _ := req.Context().Value(cookieAuthContextKey{}).(map[string]struct{})
// Contexts can be shared by clones and redirects. Publish a new set rather
// than modifying the names recorded by an earlier strategy.
protected := make(map[string]struct{}, len(existing)+len(names))
// A Header strategy can replace the entire Cookie header. Keep earlier
// ownership only for cookie names still present in the request.
for _, header := range req.Header.Values("Cookie") {
for _, part := range httputil.SplitCookieHeader(header) {
name, _, _ := strings.Cut(part, "=")
name = strings.TrimSpace(name)
if _, ok := existing[name]; ok {
protected[name] = struct{}{}
}
}
}
for _, name := range names {
protected[name] = struct{}{}
}
*req = *req.WithContext(context.WithValue(req.Context(), cookieAuthContextKey{}, protected))
}
@@ -0,0 +1,42 @@
package authx
import (
"net/http"
"testing"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/stretchr/testify/require"
)
func TestCookieAuthOwnershipIsRequestLocal(t *testing.T) {
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
require.NoError(t, err)
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "session", Value: "configured"}}}).ApplyOnRR(req)
cloned := req.Clone(req.Context())
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "other", Value: "configured"}}}).ApplyOnRR(cloned)
require.True(t, IsAuthCookie(req.Context(), "session"))
require.False(t, IsAuthCookie(req.Context(), "other"))
require.True(t, IsAuthCookie(cloned.Context(), "session"))
require.True(t, IsAuthCookie(cloned.Context(), "other"))
require.False(t, IsAuthCookie(cloned.Context(), "Session"))
NewHeadersAuthStrategy(&Secret{Headers: []KV{{Key: "Cookie", Value: "replacement=configured"}}}).ApplyOnRR(cloned)
require.True(t, IsAuthCookie(cloned.Context(), "replacement"))
require.False(t, IsAuthCookie(cloned.Context(), "session"))
require.False(t, IsAuthCookie(cloned.Context(), "other"))
require.True(t, IsAuthCookie(req.Context(), "session"))
}
func TestHeaderAuthCookieOwnershipIgnoresQuotedFragments(t *testing.T) {
for _, header := range []string{`payload="a;session=probe"`, `payload="a\";session=probe"`, `payload="a;session=probe`} {
t.Run(header, func(t *testing.T) {
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
require.NoError(t, err)
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "session", Value: "configured"}}}).ApplyOnRR(req)
NewHeadersAuthStrategy(&Secret{Headers: []KV{{Key: "Cookie", Value: header}}}).ApplyOnRR(req)
require.Equal(t, header, req.Header.Get("Cookie"))
require.True(t, IsAuthCookie(req.Context(), "payload"))
require.False(t, IsAuthCookie(req.Context(), "session"))
})
}
}
+6
View File
@@ -23,13 +23,16 @@ func NewCookiesAuthStrategy(data *Secret) *CookiesAuthStrategy {
// Apply applies the cookies auth strategy to the request
func (s *CookiesAuthStrategy) Apply(req *http.Request) {
var names []string
for _, cookie := range s.Data.Cookies {
c := &http.Cookie{
Name: cookie.Key,
Value: cookie.Value,
}
req.AddCookie(c)
names = append(names, cookie.Key)
}
markCookieAuth(req, names)
}
// ApplyOnRR applies the cookies auth strategy to the retryable request
@@ -51,10 +54,13 @@ func (s *CookiesAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
req.AddCookie(cookie)
}
// Add new cookies
var names []string
for _, cookie := range s.Data.Cookies {
req.AddCookie(&http.Cookie{
Name: cookie.Key,
Value: cookie.Value,
})
names = append(names, cookie.Key)
}
markCookieAuth(req.Request, names)
}
+11 -3
View File
@@ -2,7 +2,9 @@ package authx
import (
"net/http"
"strings"
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
"github.com/projectdiscovery/retryablehttp-go"
)
@@ -26,6 +28,14 @@ func NewHeadersAuthStrategy(data *Secret) *HeadersAuthStrategy {
func (s *HeadersAuthStrategy) Apply(req *http.Request) {
for _, header := range s.Data.Headers {
req.Header[header.Key] = []string{header.Value}
if strings.EqualFold(header.Key, "Cookie") {
var names []string
for _, part := range httputil.SplitCookieHeader(header.Value) {
name, _, _ := strings.Cut(part, "=")
names = append(names, strings.TrimSpace(name))
}
markCookieAuth(req, names)
}
}
}
@@ -33,7 +43,5 @@ func (s *HeadersAuthStrategy) Apply(req *http.Request) {
// NOTE: This preserves exact header casing (e.g., barAuthToken stays as barAuthToken)
// This is useful for APIs that require case-sensitive header names
func (s *HeadersAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
for _, header := range s.Data.Headers {
req.Header[header.Key] = []string{header.Value}
}
s.Apply(req.Request)
}
+29
View File
@@ -59,6 +59,35 @@ func (c *Config) ReadTemplatesConfig() error {
return nil
}
// ReloadTemplateVersion refreshes the installed templates version from disk.
// Concurrent processes read the version once at startup, so one that waited on
// another's install or update must reload it to see the result instead of
// downloading the same release again. State recorded for a different templates
// directory is ignored.
func (c *Config) ReloadTemplateVersion() error {
statePath := c.getTemplatesConfigFilePath()
data, err := os.ReadFile(statePath)
if errors.Is(err, os.ErrNotExist) {
return nil
} 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)
}
c.m.Lock()
defer c.m.Unlock()
// compare the resolved directory, so state written through a symlink is
// visible to a process using the real path, and the other way around
if CanonicalTemplatesPath(state.TemplatesDirectory) == CanonicalTemplatesPath(c.TemplatesDirectory) {
c.TemplateVersion = state.TemplateVersion
}
return nil
}
// WriteTemplatesConfig atomically writes restart-persistent templates state.
func (c *Config) WriteTemplatesConfig() error {
state := templatesState{
+91
View File
@@ -139,3 +139,94 @@ func writeTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
t.Fatalf("write test file: %v", err)
}
}
func TestReloadTemplateVersion(t *testing.T) {
newConfig := func(t *testing.T, templatesDir string) *Config {
t.Helper()
cfg := &Config{stateDir: t.TempDir()}
cfg.setTemplatesDir(templatesDir)
return cfg
}
t.Run("adopts the version another process installed", func(t *testing.T) {
templatesDir := t.TempDir()
cfg := newConfig(t, templatesDir)
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
other.setTemplatesDir(templatesDir)
if err := other.WriteTemplatesConfig(); err != nil {
t.Fatalf("write templates state: %v", err)
}
if err := cfg.ReloadTemplateVersion(); err != nil {
t.Fatalf("reload templates version: %v", err)
}
if cfg.TemplateVersion != "v2.0.0" {
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
}
})
t.Run("ignores state of another templates directory", func(t *testing.T) {
cfg := newConfig(t, t.TempDir())
cfg.TemplateVersion = "v1.0.0"
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
other.setTemplatesDir(t.TempDir())
if err := other.WriteTemplatesConfig(); err != nil {
t.Fatalf("write templates state: %v", err)
}
if err := cfg.ReloadTemplateVersion(); err != nil {
t.Fatalf("reload templates version: %v", err)
}
if cfg.TemplateVersion != "v1.0.0" {
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
}
})
t.Run("keeps the loaded version without state", func(t *testing.T) {
cfg := newConfig(t, t.TempDir())
cfg.TemplateVersion = "v1.0.0"
if err := cfg.ReloadTemplateVersion(); err != nil {
t.Fatalf("reload templates version: %v", err)
}
if cfg.TemplateVersion != "v1.0.0" {
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
}
})
t.Run("adopts the version written through the other name of a symlink", func(t *testing.T) {
templatesDir := t.TempDir()
link := filepath.Join(t.TempDir(), "templates-link")
if err := os.Symlink(templatesDir, link); err != nil {
t.Skipf("symlinks are unavailable: %v", err)
}
cfg := newConfig(t, link)
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
other.setTemplatesDir(templatesDir)
if err := other.WriteTemplatesConfig(); err != nil {
t.Fatalf("write templates state: %v", err)
}
if err := cfg.ReloadTemplateVersion(); err != nil {
t.Fatalf("reload templates version: %v", err)
}
if cfg.TemplateVersion != "v2.0.0" {
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
}
cfg.TemplateVersion = "v1.0.0"
cfg.setTemplatesDir(templatesDir)
writer := &Config{stateDir: cfg.stateDir, TemplateVersion: "v3.0.0"}
writer.setTemplatesDir(link)
if err := writer.WriteTemplatesConfig(); err != nil {
t.Fatalf("write templates state through the link: %v", err)
}
if err := cfg.ReloadTemplateVersion(); err != nil {
t.Fatalf("reload templates version: %v", err)
}
if cfg.TemplateVersion != "v3.0.0" {
t.Fatalf("template version = %q, want v3.0.0", cfg.TemplateVersion)
}
})
}
+9 -1
View File
@@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
@@ -32,6 +33,13 @@ func GetKnownConfigFiles() []string {
return knownConfigFiles
}
// IsKnownConfigFile returns true if the base name of fpath is exactly one of
// the known config files. Names that only contain one of them, such as
// "cves.json.yaml", are not considered config files.
func IsKnownConfigFile(fpath string) bool {
return slices.Contains(knownConfigFiles, filepath.Base(filepath.FromSlash(fpath)))
}
// GetKnownMiscDirectories returns known misc directories with trailing slashes.
//
// The trailing slash ensures that directory matching is explicit and avoids
@@ -78,7 +86,7 @@ func IsTemplateWithRoot(fpath, rootDir string) bool {
fname := filepath.Base(fpath)
fext := strings.ToLower(filepath.Ext(fpath))
if stringsutil.ContainsAny(fname, GetKnownConfigFiles()...) {
if IsKnownConfigFile(fname) {
return false
}
+45
View File
@@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/require"
)
// toAbs converts a slash separated absolute path into an OS specific absolute
// path, prefixing it with the system volume on Windows.
func toAbs(p string) string {
if osutils.IsWindows() {
// Infer the drive letter from the Windows system path
@@ -28,6 +30,8 @@ func toAbs(p string) string {
return filepath.FromSlash(p)
}
// TestIsTemplate checks template detection for supported extensions, excluded
// directories and known config files.
func TestIsTemplate(t *testing.T) {
tests := []struct {
name string
@@ -89,6 +93,24 @@ func TestIsTemplate(t *testing.T) {
rootDir: "",
want: false,
},
{
name: "excluded config file in subdirectory",
fpath: "nuclei-templates/TEMPLATES-STATS.json",
rootDir: "",
want: false,
},
{
name: "template name containing config file name",
fpath: "http/cves.json.yaml",
rootDir: "",
want: true,
},
{
name: "json template name containing config file name",
fpath: "http/contributors.json.json",
rootDir: "",
want: true,
},
}
for _, tt := range tests {
@@ -99,6 +121,29 @@ func TestIsTemplate(t *testing.T) {
}
}
// TestIsKnownConfigFile ensures only exact config file base names match, and
// names that merely contain one (e.g. cves.json.yaml) do not.
func TestIsKnownConfigFile(t *testing.T) {
for _, fpath := range []string{
"cves.json",
"contributors.json",
"TEMPLATES-STATS.json",
toAbs("/home/user/nuclei-templates/cves.json"),
"https://example.com/nuclei-templates/cves.json",
} {
require.True(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
}
for _, fpath := range []string{
"cves.json.yaml",
"http/cves.json.yaml",
"my-contributors.json",
toAbs("/home/user/cves.json/test.yaml"),
} {
require.False(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
}
}
func TestGetNucleiTemplatesIndexIncludesCustomDirSiblingPrefix(t *testing.T) {
tmpDir := t.TempDir()
cfgDir := t.TempDir()
+62
View File
@@ -0,0 +1,62 @@
package config
import (
"os"
"path/filepath"
"strings"
)
// CanonicalTemplatesPath is the identity of a templates directory. A symlink
// and its target are the same directory, including while the target does not
// exist yet, so a lock and a reloaded version follow either name.
func CanonicalTemplatesPath(path string) string {
if strings.TrimSpace(path) == "" {
return ""
}
return resolveTemplatesPath(path, map[string]struct{}{})
}
func resolveTemplatesPath(path string, seen map[string]struct{}) string {
abs, err := filepath.Abs(path)
if err != nil {
abs = filepath.Clean(path)
}
if _, ok := seen[abs]; ok {
return abs
}
seen[abs] = struct{}{}
dir := abs
var missing []string
for {
info, err := os.Lstat(dir)
if err == nil {
if info.Mode()&os.ModeSymlink != 0 {
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
return filepath.Join(append([]string{resolved}, missing...)...)
}
target, readErr := os.Readlink(dir)
if readErr != nil {
return filepath.Join(append([]string{dir}, missing...)...)
}
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(dir), target)
}
resolvedTarget := resolveTemplatesPath(target, seen)
return filepath.Join(append([]string{resolvedTarget}, missing...)...)
}
resolved := dir
if r, err := filepath.EvalSymlinks(dir); err == nil {
resolved = r
}
return filepath.Join(append([]string{resolved}, missing...)...)
}
parent := filepath.Dir(dir)
if parent == dir {
return filepath.Join(append([]string{dir}, missing...)...)
}
missing = append([]string{filepath.Base(dir)}, missing...)
dir = parent
}
}
+2 -6
View File
@@ -26,9 +26,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
erred := make(map[string]error)
for _, t := range definitions {
if stringsutil.ContainsAny(t, knownConfigFiles...) {
// TODO: this is a temporary fix to avoid treating these files as templates
// this should be replaced with more appropriate and robust logic
if config.IsKnownConfigFile(t) {
continue
}
if strings.Contains(t, urlutil.SchemeSeparator) && stringsutil.ContainsAny(t, config.GetSupportTemplateFileExtensions()...) {
@@ -52,9 +50,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
// purge all false positives
filteredTemplates := []string{}
for _, v := range allTemplates {
// TODO: this is a temporary fix to avoid treating these files as templates
// this should be replaced with more appropriate and robust logic
if !stringsutil.ContainsAny(v, knownConfigFiles...) {
if !config.IsKnownConfigFile(v) {
filteredTemplates = append(filteredTemplates, v)
}
}
+43 -1
View File
@@ -1,9 +1,10 @@
package disk
import (
"testing/fstest"
"os"
"path/filepath"
"testing"
"testing/fstest"
"github.com/stretchr/testify/require"
)
@@ -42,6 +43,8 @@ func TestFindGlobPathMatchesUsesCanonicalContainment(t *testing.T) {
}
}
// TestFindGlobPathMatchesResolvesContainedPath ensures a glob inside the
// templates directory resolves against the embedded FS.
func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
memFS := fstest.MapFS{
"http/test.yaml": {Data: []byte("legit")},
@@ -56,3 +59,42 @@ func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []string{"http/test.yaml"}, matches)
}
// TestGetTemplatesPathKnownConfigFiles ensures that only exact known config
// file names are skipped, and templates whose names merely contain one (e.g.
// cves.json.yaml) are still loaded.
func TestGetTemplatesPathKnownConfigFiles(t *testing.T) {
templatesDir := t.TempDir()
httpDir := filepath.Join(templatesDir, "http")
require.NoError(t, os.MkdirAll(httpDir, 0755))
template := filepath.Join(httpDir, "cves.json.yaml")
for _, f := range []string{
template,
filepath.Join(templatesDir, "cves.json"),
filepath.Join(templatesDir, "contributors.json"),
filepath.Join(templatesDir, "TEMPLATES-STATS.json"),
} {
require.NoError(t, os.WriteFile(f, []byte("id: test"), 0644))
}
cat := NewCatalog(templatesDir)
t.Run("direct path", func(t *testing.T) {
paths, errs := cat.GetTemplatesPath([]string{template})
require.Empty(t, errs)
require.Equal(t, []string{template}, paths)
})
t.Run("directory traversal", func(t *testing.T) {
paths, errs := cat.GetTemplatesPath([]string{templatesDir})
require.Empty(t, errs)
require.Equal(t, []string{template}, paths)
})
t.Run("config file is skipped", func(t *testing.T) {
paths, errs := cat.GetTemplatesPath([]string{filepath.Join(templatesDir, "cves.json")})
require.Empty(t, errs)
require.Empty(t, paths)
})
}
-3
View File
@@ -1,3 +0,0 @@
package disk
var knownConfigFiles = []string{"cves.json", "contributors.json", "TEMPLATES-STATS.json"}
+2 -1
View File
@@ -19,7 +19,7 @@ const (
// IndexVersion is the schema version for cache invalidation on breaking
// changes.
IndexVersion = 2
IndexVersion = 3
// DefaultMaxSize is the default maximum number of templates to cache.
DefaultMaxSize = 50000
@@ -70,6 +70,7 @@ func NewIndex(cacheDir string) (*Index, error) {
weight += len(value.Name)
weight += len(value.Severity)
weight += len(value.ProtocolType)
weight += len(value.Product)
weight += len(value.TemplateVerifier)
weight += len(value.VerifierFingerprint)
weight += len(value.ContentDigest)
+2
View File
@@ -905,6 +905,7 @@ func TestNewMetadataFromTemplate(t *testing.T) {
SeverityHolder: severity.Holder{
Severity: severity.Low,
},
Metadata: map[string]any{"product": " Tomcat "},
},
Verified: true,
TemplateVerifier: "verifier",
@@ -922,4 +923,5 @@ func TestNewMetadataFromTemplate(t *testing.T) {
require.Equal(t, tmpl.Type().String(), metadata.ProtocolType)
require.Equal(t, tmpl.Verified, metadata.Verified)
require.Equal(t, tmpl.TemplateVerifier, metadata.TemplateVerifier)
require.Equal(t, "tomcat", metadata.Product)
}
+10
View File
@@ -3,6 +3,7 @@ package index
import (
"os"
"slices"
"strings"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
@@ -48,6 +49,9 @@ type Metadata struct {
// ProtocolType is the primary protocol type of the template.
ProtocolType string `gob:"protocol_type"`
// Product is the lowercased info.metadata.product of the template.
Product string `gob:"product,omitempty"`
// Verified indicates whether the template is verified.
Verified bool `gob:"verified"`
@@ -114,6 +118,7 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
Severity: tpl.Info.SeverityHolder.Severity.String(),
ProtocolType: tpl.Type().String(),
Product: templateProduct(tpl),
Verified: tpl.Verified,
TemplateVerifier: tpl.TemplateVerifier,
@@ -122,6 +127,11 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
}
}
func templateProduct(tpl *templates.Template) string {
product, _ := tpl.Info.Metadata["product"].(string)
return strings.ToLower(strings.TrimSpace(product))
}
// IsValid checks if the cached metadata is still valid by comparing the file
// modification time.
func (m *Metadata) IsValid() bool {
+17
View File
@@ -76,6 +76,10 @@ type Config struct {
// MetadataIndex is an optional shared index borrowed by the store. The
// caller remains responsible for persisting it.
MetadataIndex *index.Index
// TargetFilter narrows loading to the templates per-target profiles
// select for at least one target; nil loads every filtered template.
TargetFilter index.FilterFunc
}
// Store is a storage for loaded nuclei templates
@@ -387,6 +391,11 @@ func (store *Store) buildIndexFilter() *index.Filter {
}
}
// selectedByTargets reports whether some target's profile selects the template.
func (store *Store) selectedByTargets(metadata *index.Metadata) bool {
return store.config.TargetFilter == nil || store.config.TargetFilter(metadata)
}
func (store *Store) loadTemplatesIndex() *index.Index {
var metadataIdx *index.Index
@@ -882,6 +891,10 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
return
}
if !store.selectedByTargets(metadata) {
return
}
if len(tags) > 0 && !slices.ContainsFunc(tags, metadata.HasTag) {
return
}
@@ -916,6 +929,10 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
noteExcludedByTag(templatePath, metadata)
return
}
if metadata != nil && !store.selectedByTargets(metadata) {
return
}
}
if err != nil {
+7 -1
View File
@@ -1,6 +1,8 @@
package core
import (
"sync"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
@@ -40,7 +42,11 @@ type TemplateExecutionCallback func(TemplateExecutionEvent)
// templates to leading to the final execution by the work pool, it is
// handled by the engine.
type Engine struct {
workPool *WorkPool
workPool *WorkPool
// originScoped tracks origin-scoped template attempts. A pair is finished
// only after a successful execution, so a failed first target does not
// suppress every later URL on that origin.
originScoped sync.Map
options *types.Options
executerOpts *protocols.ExecutorOptions
Callback func(*output.ResultEvent) // Executed on results
+67 -16
View File
@@ -7,6 +7,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
@@ -62,20 +63,6 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
return &atomic.Bool{}
}
if e.executerOpts.Progress != nil {
// Notes:
// workflow requests are not counted as they can be conditional
// templateList count is user requested templates count (before clustering)
// totalReqAfterClustering is total requests count after clustering
e.executerOpts.Progress.Init(target.Count(), len(templatesList), int64(totalReqAfterClustering))
}
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
// based on no of hosts , templates , stream and other optimization parameters
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
}
filtered := []*templates.Template{}
selfContained := []*templates.Template{}
// Filter Self Contained templates since they are not bound to target
@@ -87,6 +74,22 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
}
}
if e.executerOpts.Progress != nil {
// Notes:
// workflow requests are not counted as they can be conditional
// templateList count is user requested templates count (before clustering)
// totalReqAfterClustering is total requests count after clustering
requests, tasks := e.expectedWork(finalTemplates, filtered, target, totalReqAfterClustering)
e.executerOpts.Progress.Init(target.Count(), len(templatesList), requests)
// a self-contained template runs once
e.addTaskTotal(tasks + int64(len(selfContained)))
}
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
// Last-resort fallback: runner scan planner normally resolves auto.
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
}
// Execute All SelfContained in parallel
e.executeAllSelfContained(ctx, selfContained, results, selfcontainedWg)
@@ -122,7 +125,7 @@ func (e *Engine) executeTemplateSpray(ctx context.Context, templatesList []*temp
}
// resize check point - nop if there are no changes
wp.RefreshWithConfig(e.GetWorkPoolConfig())
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
templateType := template.Type()
var wg *syncutil.AdaptiveWaitGroup
@@ -169,7 +172,10 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
default:
}
wp.Add()
if err := wp.AddWithContext(ctx); err != nil {
return false
}
go func(targetval *contextargs.MetaInput) {
defer wp.Done()
e.executeTemplatesOnTarget(ctx, templatesList, targetval, results)
@@ -179,8 +185,50 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
return results
}
// expectedWork returns the requests and the tasks the target-bound templates
// will run. It counts only the template and target pairs that per-target
// profiles select, so progress reflects the work that will be done. A task is
// one template, cluster or workflow run on one target.
func (e *Engine) expectedWork(templatesList, targetBound []*templates.Template, target provider.InputProvider, unscopedRequests int) (requests, tasks int64) {
scope := e.executerOpts.TargetScope
if scope == nil {
return int64(unscopedRequests), target.Count() * int64(len(targetBound))
}
targets := make(map[protocols.TemplateSelection]int64)
target.Iterate(func(value *contextargs.MetaInput) bool {
targets[scope.For(value)]++
return true
})
for selection, count := range targets {
requests += count * int64(getSelectedRequestCount(templatesList, selection))
tasks += count * int64(getSelectedCount(targetBound, selection))
}
return requests, tasks
}
// getSelectedCount returns the number of templates selection selects; a nil
// selection selects every template.
func getSelectedCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
if selection == nil {
return len(templates)
}
count := 0
for _, template := range templates {
if template.SelectedBy(selection) {
count++
}
}
return count
}
// returns total requests count
func getRequestCount(templates []*templates.Template) int {
return getSelectedRequestCount(templates, nil)
}
// getSelectedRequestCount returns the requests of the templates selection
// selects; a nil selection selects every template.
func getSelectedRequestCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
count := 0
for _, template := range templates {
// ignore requests in workflows as total requests in workflow
@@ -188,6 +236,9 @@ func getRequestCount(templates []*templates.Template) int {
if len(template.Workflows) > 0 {
continue
}
if selection != nil && !template.SelectedBy(selection) {
continue
}
count += template.TotalRequests
}
return count
+143 -12
View File
@@ -6,8 +6,11 @@ import (
"sync/atomic"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
@@ -35,6 +38,7 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
}
finished := e.templateExecutionStarted(template, "")
defer func() { finished(ctx.Err()) }()
defer e.tasksDone(1)
var err error
var match bool
ctx := scan.NewScanContext(ctx, contextargs.New(ctx))
@@ -154,17 +158,29 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
skip = false
}
// out of scope pairs keep their index so resume positions stay stable
inScope := e.inScope(template, scannedValue)
if !inScope {
skip = true
}
currentInfo.Lock()
currentInfo.InFlight[index] = struct{}{}
currentInfo.Unlock()
// Skip if the host has had errors
if e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
hostDown := inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(template.Type().String(), contextargs.NewWithMetaInput(ctx, scannedValue))
// in-scope pairs that never reach executeTemplateOnInput are done here:
// those done by the resumed scan and those of an unresponsive host
if inScope && (skip || hostDown) {
e.tasksDone(1)
}
if hostDown {
skipEvent := &output.ResultEvent{
TemplateID: template.ID,
TemplatePath: template.Path,
Info: template.Info,
Type: e.executerOpts.ProtocolType.String(),
Type: template.Type().String(),
Host: scannedValue.Input,
MatcherStatus: false,
Error: "host was skipped as it was found unresponsive",
@@ -203,22 +219,26 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
wp := e.GetWorkPool()
defer wp.Wait()
for _, tpl := range alltemplates {
for i, tpl := range alltemplates {
select {
case <-ctx.Done():
return
default:
}
if !e.inScope(tpl, target) {
continue
}
// Check whether the target has already been marked as permanently
// unresponsive by HostErrorsCache before spawning another goroutine.
if e.executerOpts.HostErrorsCache != nil &&
e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, target)) {
e.executerOpts.HostErrorsCache.Check(tpl.Type().String(), contextargs.NewWithMetaInput(ctx, target)) {
skipEvent := &output.ResultEvent{
TemplateID: tpl.ID,
TemplatePath: tpl.Path,
Info: tpl.Info,
Type: e.executerOpts.ProtocolType.String(),
Type: tpl.Type().String(),
Host: target.Input,
MatcherStatus: false,
Error: "host was skipped as it was found unresponsive",
@@ -229,11 +249,13 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
} else if e.executerOpts.Output != nil {
_ = e.executerOpts.Output.Write(skipEvent)
}
// the remaining templates are skipped for this target as well
e.tasksDone(int64(getSelectedCount(alltemplates[i:], e.selectionFor(target))))
break
}
// resize check point - nop if there are no changes
wp.RefreshWithConfig(e.GetWorkPoolConfig())
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
var sg *syncutil.AdaptiveWaitGroup
if tpl.Type() == types.HeadlessProtocol {
@@ -266,22 +288,74 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
}
}
// inScope reports whether per-target profiles select template for input.
func (e *Engine) inScope(template *templates.Template, input *contextargs.MetaInput) bool {
selection := e.selectionFor(input)
return selection == nil || template.SelectedBy(selection)
}
// selectionFor returns the templates per-target profiles select for input; nil
// selects every template.
func (e *Engine) selectionFor(input *contextargs.MetaInput) protocols.TemplateSelection {
if e.executerOpts.TargetScope == nil {
return nil
}
return e.executerOpts.TargetScope.For(input)
}
// taskTracker returns the progress when it counts tasks, nil otherwise.
func (e *Engine) taskTracker() progress.TaskTracker {
if e.executerOpts == nil {
return nil
}
tracker, _ := e.executerOpts.Progress.(progress.TaskTracker)
return tracker
}
// addTaskTotal adds count tasks to the total when the progress counts tasks.
func (e *Engine) addTaskTotal(count int64) {
if tracker := e.taskTracker(); tracker != nil {
tracker.AddToTaskTotal(count)
}
}
// tasksDone marks count tasks as done when the progress counts tasks.
func (e *Engine) tasksDone(count int64) {
if tracker := e.taskTracker(); tracker != nil {
tracker.IncrementTasksBy(count)
}
}
// executeTemplateOnInput performs template execution for a single input and returns match status and error
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (bool, error) {
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (matched bool, err error) {
// a pair skipped as a repeat of its origin is done as well
defer e.tasksDone(1)
// A template fixed to the target's origin makes the same requests for every
// target that shares one, which a crawled list produces by the hundred.
// Running it once per origin sends exactly the same traffic, minus the
// repeats. The reservation is kept only when execution succeeds, so a
// failed attempt leaves the origin free for a later target.
finish, run := e.claimOriginScoped(template, value)
if !run {
return false, nil
}
defer func() { finish(err == nil) }()
finished := e.templateExecutionStarted(template, value.Input)
defer func() { finished(ctx.Err()) }()
ctxArgs := contextargs.New(ctx)
// every protocol dials the target's host through its pinned ip, if any
pinnedCtx := contextargs.WithCustomIP(ctx, value)
ctxArgs := contextargs.New(pinnedCtx)
ctxArgs.MetaInput = value
scanCtx := scan.NewScanContext(ctx, ctxArgs)
scanCtx := scan.NewScanContext(pinnedCtx, ctxArgs)
switch template.Type() {
case types.WorkflowProtocol:
return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil
default:
if e.Callback != nil {
results, err := template.Executer.ExecuteWithResults(scanCtx)
if err != nil {
return false, err
results, execErr := template.Executer.ExecuteWithResults(scanCtx)
if execErr != nil {
return false, execErr
}
for _, result := range results {
e.Callback(result)
@@ -291,3 +365,60 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
return template.Executer.Execute(scanCtx)
}
}
// originAttempt is one origin-scoped execution. done is set only after a
// successful run. While inflight is set, other callers wait, then retry if
// that attempt failed.
type originAttempt struct {
mu sync.Mutex
cond *sync.Cond
inflight bool
done bool
}
func newOriginAttempt() *originAttempt {
attempt := &originAttempt{}
attempt.cond = sync.NewCond(&attempt.mu)
return attempt
}
// claimOriginScoped reports whether this caller should execute an origin-scoped
// template. finish(true) records a successful run. finish(false) releases the
// claim so a later target, including one that waited, can try again. Templates
// whose requests depend on the target path always run. CustomIP is part of the
// key so each address selected for one URL is tested on its own.
func (e *Engine) claimOriginScoped(template *templates.Template, value *contextargs.MetaInput) (finish func(bool), run bool) {
noop := func(bool) {}
if template == nil || value == nil || !template.IsOriginScoped() {
return noop, true
}
origin := normalize.Origin(value.Input)
if origin == "" {
return noop, true
}
key := template.ID + "\x00" + origin + "\x00" + value.CustomIP
actual, _ := e.originScoped.LoadOrStore(key, newOriginAttempt())
slot := actual.(*originAttempt)
slot.mu.Lock()
for {
if slot.done {
slot.mu.Unlock()
return noop, false
}
if !slot.inflight {
slot.inflight = true
slot.mu.Unlock()
return func(success bool) {
slot.mu.Lock()
slot.inflight = false
if success {
slot.done = true
}
slot.cond.Broadcast()
slot.mu.Unlock()
}, true
}
slot.cond.Wait()
}
}
+396
View File
@@ -2,6 +2,7 @@ package core
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
@@ -9,10 +10,12 @@ import (
"testing"
"time"
"github.com/projectdiscovery/gologger"
inputtypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
@@ -140,6 +143,289 @@ func TestExecuteTemplateOnInputReportsCancellationOnFinish(t *testing.T) {
}
}
type scriptedExecuter struct {
calls int
errs []error
miss bool
}
func (s *scriptedExecuter) Compile() error { return nil }
func (s *scriptedExecuter) Requests() int { return 1 }
func (s *scriptedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
s.calls++
if len(s.errs) == 0 {
return !s.miss, nil
}
err := s.errs[0]
s.errs = s.errs[1:]
return false, err
}
func (s *scriptedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
_, err := s.Execute(ctx)
return nil, err
}
func originScopedTemplate(path string) *templates.Template {
return &templates.Template{
ID: "root-health",
RequestsHTTP: []*httpprotocol.Request{{
Path: []string{path},
}},
}
}
func TestOriginScopedRetriesAfterFailure(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
if err == nil {
t.Fatal("expected the first attempt to fail")
}
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
if err != nil || !ok {
t.Fatalf("later target should retry the origin, ok=%v err=%v", ok, err)
}
if exec.calls != 2 {
t.Fatalf("calls = %d, want 2", exec.calls)
}
}
func TestOriginScopedSkipsAfterSuccess(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err != nil {
t.Fatal(err)
}
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b?q=1"}); err != nil {
t.Fatal(err)
}
if exec.calls != 1 {
t.Fatalf("calls = %d, want 1", exec.calls)
}
}
func TestOriginScopedKeepsEachCustomIP(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
for _, ip := range []string{"203.0.113.1", "203.0.113.2"} {
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
Input: "https://example.com/a",
CustomIP: ip,
})
if err != nil {
t.Fatal(err)
}
}
if exec.calls != 2 {
t.Fatalf("calls = %d, want 2", exec.calls)
}
}
func TestBaseURLTemplateIsNotOriginScoped(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{BaseURL}}/health")
tpl.Executer = exec
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
t.Fatal(err)
}
}
if exec.calls != 2 {
t.Fatalf("calls = %d, want 2", exec.calls)
}
}
func TestOriginScopedWaiterRetriesAfterFailedAttempt(t *testing.T) {
e := newTestEngine()
started := make(chan struct{})
release := make(chan struct{})
var calls atomic.Int32
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = &gatedExecuter{started: started, release: release, calls: &calls}
firstDone := make(chan error, 1)
go func() {
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
firstDone <- err
}()
<-started
secondDone := make(chan error, 1)
go func() {
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
secondDone <- err
}()
select {
case err := <-secondDone:
t.Fatalf("second target returned while the first attempt was still running: %v", err)
case <-time.After(50 * time.Millisecond):
}
close(release)
if err := <-firstDone; err == nil {
t.Fatal("expected the first attempt to fail")
}
if err := <-secondDone; err != nil {
t.Fatalf("waiter should run after the failed attempt: %v", err)
}
if calls.Load() != 2 {
t.Fatalf("calls = %d, want 2", calls.Load())
}
}
func TestOriginScopedKeepsDistinctOriginsAndTemplates(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
first := originScopedTemplate("{{RootURL}}/health")
first.Executer = exec
second := originScopedTemplate("{{RootURL}}/health")
second.ID = "other-template"
second.Executer = exec
inputs := []*contextargs.MetaInput{
{Input: "https://example.com/a"},
{Input: "https://other.test/a"},
{Input: "http://example.com/a"},
{Input: "https://example.com:8443/a"},
}
for _, value := range inputs {
if _, err := e.executeTemplateOnInput(context.Background(), first, value); err != nil {
t.Fatal(err)
}
}
if _, err := e.executeTemplateOnInput(context.Background(), second, inputs[0]); err != nil {
t.Fatal(err)
}
if exec.calls != 5 {
t.Fatalf("calls = %d, want 5", exec.calls)
}
}
func TestOriginScopedFoldsDefaultPort(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
for _, raw := range []string{"https://example.com/a", "https://example.com:443/b"} {
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
t.Fatal(err)
}
}
if exec.calls != 1 {
t.Fatalf("calls = %d, want 1", exec.calls)
}
}
func TestOriginScopedSameCustomIPSkips(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
for range 2 {
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
Input: "https://example.com/a",
CustomIP: "203.0.113.1",
})
if err != nil {
t.Fatal(err)
}
}
if exec.calls != 1 {
t.Fatalf("calls = %d, want 1", exec.calls)
}
}
func TestOriginScopedMissStillCounts(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{miss: true}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw})
if err != nil || ok {
t.Fatalf("non-match ok=%v err=%v", ok, err)
}
}
if exec.calls != 1 {
t.Fatalf("calls = %d, want 1", exec.calls)
}
}
func TestOriginScopedCallbackErrorRetries(t *testing.T) {
e := newTestEngine()
e.Callback = func(*output.ResultEvent) {}
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err == nil {
t.Fatal("expected the callback path to fail")
}
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"}); err != nil {
t.Fatal(err)
}
if exec.calls != 2 {
t.Fatalf("calls = %d, want 2", exec.calls)
}
}
func TestOriginScopedLeavesValuesWithoutAHost(t *testing.T) {
e := newTestEngine()
exec := &scriptedExecuter{}
tpl := originScopedTemplate("{{RootURL}}/health")
tpl.Executer = exec
for range 2 {
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "not a url"}); err != nil {
t.Fatal(err)
}
}
for _, raw := range []string{"example.com", "other.test", "example.com"} {
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
t.Fatal(err)
}
}
if exec.calls != 4 {
t.Fatalf("calls = %d, want 4", exec.calls)
}
}
type gatedExecuter struct {
started chan struct{}
release chan struct{}
calls *atomic.Int32
}
func (g *gatedExecuter) Compile() error { return nil }
func (g *gatedExecuter) Requests() int { return 1 }
func (g *gatedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
if g.calls.Add(1) == 1 {
close(g.started)
<-g.release
return false, errors.New("timeout")
}
return true, nil
}
func (g *gatedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
ok, err := g.Execute(ctx)
if err != nil || !ok {
return nil, err
}
return []*output.ResultEvent{{}}, nil
}
type fakeTargetProvider struct {
values []*contextargs.MetaInput
}
@@ -370,3 +656,113 @@ func Test_executeTemplateWithTargets_RespectsCancellation(t *testing.T) {
var matched atomic.Bool
e.executeTemplateWithTargets(ctx, tpl, targets, &matched)
}
func newCancelTestEngine() *Engine {
opts := &types.Options{
TemplateThreads: 1,
HeadlessTemplateThreads: 1,
BulkSize: 1,
HeadlessBulkSize: 1,
Logger: gologger.DefaultLogger,
}
e := New(opts)
e.SetExecuterOptions(&protocols.ExecutorOptions{Logger: e.Logger, ResumeCfg: types.NewResumeCfg(), ProtocolType: tmpltypes.HTTPProtocol})
return e
}
type gateExecuter struct {
started chan struct{}
once sync.Once
}
func (g *gateExecuter) Compile() error { return nil }
func (g *gateExecuter) Requests() int { return 1 }
func (g *gateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
g.once.Do(func() { close(g.started) })
<-ctx.Context().Done()
return false, ctx.Context().Err()
}
func (g *gateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
_, err := g.Execute(ctx)
return nil, err
}
func TestExecuteTemplateSprayUnblocksCancelledAcquire(t *testing.T) {
e := newCancelTestEngine()
gate := &gateExecuter{started: make(chan struct{})}
templatesList := []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
e.executeTemplateSpray(ctx, templatesList, targets)
}()
select {
case <-gate.started:
case <-time.After(2 * time.Second):
t.Fatal("first template did not start")
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("executeTemplateSpray stayed blocked after cancel")
}
}
func TestExecuteHostSprayUnblocksCancelledAcquire(t *testing.T) {
e := newCancelTestEngine()
gate := &gateExecuter{started: make(chan struct{})}
templatesList := []*templates.Template{{Executer: gate}}
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}, {Input: "b"}}}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
e.executeHostSpray(ctx, templatesList, targets)
}()
select {
case <-gate.started:
case <-time.After(2 * time.Second):
t.Fatal("first host did not start")
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("executeHostSpray stayed blocked after cancel")
}
}
func TestExecuteTemplatesOnTargetUnblocksCancelledAcquire(t *testing.T) {
e := newCancelTestEngine()
gate := &gateExecuter{started: make(chan struct{})}
var matched atomic.Bool
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
e.executeTemplatesOnTarget(ctx, []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}, &contextargs.MetaInput{Input: "a"}, &matched)
}()
select {
case <-gate.started:
case <-time.After(2 * time.Second):
t.Fatal("first template did not start")
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("executeTemplatesOnTarget stayed blocked after cancel")
}
}
+165
View File
@@ -0,0 +1,165 @@
package plan
import (
"fmt"
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
)
// Input is the scan shape the planner uses to pick strategy and reachability.
type Input struct {
Hosts int
Templates int
Requests int // template×host request estimate (baseline)
BulkSize int
Stream bool
StrictProbe bool
// Reachability / probe budget (known before or after probing).
PortsToProbe int
ConcreteNetworkTemplates int
// FilteredRequests, when > 0, is the post-filter request estimate the plan reports.
FilteredRequests int
// GroupCount is distinct host signatures after probing (0 = unknown).
GroupCount int
// TechFilter enables product/macro tag filtering after cheap HTTP fingerprinting.
TechFilter bool
// TechBoundTemplates is how many templates carry product/macro tags.
TechBoundTemplates int
}
// Plan is the execution plan for a single scan pass.
type Plan struct {
Strategy string
BuildReachability bool
ProbePorts bool
UseReachabilityFilter bool
UseTechFilter bool
ExpectedRequests int64
Reason string
}
// Decide chooses spray strategy and whether to spend budget on reachability probing.
//
// Hard bar: never choose work that is expected to regress wall clock vs baseline spray.
// When probe cost would dominate and savings are uncertain, reachability is skipped.
func Decide(in Input) Plan {
p := Plan{
Strategy: chooseStrategy(in),
ExpectedRequests: int64(max(in.Requests, 0)),
}
reachReason := "reachability=off"
if in.StrictProbe {
reach := decideReachability(in)
p.BuildReachability = reach.build
p.ProbePorts = reach.probePorts
p.UseReachabilityFilter = reach.useFilter
reachReason = reach.reason
}
tech := decideTechFilter(in)
p.UseTechFilter = tech.use
p.Reason = strategyReason(in, p.Strategy) + "; " + reachReason + "; " + tech.reason
if in.FilteredRequests > 0 {
p.ExpectedRequests = int64(in.FilteredRequests)
}
return p
}
func chooseStrategy(in Input) string {
if in.Stream || in.Hosts <= 0 || in.Templates <= 0 {
return scanstrategy.TemplateSpray.String()
}
bulk := in.BulkSize
if bulk <= 0 {
bulk = 25
}
// Host-spray only when the shape strongly favors per-host locality.
// Conservative default is template-spray (historical auto) so wall clock
// does not regress on small / mixed benches.
if in.Hosts <= bulk && in.Hosts <= 10 && in.Templates >= 10*in.Hosts && in.Requests >= 5000 {
return scanstrategy.HostSpray.String()
}
return scanstrategy.TemplateSpray.String()
}
func strategyReason(in Input, strategy string) string {
return fmt.Sprintf("strategy=%s hosts=%d templates=%d", strategy, in.Hosts, in.Templates)
}
type reachabilityDecision struct {
build bool
probePorts bool
useFilter bool
reason string
}
func decideReachability(in Input) reachabilityDecision {
if in.Hosts <= 0 || in.Templates <= 0 {
return reachabilityDecision{reason: "reachability=skipped-empty"}
}
probeCost := in.PortsToProbe * in.Hosts
baseline := in.Requests
if baseline <= 0 {
baseline = in.Templates * in.Hosts
}
// No concrete network ports to probe: HTTP-only index is cheap (reuse httpx map).
if in.PortsToProbe == 0 || in.ConcreteNetworkTemplates == 0 {
return reachabilityDecision{
build: true,
probePorts: false,
useFilter: true,
reason: "reachability=http-only",
}
}
// Probe budget: if dialing every port×host costs more than the scan itself
// and we have no signal of a mixed fleet, skip probing to match baseline time.
if probeCost > baseline && in.GroupCount <= 1 {
// GroupCount==0 (unknown) or 1 (homogeneous): do not pay for probes.
if probeCost > max(baseline*2, 500) {
return reachabilityDecision{
reason: fmt.Sprintf("reachability=skipped-probe-budget cost=%d baseline=%d", probeCost, baseline),
}
}
}
// After probing, homogeneous fleets still benefit from a cheap filter (no extra dials
// during spray for closed-port templates that global prune may have kept).
return reachabilityDecision{
build: true,
probePorts: true,
useFilter: true,
reason: fmt.Sprintf("reachability=full probe_cost=%d", probeCost),
}
}
type techFilterDecision struct {
use bool
reason string
}
func decideTechFilter(in Input) techFilterDecision {
// Opt-in only: never enable from strategy/reachability alone.
if !in.TechFilter {
return techFilterDecision{reason: "tech-filter=off"}
}
// Skip fingerprinting when nothing is tech-bound — otherwise we add
// probe latency with zero savings (wall-clock regression).
if in.TechBoundTemplates <= 0 {
return techFilterDecision{reason: "tech-filter=skipped-no-bound-templates"}
}
return techFilterDecision{use: true, reason: fmt.Sprintf("tech-filter=on bound=%d", in.TechBoundTemplates)}
}
// ApplyFiltered updates ExpectedRequests once post-probe estimates are known.
func (p *Plan) ApplyFiltered(filtered int) {
if filtered > 0 {
p.ExpectedRequests = int64(filtered)
}
}
+205
View File
@@ -0,0 +1,205 @@
package plan
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
"github.com/stretchr/testify/require"
)
func TestDecideStrategyHostSpray(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 200,
Requests: 10000,
BulkSize: 25,
})
require.Equal(t, scanstrategy.HostSpray.String(), p.Strategy)
require.Contains(t, p.Reason, "strategy=host-spray")
}
func TestDecideStrategyTemplateSpraySmallBench(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 100,
Requests: 500,
BulkSize: 25,
})
require.Equal(t, scanstrategy.TemplateSpray.String(), p.Strategy)
}
func TestDecideStrategyTemplateSprayManyHosts(t *testing.T) {
p := Decide(Input{
Hosts: 1000,
Templates: 50,
Requests: 50000,
BulkSize: 25,
})
require.Equal(t, scanstrategy.TemplateSpray.String(), p.Strategy)
}
func TestDecideStrategyStreamDefaultsTemplateSpray(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 100,
Stream: true,
BulkSize: 25,
})
require.Equal(t, scanstrategy.TemplateSpray.String(), p.Strategy)
}
func TestDecideReachabilityOffWithoutStrictProbe(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 60,
Requests: 300,
StrictProbe: false,
PortsToProbe: 3,
})
require.False(t, p.BuildReachability)
require.False(t, p.UseReachabilityFilter)
require.Contains(t, p.Reason, "reachability=off")
}
func TestDecideReachabilityHTTPOnly(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 60,
Requests: 300,
StrictProbe: true,
PortsToProbe: 0,
ConcreteNetworkTemplates: 0,
})
require.True(t, p.BuildReachability)
require.False(t, p.ProbePorts)
require.True(t, p.UseReachabilityFilter)
require.Contains(t, p.Reason, "reachability=http-only")
}
func TestDecideReachabilityProbeBudgetSkip(t *testing.T) {
p := Decide(Input{
Hosts: 100,
Templates: 10,
Requests: 1000,
StrictProbe: true,
PortsToProbe: 50, // cost = 5000 > 2*baseline
ConcreteNetworkTemplates: 5,
GroupCount: 0,
})
require.False(t, p.BuildReachability)
require.False(t, p.UseReachabilityFilter)
require.Contains(t, p.Reason, "reachability=skipped-probe-budget")
}
func TestDecideReachabilityProbeWithinBudget(t *testing.T) {
p := Decide(Input{
Hosts: 100,
Templates: 10,
Requests: 1000,
StrictProbe: true,
PortsToProbe: 15, // cost = 1500: above baseline, within 2*baseline
ConcreteNetworkTemplates: 5,
GroupCount: 0,
})
require.True(t, p.BuildReachability)
require.True(t, p.ProbePorts)
require.True(t, p.UseReachabilityFilter)
require.Contains(t, p.Reason, "reachability=full")
}
func TestDecideReachabilityFull(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 60,
Requests: 300,
StrictProbe: true,
PortsToProbe: 3, // cost = 15 << baseline
ConcreteNetworkTemplates: 30,
})
require.True(t, p.BuildReachability)
require.True(t, p.ProbePorts)
require.True(t, p.UseReachabilityFilter)
require.Contains(t, p.Reason, "reachability=full")
}
func TestDecideReachabilityEmptyInput(t *testing.T) {
p := Decide(Input{StrictProbe: true, Hosts: 0, Templates: 10})
require.False(t, p.BuildReachability)
require.Contains(t, p.Reason, "reachability=skipped-empty")
}
func TestDecideReachabilityConcreteZeroUsesHTTPOnly(t *testing.T) {
p := Decide(Input{
Hosts: 20,
Templates: 100,
Requests: 2000,
StrictProbe: true,
PortsToProbe: 5,
ConcreteNetworkTemplates: 0, // web-only set
})
require.True(t, p.BuildReachability)
require.False(t, p.ProbePorts)
require.True(t, p.UseReachabilityFilter)
}
func TestDecideHostSprayRequiresLargeRequestBudget(t *testing.T) {
// hosts small + many templates but requests below threshold → template-spray
p := Decide(Input{
Hosts: 5,
Templates: 200,
Requests: 4999,
BulkSize: 25,
})
require.Equal(t, scanstrategy.TemplateSpray.String(), p.Strategy)
}
func TestDecideApplyFilteredIgnoredWhenZero(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 10,
Requests: 50,
StrictProbe: false,
FilteredRequests: 0,
})
require.Equal(t, int64(50), p.ExpectedRequests)
p.ApplyFiltered(0)
require.Equal(t, int64(50), p.ExpectedRequests)
p.ApplyFiltered(12)
require.Equal(t, int64(12), p.ExpectedRequests)
}
func TestDecideTechFilterOffByDefault(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 100,
Requests: 500,
TechFilter: false,
TechBoundTemplates: 40,
})
require.False(t, p.UseTechFilter)
require.Contains(t, p.Reason, "tech-filter=off")
}
func TestDecideTechFilterSkippedWhenNoBound(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 100,
Requests: 500,
TechFilter: true,
TechBoundTemplates: 0,
})
require.False(t, p.UseTechFilter)
require.Contains(t, p.Reason, "tech-filter=skipped-no-bound-templates")
}
func TestDecideTechFilterOn(t *testing.T) {
p := Decide(Input{
Hosts: 5,
Templates: 100,
Requests: 500,
TechFilter: true,
TechBoundTemplates: 40,
})
require.True(t, p.UseTechFilter)
require.Contains(t, p.Reason, "tech-filter=on")
}
+116
View File
@@ -0,0 +1,116 @@
package core
import (
"context"
"slices"
"sync"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
"github.com/stretchr/testify/require"
)
// allowList selects the templates it lists.
type allowList struct{ paths []string }
func (a *allowList) Allows(templatePath string) bool { return slices.Contains(a.paths, templatePath) }
// scopeByInput maps inputs to selections; unlisted inputs are unrestricted.
type scopeByInput map[string]*allowList
func (s scopeByInput) For(input *contextargs.MetaInput) protocols.TemplateSelection {
if selection, ok := s[input.Input]; ok {
return selection
}
return nil
}
// recordingExecuter records the inputs it runs on.
type recordingExecuter struct {
path string
mu *sync.Mutex
runs *[]string
}
func (r *recordingExecuter) Compile() error { return nil }
func (r *recordingExecuter) Requests() int { return 1 }
func (r *recordingExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
*r.runs = append(*r.runs, r.path+"@"+ctx.Input.MetaInput.Input)
return false, nil
}
func (r *recordingExecuter) ExecuteWithResults(*scan.ScanContext) ([]*output.ResultEvent, error) {
return nil, nil
}
// progressRecorder captures the expected request count; the engine calls no
// other progress method on this path.
type progressRecorder struct {
progress.Progress
requests int64
}
func (p *progressRecorder) Init(_ int64, _ int, requests int64) { p.requests = requests }
func TestExecuteScanRunsOnlyScopedPairs(t *testing.T) {
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
t.Run(strategy.String(), func(t *testing.T) {
options := &types.Options{
BulkSize: 2,
TemplateThreads: 2,
HeadlessBulkSize: 1,
HeadlessTemplateThreads: 1,
ScanStrategy: strategy.String(),
}
progress := &progressRecorder{}
engine := New(options)
engine.SetExecuterOptions(&protocols.ExecutorOptions{
Logger: engine.Logger,
Options: options,
ResumeCfg: types.NewResumeCfg(),
ProtocolType: tmpltypes.HTTPProtocol,
Progress: progress,
TargetScope: scopeByInput{
"php": {paths: []string{"thinkphp.yaml"}},
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
"none": {},
},
})
var (
mu sync.Mutex
runs []string
)
var templatesList []*templates.Template
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
templatesList = append(templatesList, &templates.Template{
ID: path,
Path: path,
TotalRequests: 1,
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
})
}
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{
{Input: "php"}, {Input: "tomcat"}, {Input: "none"}, {Input: "plain"},
}}
engine.ExecuteScanWithOpts(context.Background(), templatesList, targets, true)
require.ElementsMatch(t, []string{
"thinkphp.yaml@php",
"tomcat.yaml@tomcat", "generic.yaml@tomcat",
"thinkphp.yaml@plain", "tomcat.yaml@plain", "generic.yaml@plain",
}, runs)
require.EqualValues(t, len(runs), progress.requests, "progress counts only scoped pairs")
})
}
}
+242
View File
@@ -0,0 +1,242 @@
package core
import (
"context"
"sync"
"sync/atomic"
"testing"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
"github.com/stretchr/testify/require"
)
// taskRecorder counts the tasks the engine schedules and completes.
type taskRecorder struct {
progress.Progress
total atomic.Int64
done atomic.Int64
}
func (r *taskRecorder) Init(int64, int, int64) {}
func (r *taskRecorder) AddToTaskTotal(delta int64) { r.total.Add(delta) }
func (r *taskRecorder) IncrementTasksBy(count int64) { r.done.Add(count) }
// downHosts reports the listed inputs as unresponsive.
type downHosts struct {
hosterrorscache.CacheInterface
inputs map[string]bool
}
func (d *downHosts) Check(_ string, ctx *contextargs.Context) bool {
return d.inputs[ctx.MetaInput.Input]
}
// newTaskEngine returns an engine for strategy whose progress records tasks.
// down lists the unresponsive inputs, and a nil resume starts a fresh scan.
func newTaskEngine(strategy scanstrategy.ScanStrategy, scope protocols.TargetScope, down map[string]bool, resume *types.ResumeCfg) (*Engine, *taskRecorder) {
options := &types.Options{
BulkSize: 2,
TemplateThreads: 2,
HeadlessBulkSize: 1,
HeadlessTemplateThreads: 1,
ScanStrategy: strategy.String(),
Logger: gologger.DefaultLogger,
}
if resume == nil {
resume = types.NewResumeCfg()
}
recorder := &taskRecorder{}
executerOpts := &protocols.ExecutorOptions{
Options: options,
ResumeCfg: resume,
ProtocolType: tmpltypes.HTTPProtocol,
Progress: recorder,
TargetScope: scope,
}
if down != nil {
executerOpts.HostErrorsCache = &downHosts{inputs: down}
}
engine := New(options)
executerOpts.Logger = engine.Logger
engine.SetExecuterOptions(executerOpts)
return engine, recorder
}
// targetsOf returns a target provider of inputs.
func targetsOf(inputs ...string) *fakeTargetProvider {
targets := &fakeTargetProvider{}
for _, input := range inputs {
targets.values = append(targets.values, &contextargs.MetaInput{Input: input})
}
return targets
}
func TestExecuteScanCountsTasks(t *testing.T) {
scope := scopeByInput{
"php": {paths: []string{"thinkphp.yaml"}},
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
"none": {},
}
inputs := []string{"php", "tomcat", "none", "plain"}
tests := []struct {
name string
scope protocols.TargetScope
down map[string]bool
wantRuns int
wantTotal int64
wantDone int64
}{
{
name: "every pair",
// 3 templates on 4 targets, and the self-contained template once
wantRuns: 13,
wantTotal: 13,
wantDone: 13,
},
{
name: "per-target profiles",
scope: scope,
// php 1, tomcat 2, none 0, plain 3, self-contained 1
wantRuns: 7,
wantTotal: 7,
wantDone: 7,
},
{
name: "unresponsive host",
down: map[string]bool{"tomcat": true},
// tomcat is skipped for all 3 templates
wantRuns: 10,
wantTotal: 13,
wantDone: 13,
},
{
name: "unresponsive host with per-target profiles",
scope: scope,
down: map[string]bool{"tomcat": true},
// tomcat is skipped for its 2 templates
wantRuns: 5,
wantTotal: 7,
wantDone: 7,
},
}
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
for _, test := range tests {
t.Run(strategy.String()+"/"+test.name, func(t *testing.T) {
engine, recorder := newTaskEngine(strategy, test.scope, test.down, nil)
var (
mu sync.Mutex
runs []string
)
var templatesList []*templates.Template
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
templatesList = append(templatesList, &templates.Template{
ID: path,
Path: path,
TotalRequests: 1,
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
})
}
templatesList = append(templatesList, &templates.Template{
ID: "self.yaml",
Path: "self.yaml",
TotalRequests: 1,
SelfContained: true,
Executer: &recordingExecuter{path: "self.yaml", mu: &mu, runs: &runs},
})
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
require.Len(t, runs, test.wantRuns)
require.Equal(t, test.wantTotal, recorder.total.Load(), "task total")
require.Equal(t, test.wantDone, recorder.done.Load(), "done tasks")
})
}
}
}
func TestExecuteScanCountsOriginScopedTasks(t *testing.T) {
// a {{RootURL}} template runs once per origin, and the skipped repeats
// are done tasks too
inputs := []string{"http://a.test/x", "http://a.test/y", "http://a.test/z", "http://b.test/x"}
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
t.Run(strategy.String(), func(t *testing.T) {
engine, recorder := newTaskEngine(strategy, nil, nil, nil)
var (
mu sync.Mutex
runs []string
)
templatesList := []*templates.Template{{
ID: "root.yaml",
Path: "root.yaml",
TotalRequests: 1,
RequestsHTTP: []*http.Request{{Path: []string{"{{RootURL}}/"}}},
Executer: &recordingExecuter{path: "root.yaml", mu: &mu, runs: &runs},
}}
require.True(t, templatesList[0].IsOriginScoped())
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
require.Len(t, runs, 2, "one run per origin")
require.EqualValues(t, 4, recorder.total.Load(), "task total")
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
})
}
}
func TestExecuteScanCountsResumedTasks(t *testing.T) {
tests := []struct {
name string
down map[string]bool
wantRuns []string
}{
{
name: "responsive hosts",
wantRuns: []string{"todo.yaml@a", "todo.yaml@b"},
},
{
// a resumed pair on an unresponsive host is counted once
name: "unresponsive host",
down: map[string]bool{"a": true},
wantRuns: []string{"todo.yaml@b"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resumeCfg := types.NewResumeCfg()
resumeCfg.ResumeFrom["done.yaml"] = &types.ResumeInfo{Completed: true}
engine, recorder := newTaskEngine(scanstrategy.TemplateSpray, nil, test.down, resumeCfg)
var (
mu sync.Mutex
runs []string
)
var templatesList []*templates.Template
for _, path := range []string{"done.yaml", "todo.yaml"} {
templatesList = append(templatesList, &templates.Template{
ID: path,
Path: path,
TotalRequests: 1,
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
})
}
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf("a", "b"), true)
require.ElementsMatch(t, test.wantRuns, runs)
require.EqualValues(t, 4, recorder.total.Load(), "task total")
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
})
}
}
+343
View File
@@ -0,0 +1,343 @@
// Package techfilter maps Wappalyzer products/categories to template tags and
// decides whether a template can produce a finding on a fingerprinted host.
//
// Fail-open rules (coverage over aggressiveness):
// - templates with no product/macro tags always run
// - hosts with no fingerprints always allow every template
// - CDN/WAF-only fingerprints do not suppress product templates
// - non-HTTP templates are not filtered here (reachability owns that)
package techfilter
import (
"encoding/json"
"strings"
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
wappalyzer "github.com/projectdiscovery/wappalyzergo"
)
// genericTags are common nuclei tags that do not bind a template to a product.
var genericTags = map[string]struct{}{
"cve": {}, "cnvd": {}, "edb": {}, "kev": {}, "vkev": {},
"misconfig": {}, "exposure": {}, "config": {}, "disclosure": {},
"tech": {}, "detect": {}, "fingerprint": {}, "favicon": {}, "waf": {},
"http": {}, "network": {}, "dns": {}, "ssl": {}, "tls": {},
"websocket": {}, "headless": {}, "file": {}, "code": {}, "javascript": {},
"workflow": {}, "intrusive": {}, "dos": {}, "fuzz": {}, "token-spray": {},
"osint": {}, "panel": {}, "login": {}, "auth": {}, "unauth": {},
"default-login": {}, "vuln": {}, "rce": {}, "lfi": {}, "sqli": {},
"xss": {}, "ssrf": {}, "xxe": {}, "ssti": {}, "idor": {},
"critical": {}, "high": {}, "medium": {}, "low": {}, "info": {},
"unknown": {}, "generic": {}, "misc": {}, "bench": {},
// Meta / source / taxonomy tags that are not products.
"oast": {}, "takeover": {}, "deserialization": {}, "packetstorm": {},
"seclists": {}, "hackerone": {}, "huntr": {}, "wp-plugin": {}, "wp-theme": {},
"wordpress-plugin": {}, "wordpress-theme": {}, "authenticated": {},
"unauthenticated": {}, "bypass": {}, "injection": {}, "traversal": {},
"redirect": {}, "crlf": {}, "csrf": {}, "cors": {},
}
// weakFingerprintTags alone must not suppress product-bound templates
// (e.g. cloudflare/cdn-only fingerprints).
var weakFingerprintTags = map[string]struct{}{
"cdn": {}, "waf": {}, "misc": {}, "security": {}, "caching": {},
}
// productAliases normalize common nuclei / wappalyzer spellings to one token.
var productAliases = map[string]string{
"wp": "wordpress",
"httpd": "apache",
"apache-http-server": "apache",
}
// categoryMacros maps Wappalyzer category names to short macro tags.
var categoryMacros = map[string]string{
"cms": "cms",
"ecommerce": "ecommerce",
"web servers": "webserver",
"web-servers": "webserver",
"web frameworks": "web-framework",
"web-frameworks": "web-framework",
"javascript frameworks": "js-framework",
"javascript-frameworks": "js-framework",
"javascript libraries": "js-library",
"javascript-libraries": "js-library",
"cdn": "cdn",
"databases": "database",
"programming languages": "language",
"programming-languages": "language",
"paas": "paas",
"saas": "saas",
"security": "security",
"caching": "caching",
"miscellaneous": "misc",
}
// HostProfile is the set of product + macro tags inferred for one target.
type HostProfile struct {
Tags map[string]struct{} // empty / nil => unknown => fail-open
// Cats are the Wappalyzer category ids of every detected product. They
// record what the fingerprint actually has evidence about, which is what
// makes it safe to rule a template out. See AllowTags.
Cats map[int]struct{}
}
// HasTags reports whether fingerprinting produced at least one tag.
func (h HostProfile) HasTags() bool {
return len(h.Tags) > 0
}
// HasSubstantiveTags reports whether the profile has a strong category macro
// (cms, webserver, …). CDN/WAF-only fingerprints are not substantive.
func (h HostProfile) HasSubstantiveTags() bool {
for tag := range h.Tags {
if isStrongMacro(tag) {
return true
}
}
return false
}
func isStrongMacro(tag string) bool {
if _, weak := weakFingerprintTags[tag]; weak {
return false
}
for _, m := range categoryMacros {
if tag == m {
return true
}
}
return false
}
// NormalizeProduct turns a Wappalyzer / nuclei product name into a tag token.
func NormalizeProduct(name string) string {
name = strings.TrimSpace(strings.ToLower(name))
if name == "" {
return ""
}
if i := strings.IndexByte(name, ':'); i > 0 {
name = name[:i]
}
name = strings.ReplaceAll(name, "_", "-")
name = strings.Join(strings.Fields(name), "-")
if alias, ok := productAliases[name]; ok {
return alias
}
return name
}
// NormalizeCategory turns a Wappalyzer category into a macro tag.
func NormalizeCategory(cat string) string {
key := strings.ToLower(strings.TrimSpace(cat))
if key == "" {
return ""
}
if m, ok := categoryMacros[key]; ok {
return m
}
return strings.ReplaceAll(key, " ", "-")
}
// IsGenericTag reports whether tag is not product/macro binding.
func IsGenericTag(tag string) bool {
tag = strings.ToLower(strings.TrimSpace(tag))
if tag == "" {
return true
}
if _, ok := genericTags[tag]; ok {
return true
}
// cve2021 / cve-2021 style year tags bind no product.
if strings.HasPrefix(tag, "cve") {
rest := strings.TrimPrefix(tag, "cve")
rest = strings.TrimPrefix(rest, "-")
if rest != "" && isAllDigits(rest) {
return true
}
}
return false
}
func isAllDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// ProfileFromFingerprint builds a host profile from products and their categories.
// products maps product name -> category names from Wappalyzer.
func ProfileFromFingerprint(products map[string][]string) HostProfile {
tags := make(map[string]struct{})
observed := make(map[int]struct{})
byProduct := productCategories()
for product, cats := range products {
if p := NormalizeProduct(product); p != "" {
tags[p] = struct{}{}
for _, id := range byProduct[p] {
observed[id] = struct{}{}
}
}
for _, c := range cats {
if m := NormalizeCategory(c); m != "" {
tags[m] = struct{}{}
}
}
}
return HostProfile{Tags: tags, Cats: observed}
}
// TemplateProductTags returns product/macro tags that bind a template to a stack.
// Empty means the template is generic and must always be allowed.
func TemplateProductTags(t *templates.Template) []string {
if t == nil {
return nil
}
raw := t.Info.Tags.ToSlice()
out := make([]string, 0, len(raw))
seen := make(map[string]struct{}, len(raw))
known := knownFingerprintTokens()
for _, tag := range raw {
tag = NormalizeProduct(tag)
if tag == "" || IsGenericTag(tag) {
continue
}
// A tag binds only when fingerprinting can actually emit it. Anything
// else would never match a profile and would prune the template on
// every host, so it must leave the template unbound.
if _, ok := known[tag]; !ok {
continue
}
if _, ok := seen[tag]; ok {
continue
}
seen[tag] = struct{}{}
out = append(out, tag)
}
return out
}
// knownFingerprintTokens is every token a host profile can contain: the
// normalized name of every Wappalyzer app plus every category macro.
//
// This is an allowlist on purpose. nuclei-templates carries thousands of tags
// that name no product at all (discovery, wpscan, top-200, auth-bypass, ...).
// Treating an unrecognized tag as a product binds the template to something no
// fingerprint can ever report, which silently drops it on every host. Unknown
// tags must therefore bind nothing.
var knownFingerprintTokens = sync.OnceValue(func() map[string]struct{} {
var data struct {
Apps map[string]json.RawMessage `json:"apps"`
}
tokens := make(map[string]struct{}, 8192)
if err := json.Unmarshal([]byte(wappalyzer.GetFingerprints()), &data); err == nil {
for name := range data.Apps {
if p := NormalizeProduct(name); p != "" {
tokens[p] = struct{}{}
}
}
}
for _, macro := range categoryMacros {
tokens[macro] = struct{}{}
}
for tag := range weakFingerprintTags {
tokens[tag] = struct{}{}
}
return tokens
})
// IsTechBound reports whether the template is constrained to a product/macro.
func IsTechBound(t *templates.Template) bool {
return len(TemplateProductTags(t)) > 0
}
// Allow reports whether template may run on a host with the given profile.
// Fail-open when the host was not fingerprinted or the template is unbound.
func Allow(profile HostProfile, t *templates.Template) bool {
return AllowTags(profile, TemplateProductTags(t))
}
// AllowTags is Allow with precomputed product tags (avoids per-pair allocation).
func AllowTags(profile HostProfile, bound []string) bool {
if len(bound) == 0 {
return true
}
if !profile.HasTags() || !profile.HasSubstantiveTags() {
return true
}
for _, tag := range bound {
if _, ok := profile.Tags[tag]; ok {
return true
}
}
// No positive match. Ruling the template out now would assume the
// fingerprint is complete, and it never is: it is one page of one host, and
// a stack is routinely invisible from it (WordPress behind nginx is the
// ordinary case, not the exception). Absence of evidence is not evidence of
// absence, so a template is dropped only when the fingerprint positively
// contradicts it: something else was identified in the same category, and
// the template's own product was not. If any bound product sits in a
// category the fingerprint says nothing about, the template runs.
byProduct := productCategories()
for _, tag := range bound {
cats, ok := byProduct[tag]
if !ok || len(cats) == 0 {
return true // category macro or unknown product: no contradiction possible
}
contradicted := false
for _, id := range cats {
if _, seen := profile.Cats[id]; seen {
contradicted = true
break
}
}
if !contradicted {
return true
}
}
return false
}
// productCategories maps a normalized product token to its Wappalyzer category
// ids. It is the evidence index: a category id present in a host profile means
// fingerprinting actually identified something of that kind on that host.
var productCategories = sync.OnceValue(func() map[string][]int {
var data struct {
Apps map[string]struct {
Cats []int `json:"cats"`
} `json:"apps"`
}
out := make(map[string][]int, 8192)
if err := json.Unmarshal([]byte(wappalyzer.GetFingerprints()), &data); err != nil {
return out
}
for name, app := range data.Apps {
p := NormalizeProduct(name)
if p == "" || len(app.Cats) == 0 {
continue
}
// Aliases collapse several apps onto one token; union their categories.
out[p] = append(out[p], app.Cats...)
}
return out
})
// CountTechBound returns how many templates carry product/macro tags.
func CountTechBound(tpls []*templates.Template) int {
n := 0
for _, t := range tpls {
if IsTechBound(t) {
n++
}
}
return n
}
+208
View File
@@ -0,0 +1,208 @@
package techfilter
import (
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
)
func TestNormalizeProductAndCategory(t *testing.T) {
if got := NormalizeProduct("Nginx:1.25"); got != "nginx" {
t.Fatalf("NormalizeProduct = %q", got)
}
if got := NormalizeProduct("wp"); got != "wordpress" {
t.Fatalf("alias = %q", got)
}
if got := NormalizeCategory("Web servers"); got != "webserver" {
t.Fatalf("category = %q", got)
}
}
func TestProfileFromFingerprint(t *testing.T) {
p := ProfileFromFingerprint(map[string][]string{
"Nginx": {"Web servers"},
"WordPress": {"CMS"},
})
for _, want := range []string{"nginx", "webserver", "wordpress", "cms"} {
if _, ok := p.Tags[want]; !ok {
t.Fatalf("missing tag %q in %#v", want, p.Tags)
}
}
}
func tagged(tags ...string) *templates.Template {
return &templates.Template{
Info: model.Info{Tags: stringslice.StringSlice{Value: tags}},
}
}
func TestAllowFailOpenAndMatch(t *testing.T) {
nginx := ProfileFromFingerprint(map[string][]string{"Nginx": {"Web servers"}})
empty := HostProfile{}
generic := tagged("misconfig", "http")
wp := tagged("cve", "wordpress")
ngx := tagged("nginx", "misconfig")
cms := tagged("cms")
if !Allow(nginx, generic) {
t.Fatal("generic must always run")
}
if !Allow(empty, wp) {
t.Fatal("unknown host must fail-open for bound templates")
}
// An nginx fingerprint says nothing about the CMS behind it, and WordPress
// behind nginx is an ordinary stack. Absence of a CMS in the profile is not
// evidence that there is none.
if !Allow(nginx, wp) {
t.Fatal("nginx says nothing about a CMS, so a wordpress template must run")
}
if !Allow(nginx, ngx) {
t.Fatal("nginx template must run on nginx host")
}
if !Allow(nginx, cms) {
t.Fatal("nginx says nothing about a CMS, so a cms-bound template must run")
}
wpHost := ProfileFromFingerprint(map[string][]string{"WordPress": {"CMS"}})
if !Allow(wpHost, cms) {
t.Fatal("cms macro must match wordpress host")
}
if !Allow(wpHost, wp) {
t.Fatal("wordpress product must match")
}
}
func TestMetaTagsAreNotTechBound(t *testing.T) {
cases := [][]string{
{"cve", "cve2021", "oast", "rce"},
{"takeover", "dns"},
{"hackerone", "packetstorm", "seclists"},
{"wp-plugin", "wordpress-plugin"},
}
for _, tags := range cases {
tpl := tagged(tags...)
if IsTechBound(tpl) {
t.Fatalf("tags %v must not be tech-bound, got %v", tags, TemplateProductTags(tpl))
}
cdnOnly := ProfileFromFingerprint(map[string][]string{"Cloudflare": {"CDN"}})
if !Allow(cdnOnly, tpl) {
t.Fatalf("unbound meta tags %v must run on any host", tags)
}
}
}
func TestCDNOnlyFingerprintFailOpen(t *testing.T) {
cdnOnly := ProfileFromFingerprint(map[string][]string{"Cloudflare": {"CDN"}})
if cdnOnly.HasSubstantiveTags() {
t.Fatalf("cdn-only profile should not be substantive: %#v", cdnOnly.Tags)
}
wp := tagged("wordpress")
if !Allow(cdnOnly, wp) {
t.Fatal("CDN-only fingerprint must fail-open for product templates")
}
}
func TestCountTechBound(t *testing.T) {
n := CountTechBound([]*templates.Template{
tagged("misconfig"),
tagged("wordpress"),
tagged("nginx", "http"),
tagged("cve2023", "oast"),
})
if n != 2 {
t.Fatalf("CountTechBound = %d", n)
}
}
// A tag only binds when fingerprinting can emit it. nuclei-templates carries
// thousands of tags that name no product; binding to one of those prunes the
// template on every host, because no profile can ever contain it.
func TestOnlyKnownFingerprintTokensBind(t *testing.T) {
nonProducts := [][]string{
{"misconfig", "headers", "generic", "vuln"}, // http-missing-security-headers
{"tech", "discovery"}, // tech-detect
{"waf", "tech", "misc", "discovery"}, // waf-detect
{"wpscan"},
{"top-100"}, {"top-200"},
{"auth-bypass"}, {"time-based-sqli"},
{"install"}, {"files"}, {"file-upload"},
{"phishing"}, {"osint-social"},
{"debug"}, {"devops"}, {"token"}, {"tokens"}, {"fpd"}, {"oss"},
}
nginx := ProfileFromFingerprint(map[string][]string{"Nginx": {"Web servers"}})
for _, tags := range nonProducts {
tpl := tagged(tags...)
if got := TemplateProductTags(tpl); len(got) != 0 {
t.Fatalf("tags %v must not bind, got %v", tags, got)
}
if !Allow(nginx, tpl) {
t.Fatalf("unbound template %v must run on every host", tags)
}
}
}
// The inversion must not disable the filter: real products still bind, and a
// template for another stack is still pruned.
func TestKnownProductsStillBind(t *testing.T) {
for _, tags := range [][]string{{"wordpress"}, {"apache"}, {"joomla"}, {"cms"}} {
if got := TemplateProductTags(tagged(tags...)); len(got) == 0 {
t.Fatalf("tags %v name a product and must bind", tags)
}
}
wp := ProfileFromFingerprint(map[string][]string{"WordPress": {"CMS"}})
if !Allow(wp, tagged("wordpress", "rce")) {
t.Fatal("a wordpress template must run on a wordpress profile")
}
}
// A template is dropped only when the fingerprint contradicts it: it identified
// something else of the same kind and not the template's own product.
func TestPruneOnlyOnPositiveContradiction(t *testing.T) {
nginx := ProfileFromFingerprint(map[string][]string{"Nginx": {"Web servers"}})
wpHost := ProfileFromFingerprint(map[string][]string{
"WordPress": {"CMS"}, "Nginx": {"Web servers"},
})
// Contradiction: a web server was identified and it is not apache.
if Allow(nginx, tagged("apache")) {
t.Fatal("nginx was identified, so an apache template is contradicted")
}
// Contradiction: a CMS was identified and it is not joomla.
if Allow(wpHost, tagged("joomla")) {
t.Fatal("wordpress was identified, so a joomla template is contradicted")
}
// No evidence: nothing of that kind was identified at all.
for _, tags := range [][]string{{"wordpress"}, {"joomla"}, {"cms"}, {"mysql"}} {
if !Allow(nginx, tagged(tags...)) {
t.Fatalf("nginx gives no evidence about %v, so it must run", tags)
}
}
// One uncontradicted product is enough to keep a multi-product template.
if !Allow(nginx, tagged("apache", "wordpress")) {
t.Fatal("wordpress is uncontradicted, so the template must run")
}
// A positive match always wins.
if !Allow(wpHost, tagged("wordpress")) {
t.Fatal("a positive match must run")
}
}
func TestKnownFingerprintTokensVocabulary(t *testing.T) {
tokens := knownFingerprintTokens()
if len(tokens) < 5000 {
t.Fatalf("expected the wappalyzer vocabulary, got %d tokens", len(tokens))
}
for _, want := range []string{"wordpress", "nginx", "apache", "cms", "webserver"} {
if _, ok := tokens[want]; !ok {
t.Fatalf("token %q missing from the vocabulary", want)
}
}
for _, bad := range []string{"discovery", "headers", "wpscan", "top-200"} {
if _, ok := tokens[bad]; ok {
t.Fatalf("token %q is not a product and must not be in the vocabulary", bad)
}
}
}
+6 -2
View File
@@ -132,7 +132,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
}
for _, subtemplate := range matcher.Subtemplates {
swg.Add()
if err := swg.AddWithContext(ctx.Context()); err != nil {
return
}
go func(subtemplate *workflows.WorkflowTemplate) {
defer swg.Done()
@@ -161,7 +163,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
}
if len(template.Subtemplates) > 0 && firstMatched {
for _, subtemplate := range template.Subtemplates {
swg.Add()
if err := swg.AddWithContext(ctx.Context()); err != nil {
return err
}
go func(template *workflows.WorkflowTemplate) {
// create a new context with the same input but with unset callbacks
+57
View File
@@ -2,7 +2,9 @@ package core
import (
"context"
"sync/atomic"
"testing"
"time"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
@@ -301,6 +303,61 @@ func TestWorkflowsSameStepExecutersRefreshTemplateContext(t *testing.T) {
require.Equal(t, "foo", secondWhoami, "could not refresh workflow context into later executers in the same step")
}
func TestWorkflowSubtemplatesUnblockCancelledAcquire(t *testing.T) {
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
var started atomic.Int32
block := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
started.Add(1)
<-ctx.Context().Done()
}}
var waitingStarted atomic.Bool
waiting := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
waitingStarted.Store(true)
<-ctx.Context().Done()
}}
// Two slots: the two blockers occupy them so the third AddWithContext waits.
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 2}}, Workflows: []*workflows.WorkflowTemplate{
{Executers: []*workflows.ProtocolExecuterPair{{
Executer: &mockExecuter{result: true, outputs: []*output.InternalWrappedEvent{
{OperatorsResult: &operators.Result{}, Results: []*output.ResultEvent{{}}},
}}, Options: &protocols.ExecutorOptions{Progress: progressBar}},
}, Subtemplates: []*workflows.WorkflowTemplate{
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
{Executers: []*workflows.ProtocolExecuterPair{{Executer: waiting, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
}},
}}
engine := &Engine{}
parent, cancel := context.WithCancel(context.Background())
input := contextargs.NewWithInput(parent, "https://test.com")
ctx := scan.NewScanContext(parent, input)
done := make(chan struct{})
go func() {
defer close(done)
engine.executeWorkflow(ctx, workflow)
}()
deadline := time.After(2 * time.Second)
for started.Load() < 2 {
select {
case <-deadline:
t.Fatal("blocking subtemplates did not occupy both slots")
case <-time.After(10 * time.Millisecond):
}
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("workflow stayed blocked after cancel")
}
require.False(t, waitingStarted.Load(), "third subtemplate must not start while slots are held")
}
type mockExecuter struct {
result bool
executeHook func(input *contextargs.MetaInput)
+5 -1
View File
@@ -62,6 +62,10 @@ func (w *WorkPool) InputPool(templateType types.ProtocolType) *syncutil.Adaptive
}
func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
w.RefreshWithConfigContext(context.Background(), config)
}
func (w *WorkPool) RefreshWithConfigContext(ctx context.Context, config WorkPoolConfig) {
if w.config.TypeConcurrency != config.TypeConcurrency {
w.config.TypeConcurrency = config.TypeConcurrency
}
@@ -74,7 +78,7 @@ func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
if w.config.HeadlessInputConcurrency != config.HeadlessInputConcurrency {
w.config.HeadlessInputConcurrency = config.HeadlessInputConcurrency
}
w.Refresh(context.Background())
w.Refresh(ctx)
}
func (w *WorkPool) Refresh(ctx context.Context) {
+18 -14
View File
@@ -2,13 +2,12 @@ package customtemplates
import (
"context"
httpclient "net/http"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/google/go-github/v30/github"
"github.com/google/go-github/v92/github"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
@@ -89,7 +88,10 @@ func (customTemplate *customTemplateGitHubRepo) logPullError(err error) {
// NewGitHubProviders returns new instance of GitHub providers for downloading custom templates
func NewGitHubProviders(options *types.Options) ([]*customTemplateGitHubRepo, error) {
providers := []*customTemplateGitHubRepo{}
gitHubClient := getGHClientIncognito()
gitHubClient, err := github.NewClient()
if err != nil {
return nil, errors.Wrap(err, "could not create github client")
}
if options.GitHubTemplateDisableDownload {
return providers, nil
@@ -150,9 +152,16 @@ getRepo:
repo, _, err := gitHubClient.Repositories.Get(context.Background(), repoOwner, repoName)
if err != nil {
// retry with authentication
if gitHubClient = getGHClientWithToken(githubToken); gitHubClient != nil && !retried {
retried = true
goto getRepo
if !retried {
authClient, authErr := getGHClientWithToken(githubToken)
if authErr != nil {
return nil, errors.Wrap(authErr, "could not create authenticated github client")
}
if authClient != nil {
gitHubClient = authClient
retried = true
goto getRepo
}
}
return nil, err
}
@@ -233,20 +242,15 @@ func getAuth(username, password string) *http.BasicAuth {
return nil
}
func getGHClientWithToken(token string) *github.Client {
func getGHClientWithToken(token string) (*github.Client, error) {
if token != "" {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
oauthClient := oauth2.NewClient(ctx, ts)
return github.NewClient(oauthClient)
return github.NewClient(github.WithHTTPClient(oauthClient))
}
return nil
}
func getGHClientIncognito() *github.Client {
var tc *httpclient.Client
return github.NewClient(tc)
return nil, nil
}
Loaded 100 of 389 files, more files were not shown because too many files have changed in this diff. Show more