Compare commits

..
Author SHA1 Message Date
Mzack9999 f39605f83e chore: refresh security context 2026-11-07 10:29:45 +00:00
Muhammad Munib NawazandMzack9999 8cda906559 feat(js): add MQTT JavaScript library (#8075)
* feat(js): add mqtt javascript library

* fix(mqtt): address CodeRabbit feedback on security, limits, and robustness

* fix(mqtt): handle in-flight publish in ping, password without username, and staticcheck

* feat(mqtt): add InsecureSkipVerify option to ConnectOptions

* fix subscribe

* fix lint

* require tls12

---------

Co-authored-by: Mzack9999 <[email protected]>
2026-11-07 00:12:10 +01:00
KVR VishnuandMzack9999 a2e403a1d8 fix(core): avoid nested workflow deadlock at template concurrency limit (#8076)
* fix(core): avoid nested workflow deadlock at template concurrency limit

Release a workflow step's AdaptiveWaitGroup slot before acquiring slots
for its children. Matching nested parents previously held a slot while
blocked on AddWithContext, so when their count reached TemplateThreads
the workflow hung forever.

Fixes #8074

* fix workflow

---------

Co-authored-by: Mzack9999 <[email protected]>
2026-11-06 23:44:28 +01:00
Mzack9999 bbebeba8b2 Merge pull request #7974 from TassioSales/fix/json-output-nested-metadata-maps
fix(output): sanitize nested metadata maps before JSON encoding (#7971)
2026-11-06 17:56:10 +01:00
Mzack9999 578d00829a Merge pull request #8071 from projectdiscovery/dwisiswant0/fix/js/make-sshclient-close-a-no-op-without-a-connection
fix(js): isolate JS objects and handle missing connections
2026-11-06 17:56:03 +01:00
Mzack9999 3be47b098f Merge pull request #8072 from AdallomRoy/dev
Upgrade github.com/alecthomas/chroma to latest
2026-11-06 17:34:41 +01:00
Mzack9999 b29ecc6907 Merge pull request #8050 from antcybersec/fix/matcher-status-keeps-response
fix(http): keep the response in -ms failure events
2026-11-06 17:34:33 +01:00
Mzack9999 29559cbd2a Merge pull request #8052 from antcybersec/fix/header-values-keep-hash
fix(cmd): keep "#" in -H header values
2026-11-06 17:34:26 +01:00
Roy Reznik 94f7148b7e Upgrade github.com/alecthomas/chroma to latest 2026-11-06 11:47:53 +00:00
Dwi Siswanto 1829fca65c fix(js): make NetConn safe to use without a connection
NetConn had the same flaw as SSHClient. new net.NetConn() creates an
object with no connection, and every method dereferenced it, so Close,
Send and Recv all aborted the template with a nil pointer panic. Close
also kept the closed connection.

Close now does nothing when no connection is open, and clears the
connection after closing it. The send and receive methods return an
error that says to open the connection with net.Open or net.OpenTLS,
which goja throws as a JS exception the template can catch.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-06 09:40:26 +07:00
Dwi Siswanto accfe3551b fix(js): make SSHClient.Close a no-op without a connection
Calling Close on an SSHClient that never connected, or whose Connect
failed, dereferenced a nil *ssh.Client. goja turned the panic into a
request error, so the template stopped at Close and returned nothing.

Close also kept the closed client. On a client reused across login
attempts, a later Close then failed with "use of closed network
connection", and Run reported a transport error instead of "no
connection".

Close now returns true when no connection is open, and clears the
connection after closing it. It returns true rather than an error
because goja throws a returned error as a JS exception, which would
still abort the template.

Fixes: #8070
Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-06 09:40:26 +07:00
Dwi Siswanto d6ec8407fb fix(js): stop JS class constructors sharing one object
GetClassConstructor handed the same Go value to every object it
constructed, in every runtime. All generated JS classes use it, so
every new ssh.SSHClient() in a scan was one client: one script's
Connect or Close changed the connection that scripts on other targets
were using, and those writes raced.

Each constructed object now starts as a shallow copy of the instance
passed in. The generated bindings all pass a zero value, so every new
object starts empty.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-06 09:05:43 +07:00
Mzack9999 bb2da9bbea Use only template values for postgres conns (#7985)
* isolate credentials

* pin options
2026-11-06 03:45:59 +07:00
Mzack9999 6e2b59827c keep template screenshots in a nuclei dir (#8068)
* screenshot limits

* named path

* close comment
2026-11-06 03:30:17 +07:00
dw1 875b229313 fix(http): read annotations only before the request line (#8062)
Raw requests applied @Host, @tls-sni and @timeout from any matching
line of the rendered request, headers and body included. A response
value reused in a later request could carry an "@Host:" line and send
that request, with its body, to a host the template does not name.

The raw parser and the docs already define annotations as the "@"
lines before the request line. raw.SplitAnnotations now holds that
rule once, and the parser, the request annotations, @once and the
compile-time @timeout check all read from it.

Fixes: #8061

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-06 03:29:42 +07:00
Mzack9999 91f8bd1e4f send rdap requests through the scan dialer (#8066)
* rdap dialer

* assert redirect

* ipv6 literal
2026-11-06 03:29:04 +07:00
dw1 fb0eed5122 fix(http): flag truncated responses at the effective read limit (#8046)
The "(Truncated)" marker on dumped responses rebuilt the read-limit
rule from the template max-size and -rsr instead of using the limit
the body was read with. With neither set, the body is cut at the
10 MB default, but the dump was not marked truncated.

Comparing the body length with the effective limit keeps one rule for
both the read and the marker. c918d010c already made -rsr a ceiling
over a smaller template max-size; no release contains it yet, which
is why v3.8.0 still reads the full -rsr limit per response.

Fixes: #8045
Reported-by: @Lidar-Baruch

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-05 18:07:29 +07:00
Mzack9999 29833da25a send template doh queries through the scan dialer (#8064) 2026-11-05 18:06:41 +07:00
dw1 9b519d2264 fix(http): render self-contained raw requests once (#8059)
A self-contained raw request was rendered twice: once to read the URL
from its request line, and again with the variables that URL provides,
such as {{Hostname}}. The second pass evaluated values that the first
pass had inserted. A response body of {{secret}} reused in a later
request was sent as the template variable's value, and with -ev a
response body naming an environment variable was sent as its value.

Render the request line first and check its host. Then evaluate again
only the template variables and payloads whose template text names a
URL variable, and render the annotations and the rest of the request
once. Response data is now sent unchanged, as in templates that are
not self-contained. A value that changes which line is the request
line is rejected, so the host checked against the network policy is
the host that is sent.

Fixes: #8058

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-05 18:00:36 +07:00
dw1 4fed40f75a fix(fuzz): stop re-rendering evaluated template variables (#8057)
Fuzz rules rendered the template's variables once against request
data, then passed the output to the Interactsh render pass as template
text. A variable such as b: "{{body}}" therefore had the request body
substituted into it and was rendered a second time, so DSL-shaped bytes
in the incoming request were evaluated and the result was sent to the
target. With payloads, the same leak depended on map iteration order,
because that pass rendered each value against the map it was writing
to.

Treat rendered variable output as data, as the HTTP request builder
does, so only generator payload definitions remain template text.
Without payloads nothing is left to render, so the second pass is
dropped. Payload values now render against the unrendered input, which
makes the result independent of map iteration order. A variable that
references a payload now gets the payload's unrendered definition; it
only saw the rendered value through the second pass.

Fixes: #8054

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-05 18:00:12 +07:00
dw1 2f4ac3d9e5 fix(expressions): evaluate template expressions in place (#8056)
Evaluate substituted every placeholder into the whole text first, then
wrote each expression result over the first matching marker it found.
When a substituted value repeated an expression's text, such as a
response body of {{to_upper(seed)}}, the result went into that value
and the template's own expression was left unevaluated.

Replace had the same flaw in another form: its § pass ran over the
output of its {{}} pass, so a substituted value of §name§ resolved to
the value of name, and a value could pull in any other variable.

Both now make one left-to-right pass over the template text. Evaluate
locates each expression before anything is substituted and writes the
result at that position, and Replace never scans a value it inserted.
Replace also stops converting every value up front, so rendering is
faster and allocates less.

Fixes: #8053
Fixes: #8055
Refs: #7221

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-05 17:59:43 +07:00
Mzack9999 27ac1e8726 chore(deps): bump go-jose to v4.1.6 (#8037) 2026-11-05 13:37:53 +07:00
Mzack9999 5b499e0650 verify TLS and validate template_id for nuclei -ai (#8044) 2026-11-05 13:36:59 +07:00
anant 7748411200 fix(cmd): keep "#" in -H header values
Signed-off-by: anant <[email protected]>
2026-11-05 09:59:47 +05:30
anant 0642f05241 fix(http): keep the response in -ms failure events
Signed-off-by: anant <[email protected]>
2026-11-05 09:59:35 +05:30
Mzack9999 df707d924f Verify TLS for cloud uploads (#8048) 2026-11-05 06:45:55 +07:00
Mzack9999 5f5a40c2d4 Merge pull request #8040 from projectdiscovery/feat/js-ipmi
add ipmi javascript library
2026-11-04 22:12:54 +01:00
Mzack9999 82d5a8acc2 exporters: verify TLS certificates (#8042) 2026-11-04 16:04:54 +07:00
Dogan Can Bakir 9d3be8b22e verify bmc proofs and signed replies, match replies to requests in the ipmi library 2026-11-04 10:52:52 +03:00
Dogan Can Bakir 76dd059821 add ipmi javascript library 2026-11-04 09:26:50 +03:00
Mzack9999 46fbc92a76 Merge pull request #8023 from projectdiscovery/fix/dns-resolver-client
Give each scan its own DNS resolvers
2026-11-03 21:42:45 +01:00
Mzack9999 36d0adba18 Merge pull request #8024 from projectdiscovery/fix/headless-exclude-lock
Read headless excludes under the dialer lock
2026-11-03 20:47:14 +01:00
Mzack9999 049e6b629e Merge pull request #8018 from projectdiscovery/feat/jev-llm-provider
use jev for llm matchers when -llm-provider jev
2026-11-03 19:21:59 +01:00
Dogan Can Bakir 2c6b223bb4 Merge pull request #7983 from projectdiscovery/headless-host-canonical
Deny IPv4-mapped forms of denied hosts in the dialer
2026-11-03 20:45:05 +03:00
Mzack9999 0827b2779b preserve proxy 2026-11-03 18:34:18 +01:00
Mzack9999 0971fb83e2 skip extractors 2026-11-03 18:07:38 +01:00
Mzack9999 62dae92b3d refresh resolvers 2026-11-03 17:30:04 +01:00
Mzack9999 e44df6e947 lock excludes 2026-11-03 17:30:04 +01:00
Mzack9999 25f50f1036 Merge pull request #8012 from TassioSales/fix/clean-flow-js-runtime
fix(flow): prevent global state leakage in shared JS runtime pool (#7986)
2026-11-03 17:13:36 +01:00
Mzack9999 f9446e9290 Merge pull request #8016 from projectdiscovery/dwisiswant0/fix/headless/fail-requests-whose-url-net-url-cannot-parse
fix(headless): fail requests whose URL net/url cannot parse
2026-11-03 17:12:37 +01:00
Mzack9999 faaeaf30b1 mapped deny 2026-11-03 17:07:35 +01:00
Mzack9999 b7fe335285 merge dev 2026-11-03 17:02:03 +01:00
Mzack9999 7481b20d78 Merge pull request #8014 from TassioSales/fix/headless-ipv4-mapped-ipv6-exclude-hosts-7982
fix(headless): normalize IPv4-mapped IPv6 host forms for -exclude-hosts and -lna (#7982)
2026-11-03 13:39:20 +01:00
Mzack9999 48600a24de use canonical 2026-11-03 12:51:48 +01:00
Mzack9999 ec4036147e Merge pull request #8013 from 0xABCD01/fix/govaluate-concat-regression-test
test(expressions): add regression case for govaluate concat bug (follow-up to #7998)
2026-11-03 12:51:11 +01:00
Mzack9999 9b04153a3b merge dev 2026-11-03 12:45:38 +01:00
Dogan Can Bakir 23f3407af7 bump utils for the jev provider 2026-11-03 12:50:46 +03:00
Dwi Siswanto be40690a29 fix(headless): fail requests whose URL net/url cannot parse
With a request modification action (setheader, addheader, deleteheader,
setmethod or setbody), routingRuleHandler replays every browser request
through nuclei's Go HTTP client. go-rod builds that request with
url.Parse and drops the error, so a URL that Chrome accepts but net/url
rejects, such as a path ending in "100%.png", arrived with a nil URL.
The cookie jar sync then panicked, or httputil.DumpRequestOut did when
cookies were disabled, and the panic killed the whole scan.

Check the URL once on entry and fail such requests in the browser. The
Go client cannot send them, and continuing them in the browser would
drop the template's modification rules and, with -cdp-endpoint, skip
the dial-time address checks of nuclei's network policy.

Fixes: #8015
Reported-by: @l4rm4nd
Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-03 16:13:37 +07:00
Dogan Can Bakir 64f94f3e66 use jev for llm matchers when -llm-provider jev 2026-11-03 12:04:47 +03:00
TassioSales b2b54a7ac4 fix(headless): preserve port in CanonicalizeHost and stabilize proxy test
Signed-off-by: TassioSales <[email protected]>
2026-11-03 01:28:54 -03:00
TassioSales 22aedebe5d fix(headless): normalize IPv4-mapped IPv6 host forms for -exclude-hosts and -lna (#7982)
Normalize hostnames and IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1,
::ffff:7f00:1, and bracketed variants) before validating against NetworkPolicy
and ExcludeTargets in headless execution. Also expand CDP blocked patterns
in SetupHeadlessNetworkPolicy to match mapped IPv6 representations.

Signed-off-by: TassioSales <[email protected]>
2026-11-03 01:07:02 -03:00
Hafiar 5896509c84 test(expressions): add regression case for govaluate concat bug
base64(username + ':' + password) evaluated to base64 of "[admin:]admin"
on govaluate v0.0.0-20260729205521-4c61bf19382e, the pin #7998 started
from. The case was requested by @dogancanbakir in review of #7998 and did
not land before the merge.
2026-11-02 23:42:37 +00:00
Mzack9999 825dcfb10a lock runtime 2026-11-02 22:58:36 +01:00
Mzack9999 d465cc82a4 Merge pull request #7995 from KaemiaDeveloper/fix/ipv6-dual-stack-target-selection
Scan every address family with -sa
2026-11-02 22:36:42 +01:00
Mzack9999 48f36b4edc keep fallback 2026-11-02 21:47:23 +01:00
Mzack9999 0e7a56b0d7 prefer ipv6 2026-11-02 21:31:52 +01:00
TassioSales f8cf960022 fix(flow): prevent global state leakage in shared JS runtime pool (#7986)
Always replenish the JS runtime pool with a fresh, clean runtime after flow
template execution instead of reusing contaminated runtimes.

Signed-off-by: TassioSales <[email protected]>
2026-11-02 16:19:52 -03:00
Mzack9999 cbd2cb3881 Merge pull request #8010 from Ayham970/websocket-subprotocol-support
websocket: negotiate Sec-WebSocket-Protocol via the dialer
2026-11-02 19:53:04 +01:00
Mzack9999 1fc87e6b02 Merge pull request #7998 from 0xABCD01/fix/bump-panicking-deps
fix: bump govaluate and antchfx/xpath so malformed expressions error instead of panicking
2026-11-02 19:00:10 +01:00
Mzack9999 7a08c714f9 Merge pull request #8009 from projectdiscovery/dwisiswant0/fix/templates/keep-eid-exclusions-independent-of-etags
fix(templates): keep -eid exclusions independent of -etags
2026-11-02 18:59:46 +01:00
Ayham970andClaude Opus 4.8 6903f2d559 websocket: reject CR/LF in subprotocols; require negotiation in test
Address review feedback:
- Skip Sec-WebSocket-Protocol values containing CR or LF before negotiating
  them, since gobwas/ws writes subprotocols into the handshake request line and
  a line break could smuggle extra handshake headers.
- Tighten the test server to reject the upgrade unless the client offers the
  vite-hmr subprotocol, so the test fails if negotiation does not happen.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-11-02 17:22:43 +04:00
Ayham970andClaude Opus 4.8 27edbd6a64 websocket: negotiate Sec-WebSocket-Protocol via the dialer
The websocket protocol sent a Sec-WebSocket-Protocol header as a raw handshake
header but never set gobwas/ws Dialer.Protocols. gobwas validates the
server-selected subprotocol against Dialer.Protocols and rejects one that was
only sent as a header, so any server that requires and echoes a subprotocol
(e.g. a Vite HMR dev server advertising "vite-hmr") failed the handshake with
"unexpected protocol in Sec-WebSocket-Protocol header".

Route a Sec-WebSocket-Protocol request header into Dialer.Protocols (comma
splitting, trimming) instead of the raw header map, so the subprotocol is
offered in the handshake and the server's echo is accepted. Adds a regression
test with a server that only upgrades when "vite-hmr" is negotiated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-11-02 17:12:17 +04:00
Dwi Siswanto be678d9d7c fix(templates): keep -eid exclusions independent of -etags
NewTagFilter added an -eid value to the excluded IDs only when -etags
did not list the same value. The check came from the tag loops in
#1448, which changed the map it writes but not the map it reads. With
-eid dup -etags dup, the filter excluded nothing by ID. Workflow steps
still go through this filter, so a workflow ran the template that a -t
scan with the same flags excluded.

Build each filter set from its own flag with one helper, so no set
reads another flag's values. The insert-if-absent checks go too:
writing an existing map key is a no-op, and the one check that changed
anything was the wrong one. -itags stops deleting from the -etags set,
because Match already lets an included tag override its exclusion.

Fixes: #8004
Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-02 19:33:26 +07:00
dw1 515b22a35a Merge pull request #8006 from projectdiscovery/dwisiswant0/fix/workflow/keep-exclusions-from-silently-emptying-workflows
fix(workflow): keep exclusions from silently emptying workflows
2026-11-02 13:13:51 +01:00
github-actions[bot]andehsandeep cd0122aa85 chore: refresh security context (#7996)
Co-authored-by: ehsandeep <[email protected]>
2026-11-02 19:09:40 +07:00
Dwi Siswanto b7d22eb52b fix(workflow): warn when a step's subtemplates cannot run
A step that loads no templates never matches, so the subtemplates
loaded below it never run. Nothing said so when an exclusion emptied
the step: a tags step dropped the filter's error, and a template step
warned only for an excluded tag, naming the template but not the
subtemplates it held back, and only with -v, since warnings are
hidden by default. Such steps are now counted in a warning shown after
loading, and -v logs each one with its workflow.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-02 18:33:15 +07:00
Dwi Siswanto c2cecb92a8 fix(workflow): skip -es for steps whose subtemplates loaded
A step whose subtemplates loaded applied every exclusion, -es included.
Most workflows start from a detection template with info severity, so
-es info, often used to hide low-value results, left them running
nothing, while -s low,medium,high,critical, which selects the same
results, still ran them. A step with subtemplates reports no results of
its own beyond late out-of-band matches, so excluding its severity
mostly keeps the checks below it from running. Such a step now skips
-es as it skips -s.

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-02 18:33:15 +07:00
Codess 9206560d1d style(test): align regression test provider fixture 2026-11-02 12:01:06 +01:00
Codess e94840ad56 test(input): initialize exclusion map and scope DNS fixtures to test hosts 2026-11-02 11:59:33 +01:00
Dwi Siswanto aec0b6eebf fix(workflow)!: load tags steps when a subtemplate passes the filters
With a template filter set, a workflow step selected by tags loaded only
the templates that had its tags and passed the filter. When the filter
selected one of the step's subtemplates instead, the step loaded
nothing, and since subtemplates run only after their step matches, the
selected subtemplate never ran. Steps selected by template already load
for a loaded subtemplate (#7965), but the tag lookup never got that
signal, and it matched the step's tags and the filters in a single call,
so it could not skip the filters without also dropping the tags.

The tag lookup now selects templates by the step's tags first and then
filters them. A step loaded for its subtemplate used to skip every
filter that rejected a template without an error, so -eid, -es and -ept
did not apply to it while -etags and -et did. Both lookups now apply
only the exclusion filters to such a step, so every exclusion holds, as
it does against -itags since #7536.

The tag filter also let -id lift -eid for the same template, so a
workflow ran a template that a scan with the same flags skips. -eid now
wins, as in the metadata index filter.

BREAKING-CHANGE: WorkflowLoader.GetTemplatePathsByTags takes noValidate
Fixes: #7966
Refs: #7534
Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-02 17:24:37 +07:00
Mzack9999 b6dd16ffbf fix concat 2026-11-02 11:23:21 +01:00
Mzack9999 d510a9c2fb Merge pull request #8003 from projectdiscovery/feat/har-input
add har input mode
2026-11-02 11:11:44 +01:00
Mzack9999 de382b5de3 Merge pull request #7994 from projectdiscovery/feat/auto-calibrate
drop HTTP matches on a host's catch-all page with -auto-calibrate
2026-11-02 11:10:23 +01:00
Mzack9999 7ed324680f Merge pull request #8000 from projectdiscovery/fix/tl-jsonl
honour -jsonl in -tl and drop the listing banner from json output
2026-11-02 11:09:12 +01:00
Dogan Can Bakir 684d55eeed add har input mode 2026-11-02 09:31:10 +03:00
Doğan Can Bakır 4dcb37ad23 honour -jsonl in -tl and drop the listing banner from json output 2026-11-02 08:23:57 +03:00
Hafiar ab506a2bb6 fix: bump govaluate and antchfx/xpath so malformed expressions error instead of panicking 2026-11-02 12:18:58 +07:00
TassioSales 6863ab16e5 fix(output): sanitize nested metadata maps before JSON encoding (#7971)
Templates containing nested maps or lists of maps in info.metadata produce yaml.v2 map[interface{}]interface{} collections during decoding. When formatting JSON output (-j / -jsonl), JSON encoders (sonic / encoding/json) fail on interface{} map keys with 'json: unsupported type: interface {}', causing Nuclei to silently drop results and report 0 matches found.

Sanitize nested metadata maps recursively to map[string]interface{} prior to JSON serialization so all matching results are written.

Fixes #7971.

Signed-off-by: TassioSales <[email protected]>
2026-11-01 22:03:25 -03:00
Codess 7b44856ce8 fix(input): avoid duplicate scans for automatic dual-stack targets 2026-11-01 21:52:20 +01:00
Codess 288f657fca fix(input): preserve IPv4 fallback when IPv6 is unreachable 2026-11-01 21:38:50 +01:00
Codess 87a8c61932 fix(input): select IPv6-only and dual-stack targets without -iv 2026-11-01 21:25:39 +01:00
Doğan Can Bakır 9c26dc1d2a document -auto-calibrate 2026-11-01 18:54:08 +03:00
Doğan Can Bakır 613e581542 calibrate against the host root and cover the http hook 2026-11-01 18:53:48 +03:00
Doğan Can Bakır 5c9f02a4fc drop http matches on a host's catch-all page with -auto-calibrate 2026-11-01 18:35:12 +03:00
ghost df57ae6d29 chore(js): update memoized functions 🤖 2026-10-30 16:11:04 +00:00
Mzack9999 2a5392eaad Merge pull request #7988 from gnuletik/fix/js-redis-reply-scan
fix(js): check redis reply headers in linear time, skip payloads
2026-10-30 17:10:36 +01:00
Mzack9999 bdf7e983c5 Merge pull request #7990 from IBazsecI/fix/openapi-swagger-nil-panics
fix(openapi,swagger): skip null spec entries instead of panicking
2026-10-30 16:58:25 +01:00
bazsec 6a3300b13d fix(openapi,swagger): drop null response headers, reject only-null servers
Address review feedback:

- swagger: a null entry under a response's `headers:` map is also
  dereferenced by openapi2conv.ToV3Response, for both operation and
  top-level responses. Drop those along with null responses.
- openapi: a `servers:` list containing only null entries now returns
  the same "no servers found" error as an empty list, instead of
  succeeding without generating any request.
2026-10-30 15:18:06 +03:00
bazsec 266593d7a0 fix(openapi,swagger): skip null spec entries instead of panicking
A null value in an OpenAPI or Swagger document (an empty list item, or a
map key with no value) decodes to a nil pointer. Several of these were
dereferenced without a check, so loading such a spec with -im openapi or
-im swagger crashed nuclei with SIGSEGV.

openapi (generator.go):
- a null entry under `servers:` -> nil *Server
- a null media type under `content:` -> nil *MediaType

swagger (dereferenced inside kin-openapi's openapi2conv.ToV3):
- a null path item under `paths:` -> nil *PathItem
- a null item under path/operation `parameters:` or a null top-level
  parameter -> nil *Parameter
- a null operation or top-level response -> nil *Response

Skip the nil entries in the generator, and drop them from the decoded
Swagger document before converting it. Other null entries are already
rejected by kin-openapi with an error and are left alone.

Also add a go-fuzz harness for the OpenAPI parser with a seed corpus, so
the weekly fuzz workflow picks it up, and regression tests that panic on
the pre-fix code.
2026-10-30 14:54:30 +03:00
gnuletikandClaude Opus 5.5 7b0ca00fdf fix(js): check redis reply headers in linear time, skip payloads
The read cap scanned a reply line by line and copied what was left of
its buffer for every line, so a 1 MiB bulk reply of newlines allocated
17 GiB. It also checked lines inside bulk payloads as if they were
headers, so a Lua script returning "a\n*abc" failed with "reply exceeds
16777216 bytes".

Follow the RESP framing as bytes pass instead: skip a bulk payload by
its declared length and check only header lines. A rejected header's
read now ends before its "\n", so go-redis never gets a line to
allocate from, and nothing is buffered or copied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 11:56:54 +01:00
Mzack9999 2e258fc3f5 Merge pull request #7981 from kamilikamil/js-streaming-hash
Add JS modules for streaming hash and decompression
2026-10-29 22:06:48 +01:00
Mzack9999 b3596ac64e dedupe imports 2026-10-29 21:33:09 +01:00
Mzack9999 ac52212d17 canonicalize headless 2026-10-29 21:25:10 +01:00
Mzack9999 1f79f786da Merge pull request #7975 from antcybersec/fix/exclude-hosts-file-comments
fix(cmd): ignore comments in -exclude-hosts values
2026-10-29 17:17:04 +01:00
Mzack9999 3ab77ec3e7 Merge pull request #7972 from arththebird/fix/json-output-nested-yaml-maps
fix(output): encode nested template maps in JSON output
2026-10-29 16:34:01 +01:00
anant 58354693e0 fix(cmd): ignore comments in -exclude-hosts values 2026-10-29 21:02:44 +05:30
Mzack9999 dc817c8dbd Merge pull request #7970 from Eren-Akdag/fix/7969-httpx-probe-resolvers
fix(runner): use -resolvers for the httpx probe of non-URL inputs (#7969)
2026-10-29 16:30:54 +01:00
Arthur LoiselleandClaude Opus 5.5 e55509d881 fix(output): reject metadata keys that collide after string conversion
Keys of different types can format to the same string (`1` and `1.0`), and
keeping either value would depend on map iteration order. `ToJSONValue` and
`ToJSONMap` now return an error on such a collision, which both
`MarshalJSON` methods propagate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 11:04:42 -04:00
Mzack9999 998641728c exclude context 2026-10-29 15:57:32 +01:00
Mzack9999 4e3f78c2c0 exclude context 2026-10-29 15:57:32 +01:00
Mzack9999 7a9ec9c8da Merge pull request #7912 from gnuletik/fix/js-redis-dial-once
fix(js): dial a redis connection once and log its failures as warnings
2026-10-29 15:50:11 +01:00
Mzack9999 5d876b88e5 Merge pull request #7965 from antcybersec/fix/workflow-subtemplate-filters
fix(templates): load workflow steps only when they or a subtemplate pass the filters
2026-10-29 15:50:03 +01:00
Kamil Kamili 7d143c989a Trim comments in the nuclei/hash and nuclei/compress modules 2026-10-29 17:56:02 +04:00
Kamil Kamili aa45ef92f6 Test streamed hashes against the HTTP engine's decoded body
The streamed sha256 and mmh3 of a response read over nuclei/net must
equal sha256(body) and mmh3(body) on the body the HTTP engine's response
chain decodes, for each Content-Encoding, with and without chunked.
2026-10-29 17:29:34 +04:00
Kamil Kamili 7f57e92074 Decode chunked transfer coding in nuclei/compress
NewDecoder now takes a comma-separated list of codings in the order they
were applied, as HTTP headers list them, and accepts chunked. Finished
reports when a self-delimiting stream has ended, so a template reading a
chunked body knows when to stop calling Recv.
2026-10-29 17:29:33 +04:00
Arthur LoiselleandClaude Opus 5.5 3ae6ca8e3d fix(output): encode nested template maps in JSON output
Template YAML decodes nested maps as `map[interface{}]interface{}`, which
the JSON encoder rejects, so a result whose `info.metadata` (or javascript
args carried in `meta`) held a map was dropped from `-j`/`-jsonl` output.
`model.Info` and `output.ResultEvent` now encode their metadata with
string-keyed maps.

Closes #7971

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:21:08 -04:00
Kamil Kamili 6c615b76df Add nuclei/compress JS module for streaming decompression
A javascript template reading a compressed response with Recv cannot
decode it without first holding the whole body. compress.NewDecoder
takes a Content-Encoding the HTTP engine decodes (gzip, deflate, br,
zstd, or identity) and decodes the chunks written to it, handing back
up to about 1 MB per call so a highly compressed chunk never has to be
held whole.

Go's decompressors pull from a reader while templates push chunks, so
each decoder runs its decompressor on a goroutine that pauses whenever
it needs input or has 1 MB to hand back. The goroutine exits when the
script's context ends, so an abandoned decoder does not leak.
2026-10-29 16:00:39 +04:00
Kamil Kamili 681854444d Add nuclei/hash JS module for incremental hashing
A javascript template that reads a large response with Recv has to
hold the whole body to compare it, since only the DSL hash functions
exist and they take the full input. hash.New(algorithm) returns a
running hash: Write and WriteString add chunks and Sum returns the
value so far, in the same format as the DSL md5, sha1, sha256, sha512
and mmh3 functions. Memory does not grow with the data hashed.
2026-10-29 16:00:39 +04:00
Eren-Akdag 449fdd2c37 test(runner): clear proxy env in the httpx probe resolver test
The probe client uses http.ProxyFromEnvironment, so a proxy set in the
environment could route the probe away from the local test server when
the test runs on its own.
2026-10-29 14:58:05 +03:00
Eren-Akdag 9d7aa53226 fix(runner): use -resolvers for the httpx probe of non-URL inputs
The httpx client that probes non-URL inputs for http or https was built
from httpx.DefaultOptions without the configured resolvers, so the probe
resolved inputs through fastdialer's default resolvers (resolv.conf plus
1.1.1.1, 1.0.0.1, 8.8.8.8 and 8.8.4.4) even when -resolvers was set.

Pass InternalResolversList to httpxOptions.Resolvers in the runner and in
the SDK. httpx then uses only those resolvers. Without -resolvers the
probe behaves as before.

Closes #7969
2026-10-29 14:37:28 +03:00
dependabot[bot] 3ee4f0f408 chore(deps): bump github.com/getkin/kin-openapi (#7968)
Bumps the go_modules group with 1 update in the / directory: [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi).


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

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.149.0
  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-10-29 04:37:22 +00:00
ghost 091191f518 docs: update syntax & JSON schema 🤖 2026-10-29 04:05:50 +00:00
dependabot[bot] 827f4a9f88 chore(deps): bump the modules group with 2 updates (#7942)
Bumps the modules group with 2 updates: [github.com/projectdiscovery/dsl](https://github.com/projectdiscovery/dsl) and [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo).


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

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

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/dsl
  dependency-version: 0.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.3.6
  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-29 11:00:17 +07:00
Mzack9999 bd1c4dd226 feat(headless): expose header/cookie DSL variables (#7573)
* header variables

* Fix review

* Browser cookies

* Reserve keys

* Host cookies
2026-10-29 10:59:17 +07:00
github-actions[bot]andMzack9999 f990fe4d5d chore: refresh security context (#7693)
Co-authored-by: Mzack9999 <[email protected]>
2026-10-29 10:57:39 +07:00
Mzack9999 00ed0552da feat(js): Apply network policy to mysql ConnectWithDSN (#7768)
* enforce policy

* Fix review
2026-10-29 10:57:00 +07:00
Mzack9999 099a037928 feat(headless): chrome sandbox root-only (#7951) 2026-10-29 10:55:52 +07:00
Mzack9999 12d6f03826 test: isolate resolvers (#7952) 2026-10-29 10:53:53 +07:00
anant 1644ba7e81 fix(templates): load workflow steps only when they or a subtemplate pass the filters 2026-10-29 04:40:36 +05:30
Mzack9999 a113cf0db8 Merge pull request #7962 from je-bugshell/feat/no-redact-flag
Add -no-redact to disable the built-in credential redaction
2026-10-28 18:12:43 +01:00
Mzack9999 97337e2b4a update readmes 2026-10-28 17:41:18 +01:00
Mzack9999 8d29c12eef fix redaction 2026-10-28 16:36:38 +01:00
John Eric von Rosen 4fc17dc284 Keep a semicolon inside a redacted query value
Stopping a key=value match at every ; left ?password=;s3cr3t unmasked, since
the value could not start. The value is now lazy and ends only at a cookie
delimiter (; name=), &, a quote or a line break.
2026-10-28 13:09:35 +01:00
John Eric von Rosen 035fc270f9 Keep neighbouring cookies and query parameters when redacting one key
A redacted value used to run to the end of the line, so -redact tracking on
Cookie: tracking=1; session=abc masked the session cookie too. A key=value
match now stops at ; or &; a Key: header is still masked whole. The test also
covers the matched URL.
2026-10-28 13:00:32 +01:00
John Eric von Rosen 3f341f0222 Add -no-redact to disable the built-in credential redaction
Since #7469 the output writer always masks Authorization, Proxy-Authorization,
Cookie, Set-Cookie and X-Api-Key, which leaves exported findings without the
data needed to reproduce them by hand. -no-redact turns the built-in list off;
keys given with -redact are still masked.
2026-10-28 12:35:18 +01:00
Mzack9999 26ce6d2ef2 Merge pull request #7959 from arxhr007/fix/7949-validate-missing-path
fix(loader): fail -validate when a template path can't be found
2026-10-27 18:13:16 +01:00
Mzack9999 0f34b1b6b8 Merge pull request #7960 from projectdiscovery/fix/7957-exclude-hosts-match
fix exclude-hosts matching unrelated hosts at dial time
2026-10-27 17:03:18 +01:00
Doğan Can Bakır 77ba7792f3 keep subdomain and regex exclude-hosts entries 2026-10-27 18:37:21 +03:00
Doğan Can Bakır 8caa65a839 match exclude-hosts entries exactly at dial time 2026-10-27 17:58:24 +03:00
arxhr007 a4afaa41b2 fix(loader): fail -validate when a template path can't be found
`nuclei -validate -t /no/such/path` logged "Could not find template" and
then still printed "All templates validated successfully" and exited 0,
so a mistyped path in a CI step passed. ValidateTemplates ignored the
paths that GetTemplatesPath could not resolve.

Count the paths that aren't handled by NotFoundCallback and return the
validation error when there is any. Paths the callback resolves are
still accepted.

Fixes #7949
2026-10-27 19:59:58 +05:30
Mzack9999 abcdcf19a4 Merge pull request #7940 from mazzz3r/feat/info-placeholders
feat(templates): fill placeholders in info description, impact and remediation
2026-10-27 15:21:09 +01:00
Michael Rodger 9dc004926d fix(trackers): gitea reporter pagination (#7945) 2026-10-27 14:11:01 +07:00
Mzack9999 3d2f411e1d Merge pull request #7925 from projectdiscovery/dwisiswant0/fix/offlinehttp/read-status-lines-by-position-not-text-search
fix(offlinehttp): read status lines by position, not text search
2026-10-26 21:38:57 +01:00
Mzack9999 763e490f42 merge dev 2026-10-26 21:10:10 +01:00
Mzack9999 421ec70686 Merge pull request #7947 from projectdiscovery/dwisiswant0/fix/trackers/follow-x-next-page-in-gitlab-duplicate-search
fix(trackers): follow X-Next-Page in gitlab duplicate search
2026-10-26 20:47:55 +01:00
gnuletikandClaude Opus 5.5 1eeb2f3979 fix(js): log go-redis messages as gologger warnings
Rather than disabling go-redis's logger, route its messages through
gologger at warning level. nuclei's CLI then shows them with -v or -debug
only, and a program embedding nuclei receives them through its gologger
writer, instead of go-redis writing to stderr on its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:05:37 +01:00
gnuletikandClaude Opus 5.5 ea108ee04f fix(js): dial a redis connection once and keep go-redis off stderr
go-redis v9.19 retries a failed dial 5 times, 100ms apart, from a goroutine
that dials on its own context rather than the caller's. Against a filtered
port, a JS call returned at its execution deadline while the pool kept
dialing for ~25s; against a closed port, one call made 20 dials and took
1.7s. Each exhausted dial also printed "connection pool: failed to dial after
5 attempts" to stderr.

The redis clients now dial once per command attempt, as go-redis v9.11 did,
and go-redis's logger is disabled, as the mysql lib does for its driver.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 20:05:37 +01:00
Dwi Siswanto aac1427cdc fix(trackers): follow X-Next-Page in gitlab duplicate search
The GitLab tracker ended the duplicate search at the first page holding
fewer issues than duplicate-issue-page-size. GitLab caps per_page at
100, so with a page size above 100 every page looked like the last one.
The tracker read only the first 100 search results and opened a new
issue when the match came later.

The client library already parses X-Next-Page into Response.NextPage.
Follow it instead of guessing from the page length. This also drops the
empty-page request the old check sent when the last page was full.

Fixes: #7946
Refs: #7944
Signed-off-by: Dwi Siswanto <[email protected]>
2026-10-26 21:10:51 +07: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
mazzz3r c4552c6011 feat(templates): fill placeholders in info description, impact and remediation
Description, impact and remediation can reference values of the finding
with the usual {{...}} syntax: named extractors, payload values of the
matching request, variables, DSL helpers, and the result fields host,
port, ip, url, scheme, path, matched-at, matcher-name and
extractor-name, which win over values of the same name. Name, severity,
tags and classification stay static, since filtering, grouping and
issue trackers key on them.

MakeDefaultResultEvent renders each result's copy of the info, so every
protocol gets it and the template never changes. Each field goes
through render.Render without scan options, like request and matcher
text, so nested placeholders and helpers behave the same and network
helpers stay denied. A field whose expression fails keeps the
template's text.

When placeholders change the text, the result keeps the written info in
TemplateInfo (json:"-" bson:"-"):
- the writer runs the -redact patterns over the changed fields, as it
  does over the raw request and response;
- SARIF builds the shared rule from the written info and puts each
  finding's filled text, capped at 4096 characters, in its result
  message.

Info is filled only from a template's own event. Templates with info
placeholders are not clustered, since a cluster's shared event holds the
first template's variables and extracted values, and a global matcher
fills its info from the result fields alone.
2026-10-25 19:37:42 +00: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
Dwi Siswanto 4331cd0f7a fix(offlinehttp): read status lines by position, not text search
Passive mode added a minor version to status lines such as "HTTP/2 200
OK" by rewriting the first "HTTP/<digit> " anywhere in the raw data. In
a valid HTTP/1.1 response that text could sit in a header, the reason
phrase, or the body, which was then altered: a Content-Length body came
out truncated and a chunked one failed to decode. Only the protocol
token at the start of the response is normalized now.

When the data starts with a request, the response is now read from
where the request body ends, with the same normalization. That path
used to reject HTTP/2 status lines and read a request body as the
status line, then fall back to the last "HTTP/" in the data, which
fails whenever the response body contains one. A dump that keeps a
request's Content-Length but leaves out its body is retried right after
the headers. Two exchanges back to back now yield the first response
for HTTP/2, as they already did for HTTP/1.1.

Up to five interim 1xx responses are skipped to reach the final one,
and an interim response is returned only when no final response is
found. A response read after a request carries that request. Data that
cannot be read this way still uses the last "HTTP/" search; httpx
-store-chain output, which puts a newline after the first request,
relies on it to return the chain's final response.

This supersedes #7922, which anchors the version pattern and so fixes

Fixes: #7921
Fixes: #7924
Reported-by: @MasterchiefGalaxy
Signed-off-by: Dwi Siswanto <[email protected]>
2026-10-25 13:16:36 +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
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
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 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
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 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
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
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
Maximilian Hildebrand 2815cb2aef fix(installer): fail fast on unremovable templates update lock 2026-10-06 10:12:11 +02: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
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
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
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
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
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
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
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
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
IgorDaniel45 ccd0aaca40 fix compiled cache after metadata load 2026-09-25 10:56:02 -03:00
Mzack9999 3fb407b345 restore tls 2026-09-23 23:47:09 +02:00
Mzack9999 c987c2df6a fix quarantine 2026-09-23 21:58:13 +02:00
Mzack9999 213d3badfd merge dev 2026-09-23 20:23:30 +02: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
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
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
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 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
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
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
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
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
359 changed files with 28620 additions and 2404 deletions

No files matched your search

+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
+1 -1
View File
@@ -36,7 +36,7 @@ It also specifies a `WorkflowLoader` interface that is used during workflow load
```go
type WorkflowLoader interface {
GetTemplatePathsByTags(tags []string) []string
GetTemplatePathsByTags(tags []string, noValidate bool) []string
GetTemplatePaths(templatesList []string, noValidate bool) []string
}
```
+21 -12
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
@@ -144,12 +145,12 @@ TARGET:
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
-resume string resume scan from and save to specified file (clustering will be disabled)
-sa, -scan-all-ips scan all the IP's associated with dns record
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
-iv, -ip-version string[] IP version to scan of hostname (4,6); default uses dialer fallback, -sa scans all addresses
-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")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -176,7 +177,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
-llm-model string model used for semantic matching
-llm-timeout int time in seconds to wait for a single llm call (default 30)
@@ -220,6 +221,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -249,9 +251,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
@@ -294,6 +298,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)
@@ -321,6 +327,7 @@ OPTIMIZATIONS:
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
-ac, -auto-calibrate drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)
HEADLESS:
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
@@ -337,6 +344,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
@@ -352,10 +360,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
@@ -677,7 +686,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>
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+726
View File
@@ -0,0 +1,726 @@
![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, har, 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, jev) (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
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
-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)
-nh, -no-httpx disable httpx probing for non-url input
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
-no-stdin disable stdin processing
HEADLESS:
-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>
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+12 -8
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
@@ -147,7 +148,7 @@ TARGET:
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-im, -input-mode string mode of input file (list, burp, har, 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)
@@ -174,7 +175,7 @@ TEMPLATES:
-egm, -enable-global-matchers enable loading global matchers templates
-file enable loading file templates
-llm enable llm matchers and extractors (semantic matching)
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (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)
@@ -218,6 +219,7 @@ OUTPUT:
-jle, -jsonl-export string file to export results in JSONL(ine) format
-pe, -pdf-export string file to export results in PDF format
-rd, -redact string[] redact given list of keys from query parameter, request header and body
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
CONFIGURATIONS:
-config string path to the nuclei configuration file
@@ -250,6 +252,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
@@ -350,10 +353,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
+111 -238
View File
@@ -1,255 +1,128 @@
<!-- Generated by securitycontext.dev. Source HEAD: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1. -->
<!-- Generated by securitycontext.dev. Source HEAD: 8cda90655970270b215749b42ab59ecc38669293. -->
# Security Context: projectdiscovery/nuclei @ dev
> Reference context for editing projectdiscovery/nuclei. Before changing any component below, **preserve the listed guards and do not reintroduce these fixed issues**. Each fingerprint is a regression check, and each shared attack surface is a guard that must keep holding on every path.
_Generated 2026-09-01 16:22 UTC by secc_
_Analyzed commit: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1_
**Remediation coverage: 56.7%**
_17 of 30 historical fixes have no remaining unguarded variant; 368 open remediation leads to verify. Measures fix consistency, not a vulnerability verdict._
- Commits analyzed: **4814**
- Commits flagged for review: **235**
- Confirmed historical security fixes: **32**
- Known published CVEs: **0**
## Summary
Nuclei's historical security posture is dominated by sandbox escapes and code execution via unsigned or unverified templates, with recurring SSRF, path traversal, and auth bypass issues across JS libraries, loaders, and workflow paths. The most critical theme is inconsistent enforcement of signature verification and local-file-access (-lfa) guards across direct, workflow, DAST, and JS-sandbox entry points, enabling variant attacks. A second theme is network policy (host allowlist) gaps in LDAP dialing and proxy handling. The security context also includes dependency upgrades (Alpine, go-git) and output file permission hardening, but the highest-value focus is on template trust boundaries and sandbox enforcement parity.
_Analyzed [8cda906559][c-8cda906559] on `dev` · 5234 commits, 574 security-relevant examined · generated 2026-11-07_
## Recurring Security Patterns
**15.6% of classified fixes repeat a previously fixed pattern** (5 recurring, 27 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
- **Code Execution:** [1c440e755a](https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645) → [9de96e4dda](https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76) — matched on `class:code execution`, `component:pkg/catalog/loader/loader.go`
- **Code Execution via Unsigned Template:** [fa56800fcc](https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de) → [fac0b7196d](https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37) — matched on `class:code execution via unsigned template`, `CWE-347`, `sink:loadtemplates`
- **SSRF:** [a167e6c57b](https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770) → [9def9af7e3](https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572) — matched on `class:ssrf`, `CWE-918`, `component:pkg/js/libs/ldap/ldap.go`
- **Dependency Vulnerability:** [d93b4a01df](https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe) → [fcd5c6b111](https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529) — matched on `class:dependency vulnerability`, `CWE-1104`, `component:dockerfile`
- **Sandbox Escape:** [96049a32fa](https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a) → [cf2a98b03d](https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91) — matched on `class:sandbox escape`, `CWE-284`, `component:v2/internal/runner/processor.go`, `sink:stdlib allmodulenames`, `sink:stdlib getmodulemap`
**12.5% of classified fixes repeat a previously fixed pattern** (2 recurring, 14 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
## Top Risks
### 1. Sandbox Escape: `pkg/js/compiler/pool.go, v2/pkg/protocols/javascript/js.go, v2/pkg/protocols/headless/engine/engine.go, v2/internal/runner/processor.go` (high)
Components with the strongest concentration of past fixes for one class.
Multiple sandbox escapes across JS require(), eval/ReadFile, headless browser flags, and tengo imports, indicating weak default sandbox boundaries that require consistent -lfa and safe-module enforcement.
Evidence: 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, df55f7a2eb591a7949b5c66a9c54db54aa28f450, cf2a98b03d76327c20fb0183b7f1c849eb692c91, 96049a32fa70aef0483d4180d386a57b3c2d973a
### 2. Code Execution via Unsigned Template: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go, pkg/templates/templates.go` (critical)
Unsigned templates can execute code through direct loading, workflows, and DAST branches; signature verification was bypassed via cache poisoning and missing guards on specific paths.
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de
### 3. Auth Bypass: `pkg/templates/compile.go, pkg/templates/signer/tmpl_signer.go, pkg/catalog/loader/loader.go, pkg/protocols/http/request.go` (high)
Signature verification can be bypassed via cached metadata, improper signature extraction, or missing checks on specific load paths, and cookie jar bypass via disabled cookie handling.
Evidence: 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b, a976ca8d3f6a18c9ca746c8ce73f28fdcd518642
### 4. SSRF: `pkg/js/libs/ldap/ldap.go, v2/internal/runner/proxy.go` (high)
SSRF via LDAP dialing to internal hosts and via proxy configuration; host allowlist checks were missing or applied to the wrong target (domain vs controller).
Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f
### 5. Path Traversal: `pkg/catalog/config/nucleiconfig.go, v2/internal/runner/update.go, v2/pkg/protocols/common/protocolstate/headless.go` (high)
Path traversal via lexical prefix checks, zip-slip in template extraction, and file:// scheme in headless mode; canonicalization and containment checks are required.
Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, d3928e080d819f25306bfbaecc399f03516db5aa
### 6. Code Execution: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go` (high)
Code execution via cache poisoning and unverified code templates in workflows; requires -code flag and verified templates.
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, e86f38299765b82ad724fdb701557e0eaff3884d
## Shared Attack Surfaces
Capabilities guarded by more than one past fix on different code paths. A guard enforced on one entry point is often missing on a sibling; verify each alternate path before assuming it's covered.
### 1. Template signature verification and unsigned-template opt-in enforcement across all load paths
- Guard that should hold everywhere: signature verification (template.Verified, Verify), DisableUnsignedTemplates flag, -code flag for code templates, cache invalidation on verification status/fingerprint/digest
- Entry points: `pkg/catalog/loader/loader.go (LoadTemplatesWithTags, DAST branch)`, `pkg/templates/workflows.go (loadTemplates, workflowExecutor)`, `pkg/templates/templates.go (loadTemplates)`, `pkg/templates/compile.go (template.Verified)`, `pkg/templates/signer/tmpl_signer.go (Verify)`
- Check: Inspect the DAST and workflow sub-loaders: they must enforce the same DisableUnsignedTemplates and -code checks as the direct loader, and must not rely on cached verification results without re-checking signatures.
- Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de, 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b
### 2. Local file access (read/write) outside the sandbox, gated by -lfa flag
- Guard that should hold everywhere: -lfa flag enforcement, path canonicalization and containment (sandbox directory), file:// scheme blocking, allowAllFiles stripping, require() registry rebuild
- Entry points: `pkg/js/libs/krbforge/krbforge.go (CreateTicket)`, `pkg/js/libs/mysql/mysql_private.go (sql.Open)`, `pkg/js/compiler/pool.go (require)`, `v2/pkg/protocols/javascript/js.go (ReadFile, eval)`, `v2/pkg/protocols/common/protocolstate/headless.go (file:)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
- Check: Verify that every JS library and protocol helper (krbforge, mysql, require, headless file://, payload loaders) enforces the -lfa flag and path containment; a sibling path that skips the -lfa check is a sandbox escape.
- Evidence: 3a8b65cb45f121fcdf418f60e5177ed9f250c122, 9611926a9a1ca99e8ed554d1b26fbeff60e06368, 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, d3928e080d819f25306bfbaecc399f03516db5aa, e7fb40a41308f6c41267e858d1781eb67f7e0572
### 3. Network policy (host allowlist) enforcement before outbound connections
- Guard that should hold everywhere: IsHostAllowed check on the actual target host (controller, not domain), proxy validation and connectivity check, ProxyInternal gate for HTTP_PROXY env
- Entry points: `pkg/js/libs/ldap/ldap.go (Dial, newLdapSession)`, `v2/internal/runner/proxy.go (loadProxyServers, os.Setenv HTTP_PROXY)`
- Check: Inspect other dialing paths (e.g., HTTP, headless) to ensure they apply the same host allowlist check on the resolved IP/host, not just a user-supplied domain, and that proxy settings cannot redirect traffic to internal hosts.
- Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f, 3f5c055aca1499642caf4f40e8cf55ad32bd4da9
### 4. Path traversal in file extraction and template path trust boundaries
- Guard that should hold everywhere: canonical path containment check (filepath.Rel, prefix), zip-slip validation against template directory, sandbox path prefix validation
- Entry points: `pkg/catalog/config/nucleiconfig.go (IsCustomTemplate)`, `v2/internal/runner/update.go (os.OpenFile)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
- Check: Audit all file-write and file-read paths that accept template-controlled paths (e.g., template extraction, payload loading) to ensure they use canonical containment, not lexical prefix checks, and apply the same sandbox directory restriction.
- Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, e7fb40a41308f6c41267e858d1781eb67f7e0572
## Historical Vulnerability Classes
| Class | Count |
|---|---|
| Sandbox Escape | 7 |
| Auth Bypass | 4 |
| Code Execution | 3 |
| Dependency Vulnerability | 3 |
| Path Traversal | 3 |
| SSRF | 3 |
| Code Execution via Unsigned Template | 2 |
| Arbitrary File Read | 1 |
| Cryptographic Weakness | 1 |
| Deserialization of Untrusted Data | 1 |
| Information Disclosure | 1 |
| Insecure File Permissions | 1 |
| Race Condition | 1 |
| Security Misconfiguration | 1 |
| # | Class | Component | Severity | Fixes | Evidence |
|---|---|---|---|---|---|
| 1 | Code Execution | `pkg/fuzz/execute.go` | high | 2 | [4fed40f75a][c-4fed40f75a], [ccbfb12bd0][c-ccbfb12bd0] |
| 2 | SSRF | `pkg/protocols/common/protocolstate/headless.go` | high | 2 | [d43c1f547f][c-d43c1f547f], [d9dabbb4a1][c-d9dabbb4a1] |
| 3 | Arbitrary File Access | `v2/pkg/protocols/common/generators/load.go` | high | 0 (1 component gone, not counted) | [e5154d362a][c-e5154d362a] |
| 4 | Arbitrary File Access | `pkg/js/libs/mysql/mysql.go` | high | 1 | [634cecbb28][c-634cecbb28] |
| 5 | Certificate Validation Bypass | `pkg/reporting/exporters/es/elasticsearch.go` | high | 1 | [82d5a8acc2][c-82d5a8acc2] |
| 6 | Code Execution | `pkg/protocols/common/expressions/expressions.go` | high | 1 | [d221732016][c-d221732016] |
## Fixed-Issue Fingerprints
These are real security fixes mined from git history. Use them as regression checks.
Real security fixes mined from git history; use them as regression checks. `verified` fixes are linked to a published advisory, `inferred` fixes were confirmed from the diff evidence alone. Each is identified by its short commit SHA, which the variant-leads report uses to reference it back to this history.
- **Auth Bypass** (high) [CWE-287]: Honors disable-cookie by preventing the shared cookie jar from being applied when cookie handling is disabled, closing a path where cookies could be reused across requests.
- sink: `SetCookieJar(input.CookieJar)`
- fix: Adds !request.DisableCookie check before applying shared cookie jar
- commit: https://github.com/projectdiscovery/nuclei/commit/a976ca8d3f6a18c9ca746c8ce73f28fdcd518642 (2026-08-25)
- **Code Execution** (high) [CWE-347]: This commit fixes a cache poisoning vulnerability where unsigned templates could bypass signature verification and execute malicious code during compilation or request execution.
- sink: `template compilation and execution of JavaScript`
- fix: cache invalidation based on verification status, verifier fingerprint, and content digest
- commit: https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76 (2026-08-04)
- **Code Execution via Unsigned Template** (high) [CWE-347]: Enforces signature verification on JavaScript templates, preventing execution of unsigned templates that could lead to arbitrary code execution.
- sink: `loading unsigned javascript templates into execution paths`
- fix: Require signature verification for javascript templates in direct loading and workflows when signing is enforced
- commit: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37 (2026-07-03)
- **Sandbox Escape** (high) [CWE-22]: Enforces nuclei's local file access policy on krbforge output paths, preventing arbitrary file writes outside the sandbox.
- sink: `gpkrb.CreateTicket(cfg) with cfg.OutputFile`
- fix: Path normalization into sandbox and denial of outside paths unless -lfa
- commit: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122 (2026-06-24)
- **SSRF** (high) [CWE-918]: Adds network policy host allowlist checks before LDAP dialing, preventing SSRF to restricted internal hosts.
- sink: `dialers.Fastdialer.Dial`
- fix: host allowlist check via IsHostAllowed before dialing
- commit: https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572 (2026-06-24)
- **Arbitrary File Read** (high) [CWE-22]: Gates MySQL's allowAllFiles option behind the -lfa flag, preventing arbitrary local file reads via LOAD DATA LOCAL INFILE.
- sink: `sql.Open("mysql", dsn) with AllowAllFiles=true`
- fix: DSN sandbox that strips allowAllFiles unless -lfa is set
- commit: https://github.com/projectdiscovery/nuclei/commit/9611926a9a1ca99e8ed554d1b26fbeff60e06368 (2026-06-19)
- **Code Execution** (critical) [CWE-94]: Moves the unsigned code-template guard ahead of the DAST branch, preventing unsigned code templates from being loaded and executed under DAST, which previously bypassed the signature check.
- sink: `Template loading and execution`
- fix: Hoisting unsigned code-template guard before DAST branch to ensure it applies on all load paths
- commit: https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645 (2026-06-19)
- **Path Traversal** (high) [CWE-22]: This commit replaces lexical prefix checks with canonical path containment checks across multiple filesystem trust boundaries, preventing path traversal where sibling paths were incorrectly treated as trusted.
- sink: `IsCustomTemplate`
- fix: path canonicalization and containment check
- commit: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206 (2026-06-04)
- **Information Disclosure** (medium) [CWE-200]: This commit fixes a data leakage issue in workflows where sibling state could leak across concurrent executions, and race requests ignored dynamic values, by syncing context and cloning inputs.
- sink: `syncWorkflowInputToTemplateCtx`
- fix: context synchronization and input cloning
- commit: https://github.com/projectdiscovery/nuclei/commit/805bcfe76f3629dbe71021795d4ce865406c1555 (2026-05-08)
- **Sandbox Escape** (high) [CWE-284]: Fixes sandbox escape where require() bypassed local file access restrictions and reused privileged module cache across executions.
- sink: `require() using default host filesystem loader`
- fix: Rebuild require registry per execution and enforce allow-local-file-access sandbox rules
- commit: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d (2026-04-10)
- **Cryptographic Weakness** (medium) [CWE-338]: Replaces predictable math/rand PRNG with cryptographically secure crypto/rand in JS helper functions.
- sink: `rand.Intn(255) / rand.Int63()`
- fix: Replace predictable math/rand with crypto/rand
- commit: https://github.com/projectdiscovery/nuclei/commit/44ef9ce34ffcfdfca3a96626abc73d99dd0e83ab (2026-03-15)
- **Auth Bypass** (high) [CWE-347]: Caching signature verification results without cryptographic re-check allows a metadata index compromise to bypass template signature verification.
- sink: `template.Verified, template.TemplateVerifier`
- fix: cached verification data reuse
- commit: https://github.com/projectdiscovery/nuclei/commit/4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4 (2026-01-21)
- **Race Condition** (medium) [CWE-362]: Fixes a race condition in workflow execution caused by shared context callbacks, which could lead to data corruption or crashes; the primary impact is reliability, not a direct security breach.
- sink: `shared context callbacks in workflow execution`
- fix: fix race condition by making workflow execution sequential
- commit: https://github.com/projectdiscovery/nuclei/commit/24d1f58682027e23cb21a299a170abd2bdf20dc2 (2025-12-05)
- **Auth Bypass** (high) [CWE-347]: Fixes template signature verification by centralizing signature extraction and requiring the signature to be at the end of the template, preventing bypass.
- sink: `Verify`
- fix: signature extraction and verification
- commit: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a (2024-08-19)
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.5 to 3.18.6 to fix known security vulnerabilities in bundled packages.
- sink: `FROM alpine:3.18.5`
- fix: Upgrade base image to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529 (2024-03-15)
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.2 to 3.18.5 to fix known security vulnerabilities in bundled packages.
- sink: `FROM alpine:3.18.2`
- fix: Upgrade base image to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe (2024-03-15)
- **Dependency Vulnerability** (high) [CWE-1035]: Updates go-git to fix known vulnerabilities (CVE-2023-49569, CVE-2023-49568), which could be exploited when processing untrusted git data.
- sink: `go-git library usage`
- fix: dependency upgrade to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/1700cdf4e23128a358066364062246b58c1c013f (2024-03-14)
- **Code Execution via Unsigned Template** (high) [CWE-347]: Introduces a flag to disable unsigned templates, preventing execution of unverified templates that could lead to arbitrary code execution.
- sink: `loading unsigned templates when -dut flag is set`
- fix: Adds -disable-unsigned-templates flag to skip unsigned templates and report them separately
- commit: https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de (2024-03-14)
- **Auth Bypass** (medium) [CWE-347]: This commit introduces a flag to skip unsigned templates, closing a path where an attacker could execute untrusted templates by bypassing signature verification.
- sink: `parsed.Verified`
- fix: skip unverified templates when DisableUnsignedTemplates is set
- commit: https://github.com/projectdiscovery/nuclei/commit/9bd4db3f74aedd067af568df4f9198a8a090f68b (2024-03-10)
- **Code Execution** (high) [CWE-94]: Prevents execution of unverified code templates in workflows, mitigating code execution risk.
- sink: `template.RequestsCode`
- fix: require -code flag and verified templates for code templates in workflows
- commit: https://github.com/projectdiscovery/nuclei/commit/e86f38299765b82ad724fdb701557e0eaff3884d (2024-03-01)
- **SSRF** (high) [CWE-918]: Fixes SSRF by validating the actual LDAP controller host against the network policy instead of the domain name.
- sink: `newLdapSession`
- fix: host allowlist check on controller instead of domain
- commit: https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770 (2024-01-20)
- **Sandbox Escape** (high) [CWE-94]: Disables eval and tightens file read validation in the JavaScript sandbox, preventing template-driven code execution and file access outside the sandbox.
- sink: `eval function and file read in sandbox mode`
- fix: Disable eval function and rewrite file validation in sandbox mode
- commit: https://github.com/projectdiscovery/nuclei/commit/c35162c8ef061a96dd74b54c8f022c1a41cf5833 (2023-10-13)
- **Path Traversal** (high) [CWE-22]: Adds network policy and blocks file:// protocol in headless mode unless -lfa is set, preventing local file disclosure.
- sink: `file:// protocol access in headless browser`
- fix: Network policy validation and file:// scheme blocking when local file access is disabled
- commit: https://github.com/projectdiscovery/nuclei/commit/d3928e080d819f25306bfbaecc399f03516db5aa (2023-08-25)
- **Deserialization of Untrusted Data** (medium) [CWE-502]: Fixes JSON deserialization by adding explicit UnmarshalJSON methods and correcting tags, preventing type confusion and potential logic bypasses.
- sink: `UnmarshalJSON on user-controlled fields like Type and Name`
- fix: Adds proper UnmarshalJSON implementations and corrects json tags to prevent type confusion
- commit: https://github.com/projectdiscovery/nuclei/commit/85090b75317977bb9301444c9dfd8e4c2dba667a (2023-02-07)
- **Sandbox Escape** (medium) [CWE-22]: The commit adds sandbox mode that restricts payload file paths to the template directory, preventing arbitrary file reads when sandbox is enabled.
- sink: `loadPayloadsFromFile`
- fix: path prefix validation against template directory and template path directory
- commit: https://github.com/projectdiscovery/nuclei/commit/e7fb40a41308f6c41267e858d1781eb67f7e0572 (2022-11-16)
- **Insecure File Permissions** (low) [CWE-732]: Fixes overly permissive file permissions on debug output files by changing from os.ModePerm (0777) to 0644.
- sink: `os.OpenFile`
- fix: restrict file mode to 0644
- commit: https://github.com/projectdiscovery/nuclei/commit/af4854f90de728bb08689d5cae3dec4acfdb6b9a (2022-06-20)
- **Security Misconfiguration** (medium) [CWE-16]: Prevents setting HTTP_PROXY environment variable unless explicitly enabled, avoiding potential proxy hijacking.
- sink: `os.Setenv(types.HTTP_PROXY_ENV, proxyURL.String())`
- fix: Added check for options.ProxyInternal before setting HTTP_PROXY environment variable
- commit: https://github.com/projectdiscovery/nuclei/commit/3f5c055aca1499642caf4f40e8cf55ad32bd4da9 (2022-03-09)
- **Sandbox Escape** (high) [CWE-250]: The commit restricts the use of the --no-sandbox flag to Linux root users, preventing sandbox escape vulnerabilities in other environments.
- sink: `NoSandbox(true)`
- fix: Conditionally disable sandbox only on Linux as root
- commit: https://github.com/projectdiscovery/nuclei/commit/df55f7a2eb591a7949b5c66a9c54db54aa28f450 (2021-12-05)
- **Path Traversal** (high) [CWE-22]: This commit fixes a zip-slip path traversal vulnerability by validating the extracted file path against the configured template directory.
- sink: `os.OpenFile`
- fix: path canonicalization and prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/f9c214a66f37a00fc29fe64232b690e6285ec59c (2021-11-25)
- **SSRF** (high) [CWE-918]: Adds validation and connectivity checks for SOCKS5 proxies before launching a scan, preventing SSRF through proxy configuration.
- sink: `Proxy connection validation and loading`
- fix: Proxy server validation and connectivity check before scan launch
- commit: https://github.com/projectdiscovery/nuclei/commit/0e46d3e0410f89e4af41099f5cd38fa7f54ae54f (2021-11-10)
- **Sandbox Escape** (high) [CWE-284]: This commit adds a sandbox flag that, when enabled, restricts the tengo scripting engine to safe modules, preventing sandbox escape and arbitrary command execution.
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
- fix: Added sandbox flag to restrict imports to safe modules when sandbox is enabled
- commit: https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91 (2020-12-13)
- **Sandbox Escape** (high) [CWE-284]: This commit restricts the tengo scripting engine to a safe module list, preventing sandbox escape and arbitrary command execution via workflow templates.
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
- fix: Restricted tengo script imports to a safe module list
- commit: https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a (2020-12-13)
**By component** (directories with the most fixes):
- `pkg/fuzz` — 2 fixes: Code Execution (2)
- `pkg/protocols/common/protocolstate` — 2 fixes: SSRF (2)
- [4fed40f75a][c-4fed40f75a] **fix(fuzz): stop re-rendering evaluated template variables (#8057)** — Code Execution, high [CWE-94] · inferred · 2026-11-05
`pkg/fuzz/execute.go` · sink `rule.evaluateVarsWithInteractsh` · fix: safe parser configuration
- [82d5a8acc2][c-82d5a8acc2] **exporters: verify TLS certificates (#8042)** — Certificate Validation Bypass, high [CWE-295] · inferred · 2026-11-04
`pkg/reporting/exporters/es/elasticsearch.go` · sink `DialTLS` · fix: cryptographic verification
- [1c0f77d28e][c-1c0f77d28e] **fix(js/net): enforce network policy on proxied target in JS net dial** — SSRF, high [CWE-918] · inferred · 2026-10-18
`pkg/js/libs/net/net.go` · sink `dialHTTPProxy` · fix: input validation
- [d43c1f547f][c-d43c1f547f] **fix(headless): canonicalize IPv4-mapped IPv6 literals and re-validate dial targets** — SSRF, high [CWE-918] · inferred · 2026-10-13
`pkg/protocols/common/protocolstate/headless.go` · sink `net.DialTimeout` · fix: canonicalization before validation
- [d9dabbb4a1][c-d9dabbb4a1] **fix(headless): enforce network policy on ws and wss schemes (#7867)** — SSRF, high [CWE-918] · inferred · 2026-10-12
`pkg/protocols/common/protocolstate/headless.go` · sink `isValidHost` · fix: canonicalization before validation
- [a20aa26024][c-a20aa26024] **Fix pprof/auth exposure on http-api-endpoint and harden file permissions** — Information Disclosure, high [CWE-200] · inferred · 2026-08-03
`internal/httpapi/apiendpoint.go` · sink `http.ListenAndServe` · fix: authentication verification
- [fac0b7196d][c-fac0b7196d] **feat(templates): require signatures for javascript templates (#7514)** — Signature Verification Bypass, high [CWE-347] · verified (CVE-2026-76819, GHSA-vxg7-f2jj-jmqm) · 2026-07-03
`pkg/catalog/loader/loader.go` · sink `t.HasJavascriptRequest` · fix: cryptographic verification
- [ccbfb12bd0][c-ccbfb12bd0] **refactor(protocols): template rendering boundary (#7499)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-06-30
`pkg/fuzz/execute.go` · sink `expressions.Evaluate` · fix: sandbox isolation
- [3a8b65cb45][c-3a8b65cb45] **fix(js): reject unsandboxed ccache writes in krbforge (#7482)** — Sandbox Escape, high [CWE-693] · inferred · 2026-06-24
`pkg/js/libs/krbforge/krbforge.go` · sink `OutputFile` · fix: sandbox isolation
- [68daef44a7][c-68daef44a7] **fix(js): escape dbname in lib/pq URL (#7479)** — SQL Injection, high [CWE-89] · inferred · 2026-06-23
`pkg/js/libs/postgres/postgres.go` · sink `fmt.Sprintf` (library call — the operation around it is the sink) · fix: context-aware output escaping
- [634cecbb28][c-634cecbb28] **gate mysql allowAllFiles behind -lfa** — Arbitrary File Access, high [CWE-22] · verified (CVE-2026-76803, GHSA-xhmx-w2j4-rw3q) · 2026-06-19
`pkg/js/libs/mysql/mysql.go` · sink `sql.Open` · fix: sandbox isolation
- [20cbf0bd73][c-20cbf0bd73] **refactor: use path-aware filesystem containment checks (#7420)** — Path Traversal, high [CWE-22] · inferred · 2026-06-04
`pkg/installer/template.go` · sink `filepath.Join` · fix: canonicalization before validation
- [6f2ade6a9b][c-6f2ade6a9b] **fix(js): respect \`allow-local-file-access\` in \`require\` (#7332)** — Sandbox Escape, high [CWE-693] · inferred · 2026-04-10
`pkg/js/compiler/pool.go` · sink `require.DefaultSourceLoader` · fix: sandbox isolation
- [d221732016][c-d221732016] **fix(expressions): only eval template-authored expressions (#7221)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-03-16
`pkg/protocols/common/expressions/expressions.go` · sink `FindExpressions` · fix: allowlist enforcement
- [0da993afe6][c-0da993afe6] **Merge commit from fork** — Signature Verification Bypass, medium [CWE-347] · verified (CVE-2024-43405, GHSA-7h5p-mmpp-hgmm) · 2024-08-19
`pkg/templates/signer/tmpl_signer.go` · sink `os.ReadFile` · fix: cryptographic verification
- [e5154d362a][c-e5154d362a] **fixing payload load (#3927)** — Arbitrary File Access, high [CWE-22] · verified (CVE-2023-37896, GHSA-2xx4-jj5v-6mff) · 2023-07-14
`v2/pkg/protocols/common/generators/load.go` · sink `loadPayloads` · fix: sandbox isolation
## Historical Vulnerability Classes
| Class | Fixes | Verified | Most recent fix |
|---|---|---|---|
| Code Execution | 3 | 2 | [2026-11-05][c-4fed40f75a] 4fed40f75a |
| SSRF | 3 | 0 | [2026-10-18][c-1c0f77d28e] 1c0f77d28e |
| Arbitrary File Access | 1 (1 component gone, not counted) | 2 | [2026-06-19][c-634cecbb28] 634cecbb28 |
| Sandbox Escape | 2 | 0 | [2026-06-24][c-3a8b65cb45] 3a8b65cb45 |
| Signature Verification Bypass | 2 | 2 | [2026-07-03][c-fac0b7196d] fac0b7196d |
| Certificate Validation Bypass | 1 | 0 | [2026-11-04][c-82d5a8acc2] 82d5a8acc2 |
| Information Disclosure | 1 | 0 | [2026-08-03][c-a20aa26024] a20aa26024 |
| Path Traversal | 1 | 0 | [2026-06-04][c-20cbf0bd73] 20cbf0bd73 |
| SQL Injection | 1 | 0 | [2026-06-23][c-68daef44a7] 68daef44a7 |
## Known Advisories
### Linked to a fix commit in this history (5)
- **CVE-2026-76805** / GHSA-jpvm-9frm-hjcq (medium 5.3) CWE-200 — fixed in [ccbfb12bd0][c-ccbfb12bd0], [d221732016][c-d221732016]
- Environment Variable Disclosure via Response-Derived Data in DAST/Fuzz Mode. A vulnerability in Nuclei's DAST/fuzz expression evaluation path allows a malicious target server to trigger disclosure of ...
- **CVE-2026-76803** / GHSA-xhmx-w2j4-rw3q (medium 5.3) CWE-284 — fixed in [634cecbb28][c-634cecbb28]
- Local File Read via MySQL Client Sandbox Bypass. A vulnerability in Nuclei's JavaScript MySQL client library allows arbitrary local file reads that bypass the `-allow-local-file-access` (`-lfa`) sandb...
- **CVE-2026-76819** / GHSA-vxg7-f2jj-jmqm (high 8.6) CWE-94,CWE-787 — fixed in [fac0b7196d][c-fac0b7196d]
- Arbitrary Code Execution via Goja JavaScript Engine Vulnerability. A vulnerability in the Goja JavaScript engine used by Nuclei's `javascript:` protocol allows arbitrary native code execution on the s...
- **CVE-2024-43405** / GHSA-7h5p-mmpp-hgmm (high 7.4) CWE-78 — fixed in [0da993afe6][c-0da993afe6]
- Nuclei Template Signature Verification Bypass. ## Summary A vulnerability has been identified in Nuclei's template signature verification system that could allow an attacker to bypass the signature ch...
- **CVE-2023-37896** / GHSA-2xx4-jj5v-6mff (medium 5.9) CWE-22 — fixed in [e5154d362a][c-e5154d362a]
- Nuclei < 2.9.9 - Sandbox restriction bypass. ## Overview Affected: < 2.9.9 (patched 2.9.9).
### Without a linked fix commit (6)
Published elsewhere, fixed before the history window, or not yet linked; full descriptions are in the JSON context.
| Advisory | Severity | Published | CWE | Summary |
|---|---|---|---|---|
| CVE-2026-76804 / GHSA-qgw5-7j4f-fg97 | medium 5.5 | 2026-07-16 | CWE-284 | Local File Read via Workflow File-Protocol Gate Bypass. A vulnerability in Nuclei's workfl... |
| CVE-2026-76802 / GHSA-jpf4-98qj-qr67 | medium 4.7 | 2026-07-16 | CWE-78 | Arbitrary Command Execution via DAST Code Signature Bypass. A vulnerability in Nuclei's DA... |
| CVE-2026-41645 / GHSA-jm34-66cf-qpvr | medium 5.3 | 2026-04-18 | CWE-94 | Environment variable disclosure via Response-Derived DSL Expressions. A vulnerability in N... |
| CVE-2026-41646 / GHSA-29rg-wmcw-hpf4 | medium 5.5 | 2026-04-18 | CWE-284 | Local File Read via require() Module Loader Bypass. A vulnerability in Nuclei's JavaScript... |
| CVE-2024-40641 / GHSA-c3q9-c27p-cw9h | high 7.4 | 2024-07-17 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Summary... |
| CVE-2024-27920 / GHSA-w5wx-6g2r-r78q | high 7.4 | 2024-03-15 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Overvie... |
[c-8cda906559]: https://github.com/projectdiscovery/nuclei/commit/8cda90655970270b215749b42ab59ecc38669293
[c-4fed40f75a]: https://github.com/projectdiscovery/nuclei/commit/4fed40f75a57c7c414aac62d213d772cc3f430d3
[c-ccbfb12bd0]: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6
[c-d43c1f547f]: https://github.com/projectdiscovery/nuclei/commit/d43c1f547f98ac56654f4335ad20af0cca4c229c
[c-d9dabbb4a1]: https://github.com/projectdiscovery/nuclei/commit/d9dabbb4a19e897a6bc12334a030a43035b27b73
[c-e5154d362a]: https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648
[c-634cecbb28]: https://github.com/projectdiscovery/nuclei/commit/634cecbb289db494247c04a9c4ec6a113aef3cc0
[c-82d5a8acc2]: https://github.com/projectdiscovery/nuclei/commit/82d5a8acc2c756394c7fa92eae08b41330e89d69
[c-d221732016]: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3
[c-1c0f77d28e]: https://github.com/projectdiscovery/nuclei/commit/1c0f77d28ed639a34c2036fa23c7503ffac0bf27
[c-a20aa26024]: https://github.com/projectdiscovery/nuclei/commit/a20aa260245e5db889c5a421d4a65aeb2a64bf46
[c-fac0b7196d]: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37
[c-3a8b65cb45]: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122
[c-68daef44a7]: https://github.com/projectdiscovery/nuclei/commit/68daef44a7eafd41c39690924620e418762909ef
[c-20cbf0bd73]: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206
[c-6f2ade6a9b]: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d
[c-0da993afe6]: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a
---
_Fixed-issue fingerprints and CVE matches are derived from real data; LLM-written prose is advisory and should be verified against the cited commits._
_Fixed-issue fingerprints and advisory matches are derived from git history and published advisories; the risk tables and pattern descriptions above are derived from them and should be verified against the cited commits._
+22
View File
@@ -530,6 +530,16 @@ Description of the template.
You can go in-depth here on what the template actually does.
Description, impact and remediation may hold {{placeholders}} filled for
each finding: named extractors, payloads, variables and DSL helpers
(network helpers excluded), and the result fields host, port, ip, url,
scheme, path, matched-at, matcher-name and extractor-name, which win over
values of the same name. Use join(", ", name) for an extractor with
several values, and brackets for a hyphenated name inside a helper:
to_upper([matcher-name]). Unknown placeholders stay as written, and so
does a field whose expression fails. Global matcher templates match on
other templates' responses, so only the result fields fill their text.
Examples:
@@ -543,6 +553,10 @@ description: Bower is a package manager which stores package information in the
description: Subversion ALM for the enterprise before 8.8.2 allows reflected XSS at multiple locations
```
```yaml
description: Redis {{version}} on {{host}}:{{port}} accepts the default password
```
</div>
@@ -559,6 +573,8 @@ Impact of the template.
You can go in-depth here on impact of the template.
Supports the same placeholders as description.
Examples:
@@ -665,6 +681,8 @@ Remediation steps for the template.
You can go in-depth here on how to mitigate the problem found by this template.
Supports the same placeholders as description.
Examples:
@@ -1011,6 +1029,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
@@ -3756,6 +3775,8 @@ Part Definitions:
- <code>req</code> - Headless request made from the client
- <code>duration</code> - Latest measured operation duration in seconds
- <code>resp,body,data</code> - Headless response received from client (default)
- <code>header,all_headers</code> - Headless response headers
- <code>status_code</code> - Status code received from the server
<hr />
@@ -4144,6 +4165,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
+4
View File
@@ -20,6 +20,8 @@ extend-exclude = [
"vendor/",
# Go checksum file
"go.sum",
# Generated history. Commit SHAs are not words.
"SECURITY_CONTEXT.md",
]
[default.extend-identifiers]
@@ -32,6 +34,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
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
const headerChildEnv = "NUCLEI_HEADER_CHILD_ARGS"
// -H values can contain "#", on the command line, in a header file and in the
// config file, while lines of a header file that start with "#" are skipped as
// comments.
func TestHeaderValuesKeepHash(t *testing.T) {
if args := os.Getenv(headerChildEnv); args != "" {
options = &types.Options{Logger: gologger.DefaultLogger}
os.Args = append([]string{os.Args[0]}, strings.Split(args, "\n")...)
readConfig()
fmt.Printf("headers=%s\n", strings.Join(options.CustomHeaders, "|"))
os.Exit(0)
}
root := t.TempDir()
headerFile := filepath.Join(root, "headers.txt")
require.NoError(t, os.WriteFile(headerFile, []byte("# auth headers\nX-Token: abc#def\nReferer: https://app.example.com/#/dashboard\n"), 0o600))
configFile := filepath.Join(root, "config.yaml")
require.NoError(t, os.WriteFile(configFile, []byte("header:\n - \"X-Config: cfg#value\"\n"), 0o600))
tests := []struct {
name string
args []string
want string
}{
{"cli and header file", []string{"-H", "Cookie: sid=1; color=#ff0000", "-H", headerFile}, "Cookie: sid=1; color=#ff0000|X-Token: abc#def|Referer: https://app.example.com/#/dashboard"},
{"config file", []string{"-config", configFile}, "X-Config: cfg#value"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempRoot := filepath.Join(t.TempDir(), "tmp")
require.NoError(t, os.Mkdir(tempRoot, 0o700))
args := append(tt.args, "-disable-update-check")
cmd := exec.Command(os.Args[0], "-test.run=^TestHeaderValuesKeepHash$", "-test.count=1")
cmd.Env = isolatedNucleiStorageEnv(root, tempRoot, headerChildEnv+"="+strings.Join(args, "\n"))
output, err := cmd.CombinedOutput()
require.NoError(t, err, string(output))
require.Contains(t, string(output), "headers="+tt.want+"\n")
})
}
}
+2 -1
View File
@@ -57,7 +57,8 @@
# severity: low
# # duplicate-issue-check (optional) flag to enable duplicate tracking issue check
# duplicate-issue-check: false
# # duplicate-issue-page-size (optional) controls how many issues to fetch per page when searching for duplicates
# # duplicate-issue-page-size (optional) sets the number of issues to request per page when searching for duplicates
# # (GitLab returns at most 100 issues per page)
# duplicate-issue-page-size: 100
# # duplicate-issue-max-pages (optional) limits how many pages to fetch when searching for duplicates (0 = no limit)
# duplicate-issue-max-pages: 0
+30 -4
View File
@@ -55,6 +55,17 @@ var (
inlineSecretsTempMu sync.Mutex
)
// normalizeHeaderValue trims a -H value and drops it when the whole line is a
// "#" comment. The goflags default strips inline comments, which cut values
// such as "Cookie: color=#fff" at the "#".
func normalizeHeaderValue(s string) string {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "#") {
return ""
}
return s
}
func main() {
options.Logger = gologger.DefaultLogger
@@ -244,6 +255,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
@@ -253,6 +266,11 @@ func readConfig() *goflags.FlagSet {
var pdcpAuth string
var fuzzFlag bool
// FileStringSliceOptions as set up by runner.ConfigureOptions, but keeping
// "#" inside -H values
headerSliceOptions := goflags.FileStringSliceOptions
headerSliceOptions.Normalize = normalizeHeaderValue
flagSet := goflags.NewFlagSet()
flagSet.CaseSensitive = true
flagSet.SetDescription(`Nuclei is a fast, template based vulnerability scanner focusing
@@ -269,10 +287,10 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringSliceVarP(&options.Targets, "target", "u", nil, "target URLs/hosts to scan", goflags.CommaSeparatedStringSliceOptions),
flagSet.StringVarP(&options.TargetsFilePath, "list", "l", "", "path to file containing a list of target URLs/hosts to scan (one per line)"),
flagSet.StringVarP(&options.InlineTargetsList, "targets-inline", "", "", "inline multiline target list (for use in template profiles)"),
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileCommaSeparatedStringSliceOptions),
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileNormalizedOriginalStringSliceOptions),
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.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6); default uses dialer fallback, -sa scans all addresses", goflags.CommaSeparatedStringSliceOptions),
flagSet.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)"),
)
@@ -307,7 +325,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together)"),
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev)"),
flagSet.StringVar(&options.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"),
@@ -353,6 +371,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVarP(&options.JSONLExport, "jsonl-export", "jle", "", "file to export results in JSONL(ine) format"),
flagSet.StringVarP(&options.PDFExport, "pdf-export", "pe", "", "file to export results in PDF format"),
flagSet.StringSliceVarP(&options.Redact, "redact", "rd", nil, "redact given list of keys from query parameter, request header and body", goflags.CommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.NoRedact, "no-redact", "nrd", false, "disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted"),
)
flagSet.CreateGroup("configs", "Configurations",
@@ -364,7 +383,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.IntVarP(&options.MaxRedirects, "max-redirects", "mr", 10, "max number of redirects to follow for http templates"),
flagSet.BoolVarP(&options.DisableRedirects, "disable-redirects", "dr", false, "disable redirects for http templates"),
flagSet.StringVarP(&options.ReportingConfig, "report-config", "rc", "", "nuclei reporting module configuration file"), // TODO merge into the config file or rename to issue-tracking
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", goflags.FileStringSliceOptions),
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", headerSliceOptions),
flagSet.RuntimeMapVarP(&options.Vars, "var", "V", nil, "custom vars in key=value format"),
flagSet.StringVarP(&options.ResolversFile, "resolvers", "r", "", "file containing resolver list for nuclei"),
flagSet.BoolVarP(&options.SystemResolvers, "system-resolvers", "sr", false, "use system DNS resolving as error fallback"),
@@ -383,9 +402,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"),
@@ -432,6 +453,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"),
@@ -464,6 +487,7 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVar(&options.PreflightPortScan, "preflight-portscan", false, "run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)"),
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "disable stdin processing"),
flagSet.BoolVar(&options.EnableHTTPCache, "http-cache", false, "enable HTTP cache (RFC 9111) for HTTP requests"),
flagSet.BoolVarP(&options.AutoCalibrate, "auto-calibrate", "ac", false, "drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)"),
)
flagSet.CreateGroup("headless", "Headless",
@@ -482,6 +506,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"),
@@ -500,6 +525,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"),
)
+40 -43
View File
@@ -1,6 +1,6 @@
module github.com/projectdiscovery/nuclei/v3
go 1.27.1
go 1.27.3
require (
carvel.dev/ytt v0.52.0
@@ -15,13 +15,14 @@ require (
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
github.com/adrg/xdg v0.5.3
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/alexsnet/go-vnc v0.1.0
github.com/alitto/pond v1.9.2
github.com/andybalholm/brotli v1.2.0
github.com/andygrunwald/go-jira v1.16.1
github.com/antchfx/htmlquery v1.3.5
github.com/antchfx/htmlquery v1.3.7
github.com/antchfx/xmlquery v1.4.4
github.com/antchfx/xpath v1.3.6
github.com/antchfx/xpath v1.3.9
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.31.16
github.com/aws/aws-sdk-go-v2/credentials v1.18.20
@@ -33,10 +34,11 @@ require (
github.com/clbanning/mxj/v2 v2.7.0
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
github.com/docker/go-units v0.5.0
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/elazarl/goproxy v1.7.2
github.com/fatih/structs v1.1.0
github.com/fullstorydev/grpcurl v1.9.3
github.com/getkin/kin-openapi v0.144.0
github.com/getkin/kin-openapi v0.149.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
@@ -61,6 +63,7 @@ require (
github.com/jhump/protoreflect v1.17.0
github.com/julienschmidt/httprouter v1.3.0
github.com/kitabisa/go-ci v1.0.3
github.com/klauspost/compress v1.18.7
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
github.com/lib/pq v1.12.3
github.com/logrusorgru/aurora/v4 v4.0.0
@@ -71,12 +74,13 @@ require (
github.com/miekg/dns v1.1.73
github.com/oiweiwei/go-msrpc v1.2.12
github.com/olekukonko/tablewriter v1.0.8
github.com/openrdap/rdap v0.9.1
github.com/ory/dockertest/v3 v3.12.0
github.com/pkg/errors v0.9.1
github.com/praetorian-inc/fingerprintx v1.1.15
github.com/projectdiscovery/clistats v0.1.7
github.com/projectdiscovery/dsl v0.8.24
github.com/projectdiscovery/fastdialer v0.5.25
github.com/projectdiscovery/dsl v0.9.0
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
@@ -85,25 +89,25 @@ require (
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
github.com/projectdiscovery/gologger v1.1.73
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
github.com/projectdiscovery/hmap v0.0.102
github.com/projectdiscovery/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.54
github.com/projectdiscovery/ratelimit v0.0.93
github.com/projectdiscovery/rawhttp v0.1.92
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.30
github.com/projectdiscovery/retryablehttp-go v1.3.31
github.com/projectdiscovery/sarif v0.1.0
github.com/projectdiscovery/tlsx v1.4.1
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.9
github.com/projectdiscovery/wappalyzergo v0.3.4
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350
github.com/projectdiscovery/wappalyzergo v0.3.6
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/redis/go-redis/v9 v9.19.0
github.com/remeh/sizedwaitgroup v1.0.0
@@ -113,6 +117,7 @@ require (
github.com/segmentio/ksuid v1.0.4
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
github.com/sijms/go-ora/v2 v2.9.0
github.com/spaolacci/murmur3 v1.1.0
github.com/spf13/cast v1.10.0
github.com/stretchr/testify v1.12.1
github.com/syndtr/goleveldb v1.0.0
@@ -124,12 +129,12 @@ require (
go.mongodb.org/mongo-driver v1.17.9
go.uber.org/multierr v1.11.0
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
golang.org/x/net v0.58.0
golang.org/x/net v0.59.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.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
@@ -171,11 +176,9 @@ require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
@@ -234,12 +237,10 @@ require (
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
@@ -262,8 +263,8 @@ require (
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.6 // 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
@@ -323,7 +324,6 @@ require (
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/kataras/jwt v0.1.10 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@@ -349,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
@@ -366,7 +366,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.3.6 // indirect
github.com/openrdap/rdap v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
@@ -375,7 +374,7 @@ 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.1 // 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
@@ -385,14 +384,13 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // 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
@@ -418,7 +416,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.3 // 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
@@ -439,18 +437,17 @@ require (
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/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // 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
@@ -458,8 +455,8 @@ require (
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/mod v0.39.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
+72 -91
View File
@@ -130,7 +130,6 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
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=
@@ -148,15 +147,13 @@ github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -181,14 +178,13 @@ github.com/andygrunwald/go-jira v1.16.1 h1:WoQEar5XoDRAibOgKzTFELlPNlKAtnfWr296R
github.com/andygrunwald/go-jira v1.16.1/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=
github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=
github.com/antchfx/htmlquery v1.3.7 h1:KCUklE1EO/xm6iS3CWx6iatFlcnENBk4I4UYa2pBmLU=
github.com/antchfx/htmlquery v1.3.7/go.mod h1:/uBTMnFEl6LaBlIgw3KZ+c061tAQdlfbCXObZoxwQf4=
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.9 h1:muXJSod6jRzhsiUjvduNSxsRSptFZ2GOJ1oMD1QkPSA=
github.com/antchfx/xpath v1.3.9/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@@ -267,7 +263,6 @@ 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=
@@ -325,7 +320,6 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
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=
@@ -362,7 +356,6 @@ github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
@@ -430,8 +423,8 @@ github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc=
github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe h1:QKe/kmAYbndxwu91TcjHERsnMh5SgOB1x/qicvOdUJ8=
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
github.com/getsops/sops/v3 v3.9.2 h1:Cuuahc/UGu7W3+NXd+mrl+2oXGxxj/jIPsw6Ah2CbxA=
@@ -459,8 +452,8 @@ github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuw
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -473,8 +466,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=
@@ -592,7 +585,6 @@ 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/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=
@@ -852,8 +844,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=
@@ -935,14 +927,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.1 h1:+qPKwqdf3mFtrw3ZoVti7hembmVaapnJBjRosYTNdRo=
github.com/projectdiscovery/cdncheck v1.3.1/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
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.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
github.com/projectdiscovery/fastdialer v0.5.25 h1:xGFpc5B7cmogqnBrcVYV5yXh6UFlqRA6u5KqautKgD0=
github.com/projectdiscovery/fastdialer v0.5.25/go.mod h1:CZZ0PwdTw2KCI/PrZTW9wyBY88MIjpT8WhsKWSw3bkw=
github.com/projectdiscovery/dsl v0.9.0 h1:i1KyCbKJyKfHkgdbfh0JkWdd14APUjpEYf+GaYANlCg=
github.com/projectdiscovery/dsl v0.9.0/go.mod h1:ntNFkkiYCTEosGFTbApA61RMrvJTCjnakPxnWj56MyY=
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
github.com/projectdiscovery/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=
@@ -961,8 +953,8 @@ github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwap
github.com/projectdiscovery/gologger v1.1.73/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2 h1:aSLbDvrJQPlp4HvojU+bTU5XV8xdAVCjoc5gX1Wfbw4=
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2/go.mod h1:Z3VjpUJV1pnXDwvAddA8aZ4e839SoeftB7CSdZntAVI=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP1Erl8kaU=
@@ -981,32 +973,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.54 h1:+v7yyuRBu2S7dzeIJToDENy1i+HjRhNzqnQrDlm00z8=
github.com/projectdiscovery/networkpolicy v0.1.54/go.mod h1:tN+YAFVlJjHv/znPEhkcSAjmBMkMhslC9XpWKTrKEHw=
github.com/projectdiscovery/ratelimit v0.0.93 h1:k2rRpCR31DqeqrORSgGzJ+z0T7+QGCt0Z0+Ov/TVrgk=
github.com/projectdiscovery/ratelimit v0.0.93/go.mod h1:Tqpx1QKdFI63L01T2gpSvV5Uro9CnZkZWr80ciSXSBY=
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.30 h1:YnDCZP04/AKTiIBT5vA1nVhy5cRW5dPy1Du5nCjxHy4=
github.com/projectdiscovery/retryablehttp-go v1.3.30/go.mod h1:MXn1MzThNmHc05KldUbfks1MvwAnUKR4GGJcFhVIc08=
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.1 h1:QuECHVJERBirNrmpjgIOAFQScjy/DxVjzXeL+L81mH0=
github.com/projectdiscovery/tlsx v1.4.1/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.9 h1:aKLje3zPcZuOGmopN51PasWxPX/CypPwzP2ke/Qa2bA=
github.com/projectdiscovery/utils v0.11.9/go.mod h1:d3BcbsHhuxW9T3jpvrgzwshvqhaL9qPSkoi+3h/DY8E=
github.com/projectdiscovery/wappalyzergo v0.3.4 h1:yO+xel+C0d/vOFWcUKVZRpIDAfcEmsjpVDjQ0jifAUw=
github.com/projectdiscovery/wappalyzergo v0.3.4/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350 h1:uXqpYa6XKV9dCvoti9PbSgIWIsBKPu5YKMywB9G4q0Q=
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/wappalyzergo v0.3.6 h1:1FXCMeCa1xVu2zaW+eeBcbVxtlMStqGOlKcMLyh2pzI=
github.com/projectdiscovery/wappalyzergo v0.3.6/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -1059,8 +1051,8 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQKO9E0=
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/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=
@@ -1079,9 +1071,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=
@@ -1187,9 +1178,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=
@@ -1259,15 +1249,14 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l
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=
@@ -1291,20 +1280,20 @@ 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=
@@ -1345,10 +1334,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=
@@ -1386,8 +1374,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=
@@ -1435,10 +1423,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=
@@ -1447,7 +1434,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=
@@ -1466,8 +1452,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.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=
@@ -1514,7 +1500,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=
@@ -1532,10 +1517,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=
@@ -1547,10 +1531,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=
@@ -1566,10 +1549,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=
@@ -1718,7 +1700,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=
+29 -5
View File
@@ -4,6 +4,8 @@ import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
@@ -87,11 +89,7 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
tmp.Update()
u.uploadURL = tmp.URL
// create http client
opts := retryablehttp.DefaultOptionsSingle
opts.NoAdjustTimeout = true
opts.Timeout = time.Duration(3) * time.Minute
u.client = retryablehttp.NewClient(opts)
u.client = newUploadClient()
// create context
ctx, u.cancel = context.WithCancel(ctx)
@@ -101,6 +99,32 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
return u, nil
}
// newUploadClient returns the client for cloud uploads. The upload sends the
// API key and the scan results, so the client verifies the server certificate.
// The retryablehttp default clients skip verification.
func newUploadClient() *retryablehttp.Client {
transport := retryablehttp.DefaultReusePooledTransport()
// fastdialer DialTLS does not verify certificates, so net/http does the
// TLS handshake over the fastdialer connection.
transport.DialTLSContext = nil
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
opts := retryablehttp.DefaultOptionsSingle
opts.NoAdjustTimeout = true
opts.Timeout = time.Duration(3) * time.Minute
opts.HttpClient = &http.Client{Transport: transport}
// a certificate error does not go away on retry
defaultRetry := retryablehttp.DefaultRetryPolicy()
opts.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
var certErr *tls.CertificateVerificationError
if errors.As(err, &certErr) {
return false, nil
}
return defaultRetry(ctx, resp, err)
}
return retryablehttp.NewClient(opts)
}
// SetScanID sets the scan id for the upload writer
func (u *UploadWriter) SetScanID(id string) error {
if !xidRegex.MatchString(id) {
+55
View File
@@ -0,0 +1,55 @@
package pdcp
import (
"context"
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/projectdiscovery/gologger"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
"github.com/stretchr/testify/require"
)
func newTestUploadWriter(t *testing.T, gotKey *atomic.Value) (*UploadWriter, *httptest.Server) {
t.Helper()
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotKey.Store(r.Header.Get(pdcpauth.ApiKeyHeaderName))
_, _ = w.Write([]byte(`{"id":"abcdefghij0123456789"}`))
}))
t.Cleanup(srv.Close)
creds := &pdcpauth.PDCPCredentials{APIKey: "test-key", Server: srv.URL}
u, err := NewUploadWriter(context.Background(), gologger.DefaultLogger, creds)
require.NoError(t, err)
t.Cleanup(u.Close)
return u, srv
}
func TestUploadRejectsUntrustedCertificate(t *testing.T) {
var gotKey atomic.Value
u, _ := newTestUploadWriter(t, &gotKey)
err := u.upload([]byte("{}\n"))
require.Error(t, err)
var unknownAuthority x509.UnknownAuthorityError
require.ErrorAs(t, err, &unknownAuthority)
require.Nil(t, gotKey.Load(), "API key must not reach a server with an untrusted certificate")
}
func TestUploadTrustedCertificate(t *testing.T) {
var gotKey atomic.Value
u, srv := newTestUploadWriter(t, &gotKey)
pool := x509.NewCertPool()
pool.AddCert(srv.Certificate())
transport, ok := u.client.HTTPClient.Transport.(*http.Transport)
require.True(t, ok)
transport.TLSClientConfig = &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}
require.NoError(t, u.upload([]byte("{}\n")))
require.Equal(t, "test-key", gotKey.Load())
require.Equal(t, "abcdefghij0123456789", u.scanID)
}
+10 -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"
@@ -38,6 +39,9 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
}
httpxOptions.RetryMax = r.options.Retries
httpxOptions.Timeout = time.Duration(r.options.Timeout) * time.Second
if len(r.options.InternalResolversList) > 0 {
httpxOptions.Resolvers = r.options.InternalResolversList
}
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
if dialers == nil {
@@ -68,12 +72,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 {
+35 -1
View File
@@ -2,8 +2,12 @@ package runner
import (
"context"
"errors"
"sync"
"time"
"github.com/projectdiscovery/gologger"
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
utilsllm "github.com/projectdiscovery/utils/llm"
@@ -28,6 +32,32 @@ func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON b
})
}
// llmJudgeClient is an llmMatcherClient whose provider classifies natively, so
// llm matchers ask it through Judge instead of Complete.
type llmJudgeClient struct {
*llmMatcherClient
warnComplete sync.Once
}
func (a *llmJudgeClient) Judge(ctx context.Context, question string, labels, inputs []string) (llmclient.Verdict, error) {
verdict, err := a.client.Judge(ctx, utilsllm.JudgeRequest{Question: question, Labels: labels, Inputs: inputs})
if err != nil {
return llmclient.Verdict{}, err
}
return llmclient.Verdict{Label: verdict.Label, Confidence: verdict.Confidence, Model: verdict.Model}, nil
}
// Complete is only reached by llm extractors, which need free text a
// classifier cannot produce. Returning here spends no call budget and
// makes no provider request. Extractors turn the error into an empty
// result, and the warning says why.
func (a *llmJudgeClient) Complete(context.Context, string, bool) (string, error) {
a.warnComplete.Do(func() {
gologger.Warning().Msgf("llm provider %q cannot run llm extractors, they return nothing", utilsllm.ProviderJev)
})
return "", errors.New("llm provider cannot run extractors")
}
// configureLLM builds the scan's llm client from options. It returns nil unless
// -llm is set, so nothing reaches a model until the user opts in. The API key is
// read from the environment by the provider layer, never from options.
@@ -52,5 +82,9 @@ func configureLLM(options *types.Options) (llmclient.Client, error) {
return nil, err
}
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
matcherClient := &llmMatcherClient{client: client, model: options.LLMModel}
if client.CanJudge() {
return &llmJudgeClient{llmMatcherClient: matcherClient}, nil
}
return matcherClient, nil
}
+65
View File
@@ -0,0 +1,65 @@
package runner
import (
"context"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestConfigureLLMExposesJudgeOnlyForClassifiers(t *testing.T) {
t.Setenv("LLM_API_KEY", "key")
client, err := configureLLM(&types.Options{EnableLLM: true, LLMProvider: "jev", LLMModel: "jev-latest"})
require.NoError(t, err)
require.Implements(t, (*llmclient.Judge)(nil), client)
client, err = configureLLM(&types.Options{EnableLLM: true, LLMProvider: "ollama", LLMModel: "llama3.2"})
require.NoError(t, err)
_, isJudge := client.(llmclient.Judge)
require.False(t, isJudge, "chat providers keep the Complete path")
}
func TestJevExtractorDoesNotSpendCallBudget(t *testing.T) {
t.Setenv("LLM_API_KEY", "key")
// ProxyFromEnvironment caches the first request in the process. Match the
// proxy the interactsh client test expects, and keep the local server direct.
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
t.Setenv(key, "")
}
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
_, _ = w.Write([]byte(`{"model":"jev-1.14.0","answers":{"q":{"type":"noul","noul":0.9}}}`))
}))
t.Cleanup(server.Close)
client, err := configureLLM(&types.Options{
EnableLLM: true,
LLMProvider: "jev",
LLMBaseURL: server.URL + "/v1",
LLMModel: "jev-latest",
LLMMaxCalls: 1,
})
require.NoError(t, err)
_, err = client.Complete(context.Background(), "extract the title", true)
require.ErrorContains(t, err, "cannot run extractors")
_, err = client.Complete(context.Background(), "extract another", true)
require.Error(t, err)
require.Zero(t, calls.Load())
judge := client.(llmclient.Judge)
verdict, err := judge.Judge(context.Background(), "login page?", nil, []string{"<form>"})
require.NoError(t, err)
require.Equal(t, "yes", verdict.Label)
require.EqualValues(t, 1, calls.Load())
}
+24 -18
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)
}
@@ -211,25 +228,14 @@ func ValidateOptions(options *types.Options) error {
}
}
// verify that a valid ip version type was selected (4, 6)
if len(options.IPVersion) == 0 {
// add ipv4 as default
options.IPVersion = append(options.IPVersion, "4")
}
var useIPV4, useIPV6 bool
// An empty -iv leaves one unpinned target so the dialer can fall back
// between address families. -sa resolves and scans every selected address.
// Explicit -iv 4, -iv 6 and -iv 4,6 retain their usual selection semantics.
for _, ipv := range options.IPVersion {
switch ipv {
case "4":
useIPV4 = true
case "6":
useIPV6 = true
default:
if ipv != "4" && ipv != "6" {
return fmt.Errorf("unsupported ip version: %s", ipv)
}
}
if !useIPV4 && !useIPV6 {
return errors.New("ipv4 and/or ipv6 must be selected")
}
return nil
}
+43
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,45 @@ 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))
})
}
}
// The default must remain empty so the provider can distinguish automatic
// IPv6 preference from explicit user-supplied -iv selections.
func TestValidateOptionsIPVersionPreference(t *testing.T) {
automatic := &types.Options{}
require.NoError(t, ValidateOptions(automatic))
require.Empty(t, automatic.IPVersion)
for _, versions := range []goflags.StringSlice{{"4"}, {"6"}, {"4", "6"}} {
require.NoError(t, ValidateOptions(&types.Options{IPVersion: versions}))
}
require.ErrorContains(t, ValidateOptions(&types.Options{IPVersion: goflags.StringSlice{"7"}}), "unsupported ip version")
}
+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)
}
})
}
+59 -11
View File
@@ -3,6 +3,8 @@ package runner
import (
"context"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
@@ -56,6 +58,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"
@@ -64,6 +67,7 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
@@ -85,7 +89,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
@@ -160,7 +164,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)
}
@@ -177,7 +183,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")
}
@@ -222,7 +233,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{}, "")
@@ -421,13 +432,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)
@@ -727,6 +732,13 @@ func (r *Runner) RunEnumeration() error {
executorOpts.HostErrorsCache = cache
}
if r.options.HostBackoff {
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
}
if r.options.AutoCalibrate {
executorOpts.CatchAll = catchall.New(0)
}
executorEngine := core.New(r.options)
executorEngine.SetExecuterOptions(executorOpts)
@@ -745,6 +757,11 @@ func (r *Runner) RunEnumeration() error {
}
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
@@ -789,6 +806,11 @@ 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
@@ -987,7 +1009,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{}
@@ -1054,6 +1076,7 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
stats.ForceDisplayWarning(templates.SkippedUnverifiedTemplateStats)
stats.ForceDisplayWarning(templates.SkippedRequestSignatureTemplateStats)
stats.ForceDisplayWarning(templates.SkippedWorkflowStepStats)
cfg := config.DefaultConfig
@@ -1230,3 +1253,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)
}
+120
View File
@@ -1,13 +1,24 @@
package runner
import (
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"sync/atomic"
"testing"
"time"
"github.com/miekg/dns"
"github.com/projectdiscovery/gologger"
"github.com/stretchr/testify/require"
"github.com/projectdiscovery/nuclei/v3/pkg/input"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
@@ -143,3 +154,112 @@ 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())
}
func TestInitializeTemplatesHTTPInputUsesResolvers(t *testing.T) {
for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"} {
t.Setenv(key, "")
}
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
queries := new(atomic.Int32)
server := &dns.Server{PacketConn: listener, Handler: dns.HandlerFunc(func(writer dns.ResponseWriter, request *dns.Msg) {
queries.Add(1)
response := new(dns.Msg).SetReply(request)
for _, question := range request.Question {
if question.Qtype == dns.TypeA {
response.Answer = append(response.Answer, &dns.A{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET}, A: net.ParseIP("127.0.0.1")})
}
}
_ = writer.WriteMsg(response)
})}
started := make(chan struct{})
server.NotifyStartedFunc = func() { close(started) }
done := make(chan error, 1)
go func() { done <- server.ActivateAndServe() }()
<-started
t.Cleanup(func() { require.NoError(t, server.Shutdown()); require.NoError(t, <-done) })
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(srv.Close)
resolverFile := filepath.Join(t.TempDir(), "resolv.conf")
require.NoError(t, os.WriteFile(resolverFile, nil, 0o600))
t.Setenv("RESOLVERS_PATH", resolverFile)
options := &types.Options{
ExecutionId: t.Name(),
Timeout: 5,
BulkSize: 1,
InternalResolversList: []string{listener.LocalAddr().String()},
}
require.NoError(t, protocolstate.Init(options))
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
target := "probe.nuclei.test:" + strconv.Itoa(srv.Listener.Addr().(*net.TCPAddr).Port)
r := &Runner{
options: options,
inputProvider: provider.NewSimpleInputProviderWithUrls(options.ExecutionId, target),
Logger: &gologger.Logger{},
}
hm, err := r.initializeTemplatesHTTPInput()
require.NoError(t, err)
t.Cleanup(func() { _ = hm.Close() })
probed, ok := hm.Get(input.ProbeKey(target, ""))
require.True(t, ok, "the probe should resolve the target through the -r resolver")
require.Equal(t, "http://"+target, string(probed))
require.Positive(t, queries.Load())
}
+24
View File
@@ -1,7 +1,9 @@
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
@@ -15,3 +17,25 @@ func (r *Runner) targetProfiles() *targetprofile.Registry {
}
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)
})
}
}
+43 -8
View File
@@ -6,13 +6,15 @@ import (
"sort"
"strings"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/v2/quick"
"github.com/logrusorgru/aurora/v4"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
@@ -38,12 +40,17 @@ func (r *Runner) verboseTemplate(tpl *templates.Template) {
tpl.Info.SeverityHolder.Severity))
}
// listedTemplate names its fields like a result event, so a -jsonl listing
// joins with scan results on the same keys.
type listedTemplate struct {
Template string `json:"template,omitempty"`
TemplateID string `json:"template-id"`
TemplatePath string `json:"template-path"`
Info model.Info `json:"info"`
}
func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
r.Logger.Print().Msgf(
"\nListing available %v nuclei templates for %v",
config.DefaultConfig.TemplateVersion,
config.DefaultConfig.TemplatesDirectory,
)
r.listingHeader("templates")
// order templates alphabetically by path
templates := store.Templates()
sort.Slice(templates, func(i, j int) bool {
@@ -51,6 +58,10 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
})
for _, tpl := range templates {
if r.options.JSONL && !r.options.TemplateDisplay {
r.listTemplateJSON(tpl)
continue
}
if hasExtraFlags(r.options) {
if r.options.TemplateDisplay {
colorize := !r.options.NoColor
@@ -78,12 +89,36 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
}
}
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
func (r *Runner) listTemplateJSON(tpl *templates.Template) {
path, _ := utils.TemplatePathURL(tpl.Path, tpl.ID, "")
marshalled, err := json.Marshal(listedTemplate{
Template: path,
TemplateID: tpl.ID,
TemplatePath: tpl.Path,
Info: tpl.Info,
})
if err != nil {
r.Logger.Error().Msgf("Could not marshal the template %s: %s", tpl.Path, err)
return
}
r.Logger.Print().Msgf("%s", marshalled)
}
// listingHeader is skipped for -jsonl, where every line must parse as JSON.
func (r *Runner) listingHeader(what string) {
if r.options.JSONL {
return
}
r.Logger.Print().Msgf(
"\nListing available %v nuclei tags for %v",
"\nListing available %v nuclei %s for %v",
config.DefaultConfig.TemplateVersion,
what,
config.DefaultConfig.TemplatesDirectory,
)
}
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
r.listingHeader("tags")
type kv struct {
Key string `json:"tag"`
+76
View File
@@ -0,0 +1,76 @@
package runner
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/formatter"
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/stretchr/testify/require"
)
const listedTemplateYAML = `id: listed-template
info:
name: Listed Template
author: pdteam
severity: info
tags: listing
http:
- method: GET
path:
- "{{BaseURL}}"
`
func TestTemplateListWritesJSONLines(t *testing.T) {
root := t.TempDir()
cfg := config.DefaultConfig
oldRoot := cfg.TemplatesDirectory
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
cfg.SetTemplatesDir(root)
path := filepath.Join(root, "http", "listed-template.yaml")
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(listedTemplateYAML), 0o600))
writer := &ignoreFileLogWriter{}
logger := &gologger.Logger{}
logger.SetFormatter(formatter.NewCLI(true))
logger.SetWriter(writer)
logger.SetMaxLevel(levels.LevelInfo)
options := testutils.DefaultOptions.Copy()
options.ExecutionId = t.Name()
options.Logger = logger
options.Templates = []string{path}
options.TemplateList = true
options.JSONL = true
testutils.Init(options)
t.Cleanup(func() { testutils.Cleanup(options) })
executerOpts := testutils.NewMockExecuterOptions(options, nil)
executerOpts.Parser = templates.NewParser()
store, err := loader.New(loader.NewConfig(options, disk.NewCatalog(root), executerOpts))
require.NoError(t, err)
require.NoError(t, store.LoadTemplatesOnlyMetadata())
require.Len(t, store.Templates(), 1)
r := &Runner{options: options, Logger: logger}
r.listAvailableStoreTemplates(store)
require.Len(t, writer.records, 1, "JSONL output carries no banner line")
var listed map[string]any
require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(writer.records[0].message)), &listed))
require.Equal(t, "listed-template", listed["template-id"])
require.Equal(t, filepath.Join("http", "listed-template.yaml"), listed["template"])
require.Equal(t, "Listed Template", listed["info"].(map[string]any)["name"])
}
+10 -1
View File
@@ -28,10 +28,12 @@ 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"
browserEngine "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
@@ -51,7 +53,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 +108,13 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
executorOpts.HostErrorsCache = cache
}
if opts.Options.HostBackoff {
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
}
if opts.Options.AutoCalibrate {
executorOpts.CatchAll = catchall.New(0)
}
executorEngine := core.New(opts.Options)
executorEngine.SetExecuterOptions(executorOpts)
+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
+124
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{}},
@@ -99,6 +102,7 @@ var httpTestcases = []integrationCase{
{Path: "protocols/http/raw-unsafe-path-single-slash.yaml", TestCase: &httpRawUnsafePathSingleSlash{}},
{Path: "protocols/http/disable-http-cache.yaml", TestCase: &httpDisableCache{}},
{Path: "protocols/http/http-cache.yaml", TestCase: &httpCache{}},
{Path: "protocols/http/get.yaml", TestCase: &httpExcludeHostsFileComments{}},
}
type httpMultiVarSharing struct{}
@@ -1606,6 +1610,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{}
@@ -1916,3 +1984,59 @@ func (h *httpDisableCache) Execute(filePath string) error {
return nil
}
// ensure that comments in an -exclude-hosts file are ignored
type httpExcludeHostsFileComments struct{}
// Execute executes a test case and returns an error if occurred
func (h *httpExcludeHostsFileComments) Execute(filePath string) error {
router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
_, _ = fmt.Fprintf(w, "This is test matcher text")
})
ts := httptest.NewServer(router)
defer ts.Close()
excludeHosts := func(content string) ([]string, error) {
file, err := os.CreateTemp("", "exclude-hosts-*.txt")
if err != nil {
return nil, err
}
defer func() { _ = os.Remove(file.Name()) }()
_, err = file.WriteString(content)
_ = file.Close()
if err != nil {
return nil, err
}
return testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", file.Name())
}
// a comment line is not a host, even when it reads as an invalid regex
results, err := excludeHosts("# hosts out of scope (see ticket\n203.0.113.10\n")
if err != nil {
return fmt.Errorf("comment line: %w", err)
}
if err := expectResultsCount(results, 1); err != nil {
return fmt.Errorf("comment line: %w", err)
}
// a trailing comment does not stop the host from being excluded
results, err = excludeHosts("127.0.0.1 # test server\n")
if err != nil {
return fmt.Errorf("trailing comment: %w", err)
}
if err := expectResultsCount(results, 0); err != nil {
return fmt.Errorf("trailing comment: %w", err)
}
// comma separated hosts are still split when passed on the command line
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", "203.0.113.10, 127.0.0.1")
if err != nil {
return fmt.Errorf("comma separated: %w", err)
}
if err := expectResultsCount(results, 0); err != nil {
return fmt.Errorf("comma separated: %w", err)
}
return nil
}
@@ -0,0 +1,89 @@
//go:build integration
// +build integration
package integration_test
import (
"fmt"
"net"
"time"
"github.com/ory/dockertest/v3"
)
// ipmiSimSetup runs OpenIPMI's BMC simulator with an anonymous user,
// ADMIN/ADMIN and root/calvin, and cipher suite 0 enabled.
const ipmiSimSetup = `apt-get update -qq && apt-get install -y -qq --no-install-recommends openipmi >/dev/null && mkdir -p /etc/ipmi && cat > /etc/ipmi/lan.conf <<'EOF'
name "bmc"
set_working_mc 0x20
startlan 1
addr 0.0.0.0 623
priv_limit admin
allowed_auths_callback none md2 md5 straight
allowed_auths_user none md2 md5 straight
allowed_auths_operator none md2 md5 straight
allowed_auths_admin none md2 md5 straight
guid a123456789abcdefa123456789abcdef
endlan
user 1 true "" "" user 10 none md2 md5 straight
user 2 true "ADMIN" "ADMIN" admin 10 none md2 md5 straight
user 3 true "root" "calvin" admin 10 none md2 md5 straight
EOF
printf 'mc_setbmc 0x20\nmc_add 0x20 0 no-device-sdrs 0x23 9 8 0x9f 0x1291 0xf02 persist_sdr\nmc_enable 0x20\n' > /etc/ipmi/emu.cmd
exec ipmi_sim -c /etc/ipmi/lan.conf -f /etc/ipmi/emu.cmd -n`
type javascriptIPMI struct{}
func (j *javascriptIPMI) Execute(filePath string) error {
pool, err := dockertest.NewPool("")
if err != nil {
return fmt.Errorf("could not create docker pool: %w", err)
}
if err := pool.Client.Ping(); err != nil {
return fmt.Errorf("could not connect to Docker: %w", err)
}
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: "debian",
Tag: "bookworm-slim",
Platform: "linux/amd64",
Cmd: []string{"sh", "-c", ipmiSimSetup},
ExposedPorts: []string{"623/udp"},
})
if err != nil {
return fmt.Errorf("could not start ipmi simulator: %w", err)
}
defer purge(pool, resource)
address := net.JoinHostPort("127.0.0.1", resource.GetPort("623/udp"))
if err := waitForServiceCheck(address, 3*time.Minute, rmcpPingCheck); err != nil {
return err
}
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
// rmcpPingCheck waits for an answer to an RMCP presence ping, since the
// port is udp and cannot be probed with a connect.
func rmcpPingCheck(address string) error {
conn, err := net.Dial("udp", address)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
if _, err := conn.Write([]byte{0x06, 0x00, 0xff, 0x06, 0x00, 0x00, 0x11, 0xbe, 0x80, 0x00, 0x00, 0x00}); err != nil {
return err
}
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
reply := make([]byte, 64)
n, err := conn.Read(reply)
if err != nil {
return err
}
if n < 9 || reply[8] != 0x40 {
return fmt.Errorf("unexpected rmcp reply")
}
return nil
}
@@ -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,6 +37,7 @@ 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{}},
@@ -42,6 +46,7 @@ var jsTestcases = []integrationCase{
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
{Path: "protocols/javascript/ipmi.yaml", TestCase: &javascriptIPMI{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
@@ -467,6 +472,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}
@@ -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,46 @@
id: ipmi-checks
info:
name: IPMI Checks
author: pdteam
severity: info
javascript:
- code: |
const ipmi = require('nuclei/ipmi');
const caps = ipmi.GetAuthCapabilities(Host, Port);
const root = ipmi.GetRAKPHash(Host, Port, 'root');
// cipher suite 0 lets a wrong password in
const anyPassword = new ipmi.Client();
const cipherZero = anyPassword.Login(Host, Port, 'ADMIN', 'wrong', 0);
anyPassword.Close();
// an encrypted, signed session, then Get Device ID and admin privilege
const client = new ipmi.Client();
client.Login(Host, Port, 'ADMIN', 'ADMIN', 3);
const device = client.Command(0x06, 0x01, []);
const admin = client.Command(0x06, 0x3b, [0x04]);
client.Close();
const manufacturer = device.Data[6] | device.Data[7] << 8 | device.Data[8] << 16;
let wrongRejected = false;
try { new ipmi.Client().Login(Host, Port, 'ADMIN', 'wrong', 3); } catch (e) { wrongRejected = true; }
ipmi.Ping(Host, Port) && caps.IPMI20 && caps.AnonymousLogin &&
ipmi.GetCipherSuites(Host, Port).includes(0) &&
ipmi.GetSystemGUID(Host, Port).length == 32 &&
ipmi.FindRAKPPassword(root, ['admin', 'calvin']) == 'calvin' &&
!ipmi.CheckRAKPPassword(root, 'admin') &&
cipherZero && wrongRejected &&
device.CompletionCode == 0 && manufacturer == 4753 &&
admin.CompletionCode == 0;
args:
Host: "{{Host}}"
Port: "623"
matchers:
- type: dsl
dsl:
- "success == true"
@@ -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]+"
@@ -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
+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)
}
})
}
}
+3 -1
View File
@@ -367,7 +367,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
+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")
}
+1 -5
View File
@@ -44,11 +44,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()
+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())
}
+16 -4
View File
@@ -70,7 +70,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 +151,9 @@ func (e *NucleiEngine) GetTemplates() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
if e.store == nil {
return nil
}
return e.store.Templates()
}
@@ -159,6 +162,9 @@ func (e *NucleiEngine) GetWorkflows() []*templates.Template {
if !e.templatesLoaded {
_ = e.LoadAllTemplates()
}
if e.store == nil {
return nil
}
return e.store.Workflows()
}
@@ -251,7 +257,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 +303,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 {
@@ -378,6 +386,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
+37 -8
View File
@@ -44,6 +44,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 +187,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 +210,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
@@ -359,6 +358,9 @@ func (e *NucleiEngine) init(ctx context.Context) error {
httpxOptions := httpx.DefaultOptions
httpxOptions.Timeout = 5 * time.Second
if len(e.opts.InternalResolversList) > 0 {
httpxOptions.Resolvers = e.opts.InternalResolversList
}
if client, err := httpx.New(&httpxOptions); err != nil {
return err
} else {
@@ -382,6 +384,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 +411,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 +423,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")
}
+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()
+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"}
+12 -4
View File
@@ -7,18 +7,22 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/v2/quick"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
"github.com/projectdiscovery/utils/errkit"
)
const (
var (
aiTemplateGeneratorAPIEndpoint = "https://api.projectdiscovery.io/v1/template/ai"
// aiTemplateHTTPClient verifies TLS, since the request carries the PDCP API key.
aiTemplateHTTPClient = &http.Client{}
// aiTemplateIDRegex keeps the template file name inside the pdcp directory.
aiTemplateIDRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
)
type AITemplateResponse struct {
@@ -112,7 +116,7 @@ func generateAITemplate(prompt string) (string, string, error) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set(pdcpauth.ApiKeyHeaderName, creds.APIKey)
resp, err := retryablehttp.DefaultClient().Do(req)
resp, err := aiTemplateHTTPClient.Do(req)
if err != nil {
return "", "", errkit.Newf("Failed to send HTTP request: %v", err)
}
@@ -138,5 +142,9 @@ func generateAITemplate(prompt string) (string, string, error) {
return "", "", errkit.Newf("Failed to generate template")
}
if !aiTemplateIDRegex.MatchString(result.TemplateID) {
return "", "", errkit.Newf("Invalid template ID in API response: %q", result.TemplateID)
}
return result.Completion, result.TemplateID, nil
}
+55
View File
@@ -0,0 +1,55 @@
package loader
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
)
func newAITemplateServer(t *testing.T, templateID string, gotKey *atomic.Value) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotKey.Store(r.Header.Get("X-Api-Key"))
_ = json.NewEncoder(w).Encode(map[string]string{
"completion": "id: test\n",
"template_id": templateID,
})
}))
t.Cleanup(srv.Close)
oldEndpoint, oldClient := aiTemplateGeneratorAPIEndpoint, aiTemplateHTTPClient
t.Cleanup(func() { aiTemplateGeneratorAPIEndpoint, aiTemplateHTTPClient = oldEndpoint, oldClient })
aiTemplateGeneratorAPIEndpoint = srv.URL
t.Setenv("PDCP_API_KEY", "test-key")
if templateID != "" {
// Trust the test certificate, to check the template_id only.
aiTemplateHTTPClient = srv.Client()
}
}
func TestAITemplateVerifiesTLS(t *testing.T) {
var gotKey atomic.Value
newAITemplateServer(t, "", &gotKey)
_, _, err := generateAITemplate("check for exposed admin panels")
require.ErrorContains(t, err, "certificate")
require.Nil(t, gotKey.Load(), "the API key went to an untrusted server")
}
func TestAITemplateRejectsUnsafeID(t *testing.T) {
for _, id := range []string{"../../.config/nuclei/config", "a/b", `a\b`, "a.b", "a b"} {
var gotKey atomic.Value
newAITemplateServer(t, id, &gotKey)
_, _, err := generateAITemplate("check for exposed admin panels")
require.Error(t, err, id)
}
var gotKey atomic.Value
newAITemplateServer(t, "Abc-123_x", &gotKey)
_, templateID, err := generateAITemplate("check for exposed admin panels")
require.NoError(t, err)
require.Equal(t, "Abc-123_x", templateID)
}
+10 -4
View File
@@ -631,10 +631,10 @@ func (store *Store) noteMissingCapabilities(templatePath string, missingCaps []t
// erroring out on discovering any faulty templates.
func (store *Store) ValidateTemplates() error {
templatePaths, errs := store.config.Catalog.GetTemplatesPath(store.finalTemplates)
store.logErroredTemplates(errs)
notFound := store.logErroredTemplates(errs)
workflowPaths, errs := store.config.Catalog.GetTemplatesPath(store.finalWorkflows)
store.logErroredTemplates(errs)
notFound += store.logErroredTemplates(errs)
templatePathsMap := make(map[string]struct{}, len(templatePaths))
for _, path := range templatePaths {
@@ -646,7 +646,8 @@ func (store *Store) ValidateTemplates() error {
workflowPathsMap[path] = struct{}{}
}
if store.areTemplatesValid(templatePathsMap) && store.areWorkflowsValid(workflowPathsMap) {
// a path that can't be resolved is not validated, so it can't count as valid
if store.areTemplatesValid(templatePathsMap) && store.areWorkflowsValid(workflowPathsMap) && notFound == 0 {
return nil
}
@@ -1056,10 +1057,15 @@ func workflowContainsProtocol(workflow []*workflows.WorkflowTemplate) bool {
return false
}
func (s *Store) logErroredTemplates(erred map[string]error) {
// logErroredTemplates logs the templates that could not be found and returns
// how many of them were not handled by the NotFoundCallback.
func (s *Store) logErroredTemplates(erred map[string]error) int {
notFound := 0
for template, err := range erred {
if s.NotFoundCallback == nil || !s.NotFoundCallback(template) {
s.logger.Error().Msgf("Could not find template '%s': %s", template, err)
notFound++
}
}
return notFound
}
+43 -20
View File
@@ -176,25 +176,8 @@ func TestLoadTemplatesOnlyMetadataLogsCachedTemplateParseErrors(t *testing.T) {
require.Contains(t, output.String(), "Could not load template")
}
func TestValidateTemplatesRejectsRuntimeCompilationError(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "invalid-regex-group.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(`id: invalid-regex-group
info:
name: Invalid Regex Group
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
extractors:
- type: regex
group: -1
regex:
- "(a)(b)"
`), 0o600))
func newValidateStore(t *testing.T, executionID, templatePath string) (*Store, *bytes.Buffer) {
t.Helper()
options := testutils.DefaultOptions.Copy()
var output bytes.Buffer
@@ -203,7 +186,7 @@ http:
logger.SetWriter(&utils.CaptureWriter{Buffer: &output})
logger.SetMaxLevel(levels.LevelDebug)
options.Logger = logger
options.ExecutionId = "loader-invalid-regex-group"
options.ExecutionId = executionID
options.Templates = []string{templatePath}
options.Validate = true
options.TemplateLoadingConcurrency = 1
@@ -226,10 +209,50 @@ http:
store, err := New(NewConfig(options, catalog, executerOpts))
require.NoError(t, err)
return store, &output
}
func TestValidateTemplatesRejectsRuntimeCompilationError(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "invalid-regex-group.yaml")
require.NoError(t, os.WriteFile(templatePath, []byte(`id: invalid-regex-group
info:
name: Invalid Regex Group
author: pdteam
severity: info
http:
- method: GET
path:
- "{{BaseURL}}"
extractors:
- type: regex
group: -1
regex:
- "(a)(b)"
`), 0o600))
store, output := newValidateStore(t, "loader-invalid-regex-group", templatePath)
require.Error(t, store.ValidateTemplates())
require.Contains(t, output.String(), "regex extractor group must be >= 0")
}
func TestValidateTemplatesRejectsMissingTemplatePath(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "no-such-templates")
store, output := newValidateStore(t, "loader-missing-template-path", templatePath)
require.Error(t, store.ValidateTemplates())
require.Contains(t, output.String(), "Could not find template")
}
func TestValidateTemplatesAcceptsMissingPathHandledByCallback(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "no-such-templates")
store, _ := newValidateStore(t, "loader-missing-path-callback", templatePath)
store.NotFoundCallback = func(string) bool { return true }
require.NoError(t, store.ValidateTemplates())
}
func TestRemoteTemplates(t *testing.T) {
catalog := disk.NewCatalog("")
+46 -23
View File
@@ -63,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), e.expectedRequests(finalTemplates, target, 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
@@ -88,6 +74,23 @@ 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(), "") {
// 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()
}
// Execute All SelfContained in parallel
e.executeAllSelfContained(ctx, selfContained, results, selfcontainedWg)
@@ -123,7 +126,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
@@ -170,7 +173,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)
@@ -180,23 +186,40 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
return results
}
// expectedRequests counts only the template and target pairs that per-target
// profiles select, so progress reflects the requests that will be sent.
func (e *Engine) expectedRequests(templatesList []*templates.Template, target provider.InputProvider, unscoped int) int64 {
// 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(unscoped)
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
})
var total int64
for selection, count := range targets {
total += count * int64(getSelectedRequestCount(templatesList, selection))
requests += count * int64(getSelectedRequestCount(templatesList, selection))
tasks += count * int64(getSelectedCount(targetBound, selection))
}
return total
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
+56 -12
View File
@@ -9,6 +9,8 @@ import (
"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"
@@ -36,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))
@@ -166,12 +169,18 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
currentInfo.Unlock()
// Skip if the host has had errors
if inScope && 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",
@@ -210,7 +219,7 @@ 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
@@ -224,12 +233,12 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
// 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",
@@ -240,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 {
@@ -279,15 +290,46 @@ 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 {
if e.executerOpts.TargetScope == nil {
return true
}
selection := e.executerOpts.TargetScope.For(input)
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) (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
@@ -300,9 +342,11 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
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:
+111
View File
@@ -10,6 +10,7 @@ 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"
@@ -655,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")
}
}
+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")
})
}
}
+53 -9
View File
@@ -3,6 +3,7 @@ package core
import (
"fmt"
"net/http/cookiejar"
"sync"
"sync/atomic"
"github.com/projectdiscovery/gologger"
@@ -32,22 +33,45 @@ func (e *Engine) executeWorkflow(ctx *scan.ScanContext, w *workflows.Workflow) b
templateThreads++
}
swg, _ := syncutil.New(syncutil.WithSize(templateThreads))
// pending tracks every started step until it returns. swg only limits
// concurrency and its slots can be released early, so completion is
// tracked here instead.
var pending sync.WaitGroup
for _, template := range w.Workflows {
newCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(template, newCtx, results, swg, w); err != nil {
// Root steps do not own a waitgroup slot.
if err := e.runWorkflowStep(template, newCtx, results, swg, &pending, w, nil); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
}
swg.Wait()
pending.Wait()
return results.Load()
}
// slotRelease returns a func that gives back one swg slot. It is safe to call
// from several goroutines and several times; only the first call releases.
func slotRelease(swg *syncutil.AdaptiveWaitGroup) func() {
var once sync.Once
return func() {
once.Do(swg.Done)
}
}
// runWorkflowStep runs a workflow step for the workflow. It executes the workflow
// in a recursive manner running all subtemplates and matchers.
func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan.ScanContext, results *atomic.Bool, swg *syncutil.AdaptiveWaitGroup, w *workflows.Workflow) error {
//
// releaseSlot, when non-nil, releases the AdaptiveWaitGroup slot held by the
// goroutine that is running this step. It must be called before this step
// blocks on swg.AddWithContext for child steps; otherwise every in-flight
// parent can hold a slot while waiting for another, which deadlocks when the
// number of matching nested steps reaches TemplateThreads (see #8074).
//
// pending is incremented for every child step before it starts, and each child
// calls Done when it returns, so the root can wait for the whole tree.
func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan.ScanContext, results *atomic.Bool, swg *syncutil.AdaptiveWaitGroup, pending *sync.WaitGroup, w *workflows.Workflow, releaseSlot func()) error {
var firstMatched bool
var err error
var mainErr error
@@ -131,16 +155,25 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
continue
}
// Free this step's slot before blocking on child acquires.
if releaseSlot != nil {
releaseSlot()
}
for _, subtemplate := range matcher.Subtemplates {
swg.Add()
if err := swg.AddWithContext(ctx.Context()); err != nil {
return
}
pending.Add(1)
go func(subtemplate *workflows.WorkflowTemplate) {
defer swg.Done()
defer pending.Done()
release := slotRelease(swg)
defer release()
// create a new context with the same input but with unset callbacks
// clone the Input so that other parallel executions won't overwrite the shared variables when subsequent templates are running
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(subtemplate, subCtx, results, swg, w); err != nil {
if err := e.runWorkflowStep(subtemplate, subCtx, results, swg, pending, w, release); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, subtemplate.Template, err)
}
}(subtemplate)
@@ -160,16 +193,27 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
return mainErr
}
if len(template.Subtemplates) > 0 && firstMatched {
// Free this step's slot before blocking on child acquires so nested
// matching steps cannot exhaust TemplateThreads and deadlock (#8074).
if releaseSlot != nil {
releaseSlot()
}
for _, subtemplate := range template.Subtemplates {
swg.Add()
if err := swg.AddWithContext(ctx.Context()); err != nil {
return err
}
pending.Add(1)
go func(template *workflows.WorkflowTemplate) {
defer pending.Done()
release := slotRelease(swg)
defer release()
// create a new context with the same input but with unset callbacks
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(template, subCtx, results, swg, w); err != nil {
if err := e.runWorkflowStep(template, subCtx, results, swg, pending, w, release); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
swg.Done()
}(subtemplate)
}
}
+140
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,144 @@ func TestWorkflowsSameStepExecutersRefreshTemplateContext(t *testing.T) {
require.Equal(t, "foo", secondWhoami, "could not refresh workflow context into later executers in the same step")
}
func TestWorkflowNestedSubtemplatesDoNotDeadlockAtConcurrencyLimit(t *testing.T) {
// Repro for #8074: with TemplateThreads equal to the number of matching
// nested parents, each parent used to hold a waitgroup slot while calling
// AddWithContext for its children, so every slot was occupied by a parent
// waiting for another slot and the workflow never finished.
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
matchOutputs := []*output.InternalWrappedEvent{{
OperatorsResult: &operators.Result{},
Results: []*output.ResultEvent{{}},
}}
const parents = 3
var entered atomic.Int32
allHolding := make(chan struct{})
releaseParents := make(chan struct{})
leafRuns := &atomic.Int32{}
leaf := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
leafRuns.Add(1)
}}
parentExec := &mockExecuter{
result: true,
outputs: matchOutputs,
executeScanHook: func(ctx *scan.ScanContext) {
if entered.Add(1) == parents {
close(allHolding)
}
// Stay inside ExecuteWithResults so the parent still owns its
// waitgroup slot while every peer also holds one.
<-releaseParents
},
}
mkParent := func() *workflows.WorkflowTemplate {
return &workflows.WorkflowTemplate{
Executers: []*workflows.ProtocolExecuterPair{{
Executer: parentExec,
Options: &protocols.ExecutorOptions{Progress: progressBar},
}},
Subtemplates: []*workflows.WorkflowTemplate{{
Executers: []*workflows.ProtocolExecuterPair{{
Executer: leaf,
Options: &protocols.ExecutorOptions{Progress: progressBar},
}},
}},
}
}
workflow := &workflows.Workflow{
Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: parents}},
Workflows: []*workflows.WorkflowTemplate{{
Executers: []*workflows.ProtocolExecuterPair{{
Executer: &mockExecuter{result: true, outputs: matchOutputs},
Options: &protocols.ExecutorOptions{Progress: progressBar},
}},
Subtemplates: []*workflows.WorkflowTemplate{mkParent(), mkParent(), mkParent()},
}},
}
engine := &Engine{}
input := contextargs.NewWithInput(context.Background(), "https://test.com")
ctx := scan.NewScanContext(context.Background(), input)
done := make(chan bool, 1)
go func() {
done <- engine.executeWorkflow(ctx, workflow)
}()
select {
case <-allHolding:
case <-time.After(2 * time.Second):
t.Fatal("nested parents did not all acquire TemplateThreads slots")
}
close(releaseParents)
select {
case matched := <-done:
require.True(t, matched, "nested workflow should report a match")
require.EqualValues(t, parents, leafRuns.Load(), "each nested parent should run its leaf")
case <-time.After(3 * time.Second):
t.Fatal("nested workflow deadlocked at TemplateThreads == matching nested parents")
}
}
func TestWorkflowSubtemplatesUnblockCancelledAcquire(t *testing.T) {
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
var started atomic.Int32
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) {
+2 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
"github.com/projectdiscovery/retryablehttp-go"
)
@@ -147,10 +148,7 @@ func (b *Body) Rebuild() (*retryablehttp.Request, error) {
return nil, errors.Wrap(err, "could not encode body")
}
cloned := b.req.Clone(context.Background())
err = cloned.SetBodyString(encoded)
if err != nil {
return nil, errors.Wrap(err, "could not set body")
}
httpbody.SetString(cloned, encoded)
cloned.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
return cloned, nil
}
+2
View File
@@ -9,6 +9,7 @@ import (
"sync"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
"github.com/projectdiscovery/retryablehttp-go"
urlutil "github.com/projectdiscovery/utils/url"
"github.com/stretchr/testify/require"
@@ -45,6 +46,7 @@ func TestBodyComponent(t *testing.T) {
t.Fatal(err)
}
require.IsType(t, &httpbody.Body{}, rebuilt.Body, "rebuilt body must not be copied per read")
newBody, err := io.ReadAll(rebuilt.Body)
if err != nil {
t.Fatal(err)
+14 -18
View File
@@ -155,23 +155,14 @@ func (rule *Rule) Execute(input *ExecuteRuleInput) (err error) {
baseInteractURLs := append([]string(nil), input.InteractURLs...)
if rule.generator == nil {
for _, component := range finalComponentList {
var dataKeys map[string]struct{}
optionVars := rule.options.Options.Vars.AsMap()
constants := rule.options.Constants
dataValues := rule.options.NewVariablesScope(baseValues, optionVars, constants)
// get vars from variables while replacing interactsh urls
// Render variables once, allocating their interactsh URLs. The
// output may contain runtime data, so it is never rendered again.
evaluation := rule.options.Variables.EvaluateWithInteractshScope(dataValues, rule.options.Interactsh)
evaluatedValues, interactURLs := evaluation.Values, evaluation.InteractURLs
input.Values, dataKeys = mergeFuzzValueLayers(
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueTemplate},
fuzzValueLayer{values: baseValues, kind: fuzzValueData},
fuzzValueLayer{values: optionVars, kind: fuzzValueData},
fuzzValueLayer{values: constants, kind: fuzzValueData},
)
// Replace template-text interactsh markers without evaluating runtime values.
input.Values, interactURLs = rule.evaluateVarsWithInteractsh(input.Values, interactURLs, dataKeys)
input.InteractURLs = mergeInteractURLs(baseInteractURLs, interactURLs)
input.Values = generators.MergeMaps(evaluation.Values, baseValues, optionVars, constants)
input.InteractURLs = mergeInteractURLs(baseInteractURLs, evaluation.InteractURLs)
err := rule.executeRuleValues(input, component)
if err != nil {
@@ -240,15 +231,17 @@ func (rule *Rule) prepareGeneratorValues(values, baseValues map[string]interface
evaluation := rule.options.Variables.EvaluateWithInteractshScope(evaluationValues, rule.options.Interactsh)
evaluatedValues, interactURLs := evaluation.Values, evaluation.InteractURLs
inputValues, dataKeys := mergeFuzzValueLayers(
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueData},
fuzzValueLayer{values: values, kind: fuzzValueTemplate},
fuzzValueLayer{values: evaluatedValues, kind: fuzzValueTemplate},
fuzzValueLayer{values: baseValues, kind: fuzzValueData},
fuzzValueLayer{values: optionVars, kind: fuzzValueData},
fuzzValueLayer{values: constants, kind: fuzzValueData},
)
// Generator values are template-authored payload definitions until this
// render. Runtime/base, CLI option, and constant values remain data.
// render. Rendered variables, runtime/base, CLI option, and constant
// values remain data. Payloads layer over variables so a payload that
// shares a variable's name is still rendered.
return rule.evaluateVarsWithInteractsh(inputValues, interactURLs, dataKeys)
}
@@ -326,7 +319,7 @@ func (rule *Rule) evaluateVars(input string) (string, error) {
func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, interactshUrls []string, dataKeys map[string]struct{}) (map[string]interface{}, []string) {
// Check if Interactsh options are configured
if rule.options.Interactsh != nil {
data = maps.Clone(data)
rendered := maps.Clone(data)
interactshUrlsMap := make(map[string]struct{})
for _, url := range interactshUrls {
@@ -335,7 +328,8 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
interactshUrls = mapsutil.GetKeys(interactshUrlsMap)
// Iterate through template-text data to replace Interactsh URL markers.
// Render each template-text value against the unrendered input, so
// the result does not depend on map iteration order.
for k, v := range data {
if _, ok := dataKeys[k]; ok {
continue
@@ -349,7 +343,7 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
InteractURLs: interactshUrls,
})
if err == nil {
data[k] = got.Text
rendered[k] = got.Text
for _, url := range got.InteractURLs {
if _, ok := interactshUrlsMap[url]; ok {
continue
@@ -360,6 +354,8 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
}
}
}
data = rendered
}
return data, interactshUrls
+224
View File
@@ -0,0 +1,224 @@
package fuzz
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/variables"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/stretchr/testify/require"
)
const (
// requestBodyWithDSL is request data that looks like a DSL expression.
requestBodyWithDSL = `PLAIN-{{to_upper("abc")}}-END`
queryRuleTarget = "http://127.0.0.1/echo?p=SEED"
)
// fakeInteractshClient hands out numbered URLs without contacting a server.
// Fuzz rules only allocate URLs through NewURLWithData, so the embedded nil
// IClient is never called.
type fakeInteractshClient struct {
interactsh.IClient
issued int
}
func (c *fakeInteractshClient) NewURLWithData(string) (string, error) {
c.issued++
return fmt.Sprintf("oast%d.example", c.issued), nil
}
// newQueryRule compiles a single-mode rule that replaces the p query value
// with fuzzValue.
func newQueryRule(
t *testing.T,
fuzzValue string,
generator *generators.PayloadGenerator,
templateVars variables.Variable,
client interactsh.IClient,
) *Rule {
t.Helper()
rule := &Rule{
Part: component.RequestQueryComponent,
Type: "replace",
Mode: "single",
Fuzz: SliceOrMapSlice{Value: []string{fuzzValue}},
}
err := rule.Compile(generator, &protocols.ExecutorOptions{
Variables: templateVars,
Options: &types.Options{},
Interactsh: client,
})
require.NoError(t, err)
return rule
}
// executeQueryRule runs rule against queryRuleTarget and returns the generated
// requests.
func executeQueryRule(
t *testing.T,
rule *Rule,
method string,
body io.Reader,
values map[string]any,
) []GeneratedRequest {
t.Helper()
req, err := retryablehttp.NewRequest(method, queryRuleTarget, body)
require.NoError(t, err)
var got []GeneratedRequest
err = rule.Execute(&ExecuteRuleInput{
Input: contextargs.NewWithInput(context.Background(), queryRuleTarget),
BaseRequest: req,
Values: values,
Callback: func(gr GeneratedRequest) bool {
got = append(got, gr)
return true
},
})
require.NoError(t, err)
return got
}
// newRequestDataVariableRule returns a query rule whose template declares
// b: "{{body}}", so b captures request data.
func newRequestDataVariableRule(t *testing.T, fuzzValue string, payloads map[string]any) *Rule {
t.Helper()
templateVars := newFuzzTestVariables()
templateVars.Set("b", "{{body}}")
var generator *generators.PayloadGenerator
if payloads != nil {
var err error
generator, err = generators.New(
payloads, generators.BatteringRamAttack, "", disk.NewCatalog(""), "", types.DefaultOptions(),
)
require.NoError(t, err)
}
// The CLI creates an Interactsh client even with -no-interactsh.
return newQueryRule(t, fuzzValue, generator, templateVars, &interactsh.Client{})
}
// fuzzedQueryValues runs rule against a POST request whose body is
// requestBodyWithDSL and returns the fuzzed p query value of each generated
// request.
func fuzzedQueryValues(t *testing.T, rule *Rule) []string {
t.Helper()
generated := executeQueryRule(t, rule, http.MethodPost, strings.NewReader(requestBodyWithDSL), map[string]any{
"body": requestBodyWithDSL,
})
got := make([]string, 0, len(generated))
for _, gr := range generated {
got = append(got, gr.Request.URL.Query().Get("p"))
}
return got
}
func TestExecuteKeepsRequestDataInTemplateVariableLiteral(t *testing.T) {
t.Parallel()
t.Run("fuzz value", func(t *testing.T) {
t.Parallel()
rule := newRequestDataVariableRule(t, "{{b}}", nil)
require.Equal(t, []string{requestBodyWithDSL}, fuzzedQueryValues(t, rule))
})
t.Run("payload value", func(t *testing.T) {
t.Parallel()
rule := newRequestDataVariableRule(t, "{{inj}}", map[string]any{
"inj": []any{"{{b}}"},
})
// Each run iterates a fresh value map. Repeat so the result
// cannot depend on map iteration order.
for range 256 {
require.Equal(t, []string{requestBodyWithDSL}, fuzzedQueryValues(t, rule))
}
})
}
func TestExecuteRendersTemplateVariableInteractshMarker(t *testing.T) {
t.Parallel()
templateVars := newFuzzTestVariables()
templateVars.Set("marker", "{{interactsh-url}}")
rule := newQueryRule(t, "{{marker}}", nil, templateVars, &fakeInteractshClient{})
got := executeQueryRule(t, rule, http.MethodGet, nil, nil)
require.Len(t, got, 1)
require.Equal(t, []string{"oast1.example"}, got[0].InteractURLs)
require.Equal(t, "oast1.example", got[0].Request.URL.Query().Get("p"))
}
func TestPrepareGeneratorValuesRendersPayloadSharingVariableName(t *testing.T) {
t.Parallel()
templateVars := newFuzzTestVariables()
templateVars.Set("inj", "variable")
rule := &Rule{
options: &protocols.ExecutorOptions{
Variables: templateVars,
Options: &types.Options{},
Interactsh: &fakeInteractshClient{},
},
}
values, _ := rule.prepareGeneratorValues(map[string]any{
"inj": `{{to_upper("payload")}}`,
}, nil)
require.Equal(t, "PAYLOAD", values["inj"])
}
func TestPrepareGeneratorValuesRendersPayloadsAgainstUnrenderedInput(t *testing.T) {
t.Parallel()
rule := &Rule{
options: &protocols.ExecutorOptions{
Variables: newFuzzTestVariables(),
Options: &types.Options{},
Interactsh: &interactsh.Client{},
},
}
for range 256 {
values, _ := rule.prepareGeneratorValues(map[string]any{
"upper": `{{to_upper("x")}}`,
"ref": "{{upper}}",
}, nil)
require.Equal(t, "X", values["upper"])
// ref sees the unrendered definition of upper as data.
require.Equal(t, `{{to_upper("x")}}`, values["ref"])
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
)
func newFuzzTestInteractshClient(t *testing.T) *interactsh.Client {
func newFuzzTestInteractshClient(t *testing.T) interactsh.IClient {
t.Helper()
client, err := interactsh.New(&interactsh.Options{
+18
View File
@@ -43,6 +43,7 @@ https://d.example
| Key | Selects |
| --- | --- |
| `target` | the target (URL, host, IP, CIDR or ASN), required |
| `ip` | the address to connect to instead of resolving the target's host, see below |
| `profile` | a template profile ID from the templates `profiles` directory (paths are rejected); the line's other keys override it |
| `templates` | template paths, relative to the templates directory |
| `tags`, `exclude-tags`, `include-tags`, `author`, `template-id`, `exclude-id`, `severity`, `exclude-severity`, `type`, `exclude-type` | same as the CLI flags |
@@ -54,6 +55,23 @@ https://d.example
- Unknown keys fail the run rather than widening the scan. Profile keys that configure the whole scan, such as `code` or `var`, are ignored per target.
- Not supported with `-automatic-scan`. Global matchers still apply to every target.
### Pinning a target to an IP
`ip` sends the target's traffic to one address, like `curl --resolve`: the hostname is still used for the Host header, SNI and template variables such as `{{Hostname}}`, while connections to that hostname go to the ip. This scans a vhost on a given server, or an origin behind a CDN.
```jsonl
{"target": "https://app.example", "ip": "192.0.2.10"}
{"target": "https://app.example", "ip": "192.0.2.11", "tags": ["cve"]}
```
- Each `target` and `ip` pair is a separate target: the same host can be listed with several ips, and each pair has its own selection. `{{ip}}` and the `ip` field of results hold the pinned ip.
- The target must have a hostname; IP, CIDR and ASN targets cannot be pinned. `-sa` and `-iv` do not apply to a pinned target.
- A pinned ip excluded with `-eh`, or a local one with `-lna`, is skipped.
- `-mup` counts the urls of each ip apart, so a url listed with several ips is kept on each of them.
- HTTP, network, websocket, javascript and ssl templates connect to the ip. Headless templates and `unsafe: true` raw requests do not.
- Pinned targets cannot be combined with `-proxy`, `-replay-proxy` or `-automatic-scan`.
- `-u` splits its value on commas unless the value is quoted, so pass target lines in a file (`-l`) or on stdin.
## Formats
Fuzzing and DAST take the request shape (method, body, params) from the input, not from CLI flags, so a bare URL is always fuzzed as a `GET`. To fuzz anything else, feed a request-shaped input with `-im`:
+20 -1
View File
@@ -5,6 +5,7 @@ Formats implements support for passing a number of request source as input provi
Currently the following formats are implemented -
- Burp Suite XML Request/Response file
- HAR (HTTP Archive) file exported by browsers and proxies
- Proxify JSONL output file
- OpenAPI Specification file
- Postman Collection file
@@ -53,7 +54,25 @@ For unsupported media types, no appropriate content type is found for the body.
This module converts each operation into one or more example HTTP requests. Each request is dumped into a string format, accompanied by its method, URL, headers, and body. These are send as a callback for further processing.
_Please note: This document does not cover other features of OpenAPI specification like responses, security schemes, links, callbacks, etc. as these are not currently handled by the module._
### Security Schemes
The OpenAPI input supports HTTP basic and bearer authentication, API keys in headers, query parameters or cookies, and OAuth2/OpenID Connect access tokens. Security requirements can be declared globally or per operation.
For OAuth2 (`type: oauth2`) and OpenID Connect (`type: openIdConnect`), supply an existing access token as the full `Authorization` header value:
```sh
nuclei -l openapi.yaml -im openapi -var 'Authorization=Bearer YOUR_ACCESS_TOKEN'
```
The same variable is used for HTTP bearer authentication. For basic authentication, provide the full `Basic ...` value; for API keys, use the parameter name specified by the security scheme.
Nuclei does not perform OAuth authorization flows, discover OpenID Connect providers, acquire tokens, or refresh them. Obtain an access token with the scopes required by the API before running the scan. The provided header value is used unchanged.
Security requirement objects are alternatives. The generator selects the first supported alternative, in document order, for which all required credentials are supplied, and includes every scheme in that object. An empty object (`{}`) permits anonymous access. If no supported alternative has all its credentials, the first supported alternative is used to report missing values; credentials from different alternatives are not combined.
Operations declaring `security: []` can be generated without the global token. Absent, nil or empty credential values cause affected requests to be skipped. Normal parameter validation reports missing credentials even when another required parameter is also missing. With `-skip-format-validation`, requests with missing credentials are silently skipped and the remaining operations are generated without inventing placeholder credentials.
_Please note: Other OpenAPI features such as responses, links and callbacks are not currently handled by the module._
## Postman Collection file
+137
View File
@@ -0,0 +1,137 @@
package har
import (
"encoding/base64"
"io"
"net/http"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// HARFormat is an HTTP Archive (HAR 1.2) parser, the format browsers and
// proxies export captured traffic in.
type HARFormat struct {
opts formats.InputFormatOptions
}
// New creates a new HAR parser
func New() *HARFormat {
return &HARFormat{}
}
var _ formats.Format = &HARFormat{}
type archive struct {
Log struct {
Entries []entry `json:"entries"`
} `json:"log"`
}
type entry struct {
Request struct {
Method string `json:"method"`
URL string `json:"url"`
Headers []header `json:"headers"`
PostData *struct {
Text string `json:"text"`
} `json:"postData"`
} `json:"request"`
Response struct {
Status int `json:"status"`
StatusText string `json:"statusText"`
Headers []header `json:"headers"`
Content struct {
Text string `json:"text"`
Encoding string `json:"encoding"`
} `json:"content"`
} `json:"response"`
}
type header struct {
Name string `json:"name"`
Value string `json:"value"`
}
// Name returns the name of the format
func (h *HARFormat) Name() string {
return "har"
}
func (h *HARFormat) SetOptions(options formats.InputFormatOptions) {
h.opts = options
}
// Parse parses the input and calls the provided callback
// function for each RawRequest it discovers.
func (h *HARFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallback, filePath string) error {
var har archive
if err := json.NewDecoder(input).Decode(&har); err != nil {
return errors.Wrap(err, "could not decode har file")
}
for _, e := range har.Log.Entries {
// Browsers also log data:, blob: and extension URLs, which have no
// host to send a request to.
if !strings.HasPrefix(e.Request.URL, "http://") && !strings.HasPrefix(e.Request.URL, "https://") {
continue
}
rawRequest, err := types.ParseRawRequestWithURL(rawRequestOf(e), e.Request.URL)
if err != nil {
gologger.Warning().Msgf("har: Could not parse request %s: %s\n", e.Request.URL, err)
continue
}
// Status 0 is a request that was blocked or never answered.
if e.Response.Status != 0 {
rawRequest.Response = &types.HttpResponse{Raw: rawResponseOf(e)}
}
resultsCb(rawRequest)
}
return nil
}
// rawRequestOf rebuilds the request as HTTP/1.1 with an absolute target, which
// carries the authority that HTTP/2 captures only keep in pseudo-headers.
func rawRequestOf(e entry) string {
var b strings.Builder
b.WriteString(e.Request.Method + " " + e.Request.URL + " HTTP/1.1\r\n")
writeHeaders(&b, e.Request.Headers)
if e.Request.PostData != nil {
b.WriteString(e.Request.PostData.Text)
}
return b.String()
}
func rawResponseOf(e entry) string {
statusText := e.Response.StatusText
if statusText == "" {
statusText = http.StatusText(e.Response.Status)
}
var b strings.Builder
b.WriteString("HTTP/1.1 " + strconv.Itoa(e.Response.Status) + " " + statusText + "\r\n")
writeHeaders(&b, e.Response.Headers)
body := e.Response.Content.Text
if e.Response.Content.Encoding == "base64" {
if decoded, err := base64.StdEncoding.DecodeString(body); err == nil {
body = string(decoded)
}
}
b.WriteString(body)
return b.String()
}
func writeHeaders(b *strings.Builder, headers []header) {
for _, h := range headers {
// HTTP/2 pseudo-headers (:authority, :path, ...) are not headers.
if strings.HasPrefix(h.Name, ":") {
continue
}
b.WriteString(h.Name + ": " + h.Value + "\r\n")
}
b.WriteString("\r\n")
}
+71
View File
@@ -0,0 +1,71 @@
package har
import (
"os"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/stretchr/testify/require"
)
const chromeHAR = "../testdata/chrome.har"
func parse(t *testing.T, input string) []*types.RequestResponse {
t.Helper()
file, err := os.Open(input)
require.NoError(t, err)
defer func() { _ = file.Close() }()
var got []*types.RequestResponse
require.NoError(t, New().Parse(file, func(rr *types.RequestResponse) bool {
got = append(got, rr)
return false
}, input))
return got
}
func TestHARParse(t *testing.T) {
got := parse(t, chromeHAR)
urls := make([]string, 0, len(got))
for _, rr := range got {
urls = append(urls, rr.URL.String())
}
require.Equal(t, []string{
"https://ginandjuice.shop/catalog?category=Juice",
"https://ginandjuice.shop/login",
"https://ginandjuice.shop/blocked.js",
}, urls, "non-http entries such as data: URLs are skipped")
}
func TestHARParseHTTP2Request(t *testing.T) {
rr := parse(t, chromeHAR)[0]
require.Equal(t, "GET", rr.Request.Method)
cookie, _ := rr.Request.Headers.Get("cookie")
require.Equal(t, "session=abc123", cookie)
for _, name := range rr.Request.Headers.GetKeys() {
require.False(t, strings.HasPrefix(name, ":"), "pseudo-header %s is not a real header", name)
}
req, err := rr.BuildRequest()
require.NoError(t, err)
require.Equal(t, "ginandjuice.shop", req.Host)
}
func TestHARParseBodyAndResponse(t *testing.T) {
got := parse(t, chromeHAR)
login := got[1]
require.Equal(t, "POST", login.Request.Method)
require.Equal(t, "username=carlos&password=hunter2", login.Request.Body)
require.NotNil(t, login.Response)
require.Contains(t, login.Response.Raw, "302 Found")
require.Contains(t, login.Response.Raw, "Location: /my-account")
require.NotNil(t, got[0].Response)
require.Contains(t, got[0].Response.Raw, "<h1>Juice</h1>", "base64 content is decoded")
require.Nil(t, got[2].Response, "a request that never got an answer has no response")
}
+20
View File
@@ -0,0 +1,20 @@
//go:build gofuzz
// +build gofuzz
package openapi
// Fuzz exercises OpenAPI 3.0 schema parsing and request generation. The input
// bytes are treated as a raw schema document (YAML or JSON) and fed through the
// format's Parse, which loads the schema and generates requests from it.
func Fuzz(data []byte) int {
if len(data) == 0 {
return 0
}
if len(data) > fuzzMaxInputSize {
return -1
}
if !fuzzOpenAPIParsing(data) {
return 0
}
return 1
}
+32
View File
@@ -0,0 +1,32 @@
package openapi
import (
"bytes"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
)
const fuzzMaxInputSize = 64 << 10
// fuzzOpenAPIParsing feeds data to the OpenAPI format parser as a raw schema
// document and drives request generation. It must never panic for any input;
// it returns true when the schema parsed and at least one request was produced.
func fuzzOpenAPIParsing(data []byte) bool {
if len(data) == 0 || len(data) > fuzzMaxInputSize {
return false
}
format := New()
format.SetOptions(formats.InputFormatOptions{})
produced := false
cb := func(rr *types.RequestResponse) bool {
produced = true
return true
}
if err := format.Parse(bytes.NewReader(data), cb, "fuzz.openapi"); err != nil {
return false
}
return produced
}
@@ -0,0 +1,84 @@
package openapi
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/stretchr/testify/require"
)
func TestFuzzOpenAPIParsingSeedCorpus(t *testing.T) {
entries, err := os.ReadDir("testdata/gofuzz-corpus")
require.NoError(t, err)
require.NotEmpty(t, entries)
for _, entry := range entries {
if entry.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join("testdata/gofuzz-corpus", entry.Name()))
require.NoError(t, err)
require.Truef(t, fuzzOpenAPIParsing(data), "seed %s should produce at least one request", entry.Name())
}
}
func TestFuzzOpenAPIParsingRejectsOversizeInput(t *testing.T) {
require.False(t, fuzzOpenAPIParsing(make([]byte, fuzzMaxInputSize+1)))
}
// Null entries decode to nil pointers and used to panic during request
// generation. The remaining valid entries must still produce requests.
func TestFuzzOpenAPIParsingNullEntries(t *testing.T) {
specs := map[string]string{
"null server": `
openapi: 3.0.0
info: {title: t, version: "1"}
servers:
-
- {url: "http://example.com"}
paths:
/users:
get:
responses: {"200": {description: ok}}
`,
"null media type": `
openapi: 3.0.0
info: {title: t, version: "1"}
servers: [{url: "http://example.com"}]
paths:
/users:
post:
requestBody:
content:
application/xml:
application/json:
schema: {type: object, properties: {name: {type: string}}}
responses: {"200": {description: ok}}
`,
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
require.True(t, fuzzOpenAPIParsing([]byte(spec)))
})
}
}
func TestGenerateRequestsOnlyNullServers(t *testing.T) {
spec := `
openapi: 3.0.0
info: {title: t, version: "1"}
servers:
-
paths:
/users:
get:
responses: {"200": {description: ok}}
`
err := New().Parse(strings.NewReader(spec), func(*types.RequestResponse) bool { return false }, "spec.yaml")
require.ErrorContains(t, err, "no servers found in openapi schema")
}
+127 -68
View File
@@ -9,6 +9,7 @@ import (
"net/http/httputil"
"net/url"
"os"
"slices"
"strings"
"github.com/clbanning/mxj/v2"
@@ -33,36 +34,12 @@ const (
// GenerateRequestsFromSchema generates http requests from an OpenAPI 3.0 document object
func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOptions, callback formats.ParseReqRespCallback) error {
if len(schema.Servers) == 0 {
// Null entries in `servers:` are skipped below, so a list of only null
// entries has no usable server, the same as an empty list.
if !slices.ContainsFunc(schema.Servers, func(s *openapi3.Server) bool { return s != nil }) {
return errors.New("no servers found in openapi schema")
}
// new set of globalParams obtained from security schemes
globalParams := openapi3.NewParameters()
if len(schema.Security) > 0 {
params, err := GetGlobalParamsForSecurityRequirement(schema, &schema.Security)
if err != nil {
return err
}
globalParams = append(globalParams, params...)
}
// validate global param requirements
for _, param := range globalParams {
if val, ok := opts.Variables[param.Value.Name]; ok {
param.Value.Example = val
} else {
// if missing check for validation
if opts.SkipFormatValidation {
gologger.Verbose().Msgf("openapi: skipping all requests due to missing global auth parameter: %s\n", param.Value.Name)
return nil
} else {
// fatal error
gologger.Fatal().Msgf("openapi: missing global auth parameter: %s\n", param.Value.Name)
}
}
}
globalParams, globalSecurityErr := selectSecurityParameters(schema, &schema.Security, opts.Variables)
missingVarMap := make(map[string]struct{})
optionalVarMap := make(map[string]struct{})
@@ -75,6 +52,11 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
}
for _, serverURL := range schema.Servers {
// A null entry in the `servers:` list decodes to a nil *Server, so skip
// it rather than dereferencing it.
if serverURL == nil {
continue
}
pathURL := serverURL.URL
// Split the server URL into baseURL and serverPath
u, err := url.Parse(pathURL)
@@ -92,6 +74,10 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
requestPath = serverPath + path
}
for method, ov := range ops {
// An override need not use an unsupported global security scheme.
if ov.Security == nil && globalSecurityErr != nil {
return globalSecurityErr
}
if err := generateRequestsFromOp(&generateReqOptions{
requiredOnly: opts.RequiredOnly,
method: method,
@@ -148,7 +134,7 @@ type generateReqOptions struct {
// post request generation callback
callback formats.ParseReqRespCallback
// global parameters
// Selected global authentication parameters, used only without an override.
globalParams openapi3.Parameters
// requestparams map
reqParams openapi3.Parameters
@@ -175,21 +161,42 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
}
// add existing req params
reqParams = append(reqParams, opts.op.Parameters...)
// check for endpoint specific auth
// Select one complete requirement for this operation before adding its
// authentication parameters. Operation security overrides global security.
authParams := opts.globalParams
if opts.op.Security != nil {
params, err := GetGlobalParamsForSecurityRequirement(opts.schema, opts.op.Security)
authParams, err = selectSecurityParameters(opts.schema, opts.op.Security, opts.opts.Variables)
if err != nil {
return err
}
reqParams = append(reqParams, params...)
} else {
reqParams = append(reqParams, opts.globalParams...)
}
reqParams = append(reqParams, authParams...)
authParameters := make(map[*openapi3.Parameter]struct{}, len(authParams))
for _, parameter := range authParams {
authParameters[parameter.Value] = struct{}{}
}
// Collect missing credentials before ordinary parameter validation can return
// early. Still validate ordinary parameters so their diagnostics are retained.
missingAuth := false
for _, parameter := range authParams {
value := parameter.Value
if types.ToString(opts.opts.Variables[value.Name]) != "" {
continue
}
missingAuth = true
if opts.missingParamValueCallback != nil {
opts.missingParamValueCallback(value, opts)
}
}
query := url.Values{}
for _, parameter := range reqParams {
value := parameter.Value
if _, isAuth := authParameters[value]; missingAuth && isAuth {
continue
}
if value.Schema == nil || value.Schema.Value == nil {
continue
}
@@ -261,17 +268,25 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
req.AddCookie(&http.Cookie{Name: value.Name, Value: types.ToString(paramValue)})
}
}
if missingAuth {
return nil
}
req.URL.RawQuery = query.Encode()
req.URL.Path = opts.requestPath
if opts.op.RequestBody != nil {
for content, value := range opts.op.RequestBody.Value.Content {
// A null media-type entry (e.g. `content: {application/json: }`)
// decodes to a nil *MediaType, so skip it before dereferencing.
if value == nil {
continue
}
cloned := req.Clone(req.Context())
var val interface{}
if value.Schema == nil || value.Schema.Value == nil {
val = generateEmptySchemaValue(content)
val, _ = generateExampleFromSchema(generateEmptySchemaValue(content))
} else {
var err error
@@ -291,15 +306,25 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
cloned.Header.Set("Content-Type", "application/json")
}
case "application/xml":
values := mxj.Map(val.(map[string]interface{}))
var marshalled []byte
var err error
switch v := val.(type) {
case map[string]interface{}:
marshalled, err = mxj.Map(v).Xml()
case string:
marshalled = []byte(v)
default:
err = errors.Errorf("unsupported xml example type %T", val)
}
if marshalled, err := values.Xml(); err == nil {
if err == nil {
// body = string(marshalled)
cloned.Body = io.NopCloser(bytes.NewReader(marshalled))
cloned.ContentLength = int64(len(marshalled))
cloned.Header.Set("Content-Type", "application/xml")
} else {
gologger.Warning().Msgf("openapi: could not encode xml")
continue
}
case "application/x-www-form-urlencoded":
if values, ok := val.(map[string]interface{}); ok {
@@ -345,7 +370,7 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
// use two strings
str = "string1\nstring2"
}
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "bindary", "byte") {
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "binary", "byte") {
cloned.Body = io.NopCloser(bytes.NewReader([]byte(str)))
cloned.ContentLength = int64(len(str))
cloned.Header.Set("Content-Type", "application/octet-stream")
@@ -391,45 +416,79 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
return nil
}
// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement
// GetGlobalParamsForSecurityRequirement returns parameters for the first supported
// security alternative, or no parameters when anonymous access is permitted.
func GetGlobalParamsForSecurityRequirement(schema *openapi3.T, requirement *openapi3.SecurityRequirements) ([]*openapi3.ParameterRef, error) {
globalParams := openapi3.NewParameters()
if len(schema.Components.SecuritySchemes) == 0 {
return nil, errkit.Newf("security requirements (%+v) without any security schemes found in openapi file", schema.Security)
return selectSecurityParameters(schema, requirement, nil)
}
// selectSecurityParameters chooses the first fully supplied alternative. If none
// is supplied, return the first supported alternative for missing-value reporting.
func selectSecurityParameters(schema *openapi3.T, requirement *openapi3.SecurityRequirements, variables map[string]interface{}) ([]*openapi3.ParameterRef, error) {
if requirement == nil || len(*requirement) == 0 {
return nil, nil
}
found := false
// this api is protected for each security scheme pull its corresponding scheme
schemaLabel:
var fallback []*openapi3.ParameterRef
var firstError error
for _, security := range *requirement {
for name := range security {
if scheme, ok := schema.Components.SecuritySchemes[name]; ok {
found = true
param, err := GenerateParameterFromSecurityScheme(scheme)
if err != nil {
return nil, err
}
globalParams = append(globalParams, &openapi3.ParameterRef{Value: param})
continue schemaLabel
if len(security) == 0 {
return nil, nil
}
params := openapi3.NewParameters()
complete, supported := true, true
// Map order must not change which schemes are emitted or reported missing.
for _, name := range mapsutil.GetSortedKeys(security) {
var scheme *openapi3.SecuritySchemeRef
if schema.Components != nil {
scheme = schema.Components.SecuritySchemes[name]
}
if scheme == nil || scheme.Value == nil {
if firstError == nil {
firstError = errkit.Newf("security scheme (%s) not found in openapi file", name)
}
supported = false
break
}
param, err := GenerateParameterFromSecurityScheme(scheme)
if err != nil {
if firstError == nil {
firstError = err
}
supported = false
break
}
params = append(params, &openapi3.ParameterRef{Value: param})
complete = complete && types.ToString(variables[param.Name]) != ""
}
if !found && len(security) > 1 {
// if this is case then both security schemes are required
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", security)
if !supported {
continue
}
if complete {
return params, nil
}
if fallback == nil {
fallback = params
}
}
if !found {
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", requirement)
if fallback != nil {
return fallback, nil
}
return globalParams, nil
return nil, firstError
}
// GenerateParameterFromSecurityScheme generates an example from a schema object
func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*openapi3.Parameter, error) {
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey") {
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey", "oauth2", "openIdConnect") {
return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)
}
if generic.EqualsAny(scheme.Value.Type, "oauth2", "openIdConnect") {
// Access tokens are supplied by the caller through the Authorization variable.
// Request generation does not acquire or refresh tokens from the provider.
h := openapi3.NewHeaderParameter(DEFAULT_HTTP_SCHEME_HEADER).WithSchema(openapi3.NewStringSchema())
h.Required = true
h.Description = globalAuth
return h, nil
}
if scheme.Value.Type == "http" {
// check scheme
if !generic.EqualsAny(scheme.Value.Scheme, "basic", "bearer") {
@@ -443,12 +502,12 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
// create parameters using the scheme
switch scheme.Value.Scheme {
case "basic":
h := openapi3.NewHeaderParameter(headerName)
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
h.Required = true
h.Description = globalAuth // differentiator for normal variables and global auth
return h, nil
case "bearer":
h := openapi3.NewHeaderParameter(headerName)
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
h.Required = true
h.Description = globalAuth // differentiator for normal variables and global auth
return h, nil
@@ -466,17 +525,17 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
// create parameters using the scheme
switch scheme.Value.In {
case "query":
q := openapi3.NewQueryParameter(scheme.Value.Name)
q := openapi3.NewQueryParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
q.Required = true
q.Description = globalAuth // differentiator for normal variables and global auth
return q, nil
case "header":
h := openapi3.NewHeaderParameter(scheme.Value.Name)
h := openapi3.NewHeaderParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
h.Required = true
h.Description = globalAuth // differentiator for normal variables and global auth
return h, nil
case "cookie":
c := openapi3.NewCookieParameter(scheme.Value.Name)
c := openapi3.NewCookieParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
c.Required = true
c.Description = globalAuth // differentiator for normal variables and global auth
return c, nil
@@ -0,0 +1,46 @@
package openapi
import (
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/stretchr/testify/require"
)
func TestOpenAPIOctetStreamContentType(t *testing.T) {
tests := []struct {
name string
schema string
contentType string
}{
{name: "binary format", schema: "{type: string, format: binary}", contentType: "application/octet-stream"},
{name: "byte format", schema: "{type: string, format: byte}", contentType: "application/octet-stream"},
{name: "plain string", schema: "{type: string}", contentType: "text/plain"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
spec := `openapi: 3.0.0
info: {title: t, version: "1"}
servers: [{url: "http://example.com"}]
paths:
/upload:
post:
requestBody:
content:
application/octet-stream: {schema: ` + tt.schema + `}
responses:
"200": {description: ok}
`
var contentTypes []string
err := New().Parse(strings.NewReader(spec), func(rr *types.RequestResponse) bool {
value, _ := rr.Request.Headers.Get("Content-Type")
contentTypes = append(contentTypes, value)
return false
}, "spec.yaml")
require.NoError(t, err)
require.Equal(t, []string{tt.contentType}, contentTypes)
})
}
}
+350
View File
@@ -0,0 +1,350 @@
package openapi
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/getkin/kin-openapi/openapi3"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/stretchr/testify/require"
)
func TestGenerateRequestsWithSecuritySchemes(t *testing.T) {
for _, test := range []struct {
name string
scheme string
variable string
value string
in string
}{
{
name: "oauth2", scheme: `{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{"read":"Read access"}}}}`,
variable: "Authorization", value: "Bearer supplied-token", in: "header",
},
{
name: "openIdConnect", scheme: `{"type":"openIdConnect","openIdConnectUrl":"https://example.com/.well-known/openid-configuration"}`,
variable: "Authorization", value: "Bearer supplied-token", in: "header",
},
{
name: "bearer", scheme: `{"type":"http","scheme":"bearer"}`,
variable: "Authorization", value: "Bearer supplied-token", in: "header",
},
{
name: "basic", scheme: `{"type":"http","scheme":"basic"}`,
variable: "Authorization", value: "Basic dXNlcjpwYXNz", in: "header",
},
{
name: "apiKey-header", scheme: `{"type":"apiKey","name":"X-API-Key","in":"header"}`,
variable: "X-API-Key", value: "supplied-key", in: "header",
},
{
name: "apiKey-query", scheme: `{"type":"apiKey","name":"api_key","in":"query"}`,
variable: "api_key", value: "supplied-key", in: "query",
},
{
name: "apiKey-cookie", scheme: `{"type":"apiKey","name":"session","in":"cookie"}`,
variable: "session", value: "supplied-key", in: "cookie",
},
} {
for _, scope := range []string{"global", "operation"} {
t.Run(test.name+"/"+scope, func(t *testing.T) {
security := `"security":[{"auth":[]}],`
if test.name == "oauth2" || test.name == "openIdConnect" {
security = `"security":[{"auth":["read"]}],`
}
globalSecurity, operationSecurity := security, ""
if scope == "operation" {
globalSecurity, operationSecurity = "", security
}
document := fmt.Sprintf(`{
"openapi":"3.0.3",
"info":{"title":"Authentication test","version":"1.0"},
"servers":[{"url":"https://example.com"}],
"components":{"securitySchemes":{"auth":%s}},
%s
"paths":{"/protected":{"get":{%s"responses":{"200":{"description":"OK"}}}}}
}`, test.scheme, globalSecurity, operationSecurity)
parser := New()
parser.SetOptions(formats.InputFormatOptions{
RequiredOnly: true,
Variables: map[string]interface{}{test.variable: test.value},
})
var requests []*http.Request
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(rr.Request.Raw)))
require.NoError(t, err)
requests = append(requests, req)
return false
}, "")
require.NoError(t, err)
require.Len(t, requests, 1)
req := requests[0]
t.Cleanup(func() { require.NoError(t, req.Body.Close()) })
require.Equal(t, "/protected", req.URL.Path)
switch test.in {
case "header":
require.Equal(t, test.value, req.Header.Get(test.variable))
case "query":
require.Equal(t, test.value, req.URL.Query().Get(test.variable))
case "cookie":
cookie, err := req.Cookie(test.variable)
require.NoError(t, err)
require.Equal(t, test.value, cookie.Value)
}
})
}
}
}
func TestGenerateRequestsOAuthOperationOverride(t *testing.T) {
parser := New()
parser.SetOptions(formats.InputFormatOptions{
Variables: map[string]interface{}{
"Authorization": "Bearer supplied-token",
"X-API-Key": "supplied-key",
},
})
document := `{
"openapi":"3.0.3",
"info":{"title":"Authentication override test","version":"1.0"},
"servers":[{"url":"https://example.com"}],
"components":{"securitySchemes":{
"oauth":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{}}}},
"key":{"type":"apiKey","name":"X-API-Key","in":"header"}
}},
"security":[{"oauth":[]}],
"paths":{
"/protected":{"get":{"security":[{"key":[]}],"responses":{"200":{"description":"OK"}}}},
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
}
}`
paths := make(map[string]bool)
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
paths[rr.URL.Path] = true
key, _ := rr.Request.Headers.Get("X-Api-Key")
if rr.URL.Path == "/protected" {
require.Equal(t, "supplied-key", key)
} else {
require.Empty(t, key)
}
auth, _ := rr.Request.Headers.Get("Authorization")
require.Empty(t, auth)
return false
}, "")
require.NoError(t, err)
require.Equal(t, map[string]bool{"/protected": true, "/public": true}, paths)
}
func TestGenerateRequestsOAuthRequiresToken(t *testing.T) {
for _, skipValidation := range []bool{false, true} {
for _, schemeType := range []string{"oauth2", "openIdConnect"} {
t.Run(fmt.Sprintf("%s/skip-validation=%t", schemeType, skipValidation), func(t *testing.T) {
param, err := GenerateParameterFromSecurityScheme(&openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{Type: schemeType},
})
require.NoError(t, err)
var missing []string
err = generateRequestsFromOp(&generateReqOptions{
method: http.MethodGet,
pathURL: "https://example.com",
requestPath: "/protected",
op: &openapi3.Operation{},
opts: formats.InputFormatOptions{SkipFormatValidation: skipValidation},
globalParams: openapi3.Parameters{&openapi3.ParameterRef{Value: param}},
missingParamValueCallback: func(param *openapi3.Parameter, _ *generateReqOptions) {
missing = append(missing, param.Name)
},
callback: func(_ *httpTypes.RequestResponse) bool {
t.Fatal("must not generate an authenticated request without a token")
return false
},
})
require.NoError(t, err)
require.Equal(t, []string{"Authorization"}, missing)
})
}
}
}
func oauthSecurityDocument(paths string) string {
return fmt.Sprintf(`{
"openapi":"3.0.3",
"info":{"title":"Authentication scope test","version":"1.0"},
"servers":[{"url":"https://example.com"}],
"components":{"securitySchemes":{
"oauth":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{}}}},
"key":{"type":"apiKey","name":"X-API-Key","in":"header"}
}},
"security":[{"oauth":[]}],
"paths":{%s}
}`, paths)
}
func TestGenerateRequestsOAuthWithoutGlobalToken(t *testing.T) {
for _, skipValidation := range []bool{true, false} {
for _, test := range []struct {
name string
paths string
want []string
}{
{
name: "public-only",
paths: `"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}`,
want: []string{"/public"},
},
{
name: "mixed-security",
paths: `
"/protected":{"get":{"responses":{"200":{"description":"OK"}}}},
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}},
"/key":{"get":{"security":[{"key":[]}],"responses":{"200":{"description":"OK"}}}}
`,
want: []string{"/public", "/key"},
},
} {
t.Run(fmt.Sprintf("%s/skip-validation=%t", test.name, skipValidation), func(t *testing.T) {
parser := New()
parser.SetOptions(formats.InputFormatOptions{
SkipFormatValidation: skipValidation,
Variables: map[string]interface{}{"X-API-Key": "supplied-key"},
})
var paths []string
err := parser.Parse(strings.NewReader(oauthSecurityDocument(test.paths)), func(rr *httpTypes.RequestResponse) bool {
paths = append(paths, rr.URL.Path)
auth, _ := rr.Request.Headers.Get("Authorization")
require.Empty(t, auth)
key, _ := rr.Request.Headers.Get("X-Api-Key")
if rr.URL.Path == "/key" {
require.Equal(t, "supplied-key", key)
} else {
require.Empty(t, key)
}
return false
}, "")
require.NoError(t, err)
require.ElementsMatch(t, test.want, paths)
})
}
}
}
func TestGenerateRequestsOAuthMissingTokenCLI(t *testing.T) {
const helperEnv = "NUCLEI_TEST_OPENAPI_MISSING_TOKEN"
if os.Getenv(helperEnv) == "1" {
config.CurrentAppMode = config.AppModeCLI
document := oauthSecurityDocument(`
"/protected/{id}":{"get":{
"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],
"responses":{"200":{"description":"OK"}}
}},
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
`)
err := New().Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
fmt.Println("generated:", rr.URL.Path)
return false
}, "")
require.NoError(t, err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestGenerateRequestsOAuthMissingTokenCLI$")
cmd.Env = append(os.Environ(), helperEnv+"=1")
cmd.Dir = t.TempDir()
output, err := cmd.CombinedOutput()
var exitErr *exec.ExitError
require.ErrorAs(t, err, &exitErr)
require.Equal(t, 1, exitErr.ExitCode())
require.Contains(t, string(output), "generated: /public")
require.NotContains(t, string(output), "generated: /protected")
parameters, err := os.ReadFile(filepath.Join(cmd.Dir, formats.DefaultVarDumpFileName))
require.NoError(t, err)
require.Contains(t, string(parameters), "Authorization=")
require.Contains(t, string(parameters), "id=")
}
func TestGenerateRequestsCredentials(t *testing.T) {
for _, auth := range []struct {
scheme string
variable string
}{
{scheme: "oauth", variable: "Authorization"},
{scheme: "key", variable: "X-API-Key"},
} {
for _, scope := range []string{"global", "operation"} {
for _, skipValidation := range []bool{false, true} {
for _, credential := range []struct {
name string
present bool
value interface{}
wantProtected bool
}{
{name: "absent"},
{name: "nil", present: true},
{name: "empty", present: true, value: ""},
{name: "supplied", present: true, value: "opaque-token", wantProtected: true},
} {
t.Run(fmt.Sprintf("%s/%s/%s/skip-validation=%t", auth.scheme, scope, credential.name, skipValidation), func(t *testing.T) {
document := oauthSecurityDocument(`
"/protected":{"get":{"responses":{"200":{"description":"OK"}}}},
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
`)
schema, err := openapi3.NewLoader().LoadFromData([]byte(document))
require.NoError(t, err)
security := openapi3.SecurityRequirements{{auth.scheme: []string{}}}
if scope == "global" {
schema.Security = security
} else {
schema.Security = nil
schema.Paths.Map()["/protected"].Get.Security = &security
}
variables := map[string]interface{}{}
if credential.present {
variables[auth.variable] = credential.value
}
var paths []string
err = GenerateRequestsFromSchema(schema, formats.InputFormatOptions{
Variables: variables,
SkipFormatValidation: skipValidation,
}, func(rr *httpTypes.RequestResponse) bool {
paths = append(paths, rr.URL.Path)
for _, header := range []string{"Authorization", "X-API-Key"} {
value, _ := rr.Request.Headers.Get(http.CanonicalHeaderKey(header))
if rr.URL.Path == "/protected" && header == auth.variable && credential.wantProtected {
require.Equal(t, credential.value, value)
} else {
require.Empty(t, value)
}
}
return false
})
require.NoError(t, err)
wantPaths := []string{"/public"}
if credential.wantProtected {
wantPaths = append(wantPaths, "/protected")
}
require.ElementsMatch(t, wantPaths, paths)
})
}
}
}
}
}
func TestGenerateParameterUnsupportedSecurityScheme(t *testing.T) {
_, err := GenerateParameterFromSecurityScheme(&openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{Type: "mutualTLS"},
})
require.ErrorContains(t, err, "unsupported security scheme type (mutualTLS)")
}
+39
View File
@@ -68,3 +68,42 @@ func TestOpenAPIParser(t *testing.T) {
require.ElementsMatch(t, urls, methodToURLs[method], "invalid urls for method %s", method)
}
}
func TestOpenAPIRequestBodies(t *testing.T) {
tests := []struct {
name string
content string
bodies []string
}{
{name: "xml without schema", content: "application/xml: {}", bodies: []string{`<?xml version="1.0"?><root/>`}},
{name: "xml string schema", content: "application/xml: {schema: {type: string}}", bodies: []string{"string"}},
{name: "xml array schema", content: "application/xml: {schema: {type: array, items: {type: string}}}", bodies: nil},
{name: "json without schema", content: "application/json: {}", bodies: []string{"{}"}},
{name: "text without schema", content: "text/plain: {}", bodies: []string{"string"}},
{name: "octet-stream without schema", content: "application/octet-stream: {}", bodies: []string{"string1\nstring2"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
spec := `openapi: 3.0.0
info: {title: t, version: "1"}
servers: [{url: "http://example.com"}]
paths:
/items:
post:
requestBody:
content:
` + tt.content + `
responses:
"200": {description: ok}
`
var bodies []string
err := New().Parse(strings.NewReader(spec), func(rr *types.RequestResponse) bool {
bodies = append(bodies, rr.Request.Body)
return false
}, "spec.yaml")
require.NoError(t, err)
require.Equal(t, tt.bodies, bodies)
})
}
}
+152
View File
@@ -0,0 +1,152 @@
package openapi
import (
"bufio"
"fmt"
"net/http"
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/stretchr/testify/require"
)
func TestGenerateRequestsSecurityAlternatives(t *testing.T) {
for _, tc := range []struct {
name, security string
variables map[string]interface{}
wantCount int
wantAuth string
wantKey string
}{
{"first alternative", `[{"oauth":[]},{"key":[]}]`, map[string]interface{}{"Authorization": "Bearer token"}, 1, "Bearer token", ""},
{"second alternative", `[{"oauth":[]},{"key":[]}]`, map[string]interface{}{"X-API-Key": "key"}, 1, "", "key"},
{"both alternatives available", `[{"oauth":[]},{"key":[]}]`, map[string]interface{}{"Authorization": "Bearer token", "X-API-Key": "key"}, 1, "Bearer token", ""},
{"neither alternative available", `[{"oauth":[]},{"key":[]}]`, nil, 0, "", ""},
{"combined complete", `[{"oauth":[],"key":[]}]`, map[string]interface{}{"Authorization": "Bearer token", "X-API-Key": "key"}, 1, "Bearer token", "key"},
{"combined missing key", `[{"oauth":[],"key":[]}]`, map[string]interface{}{"Authorization": "Bearer token"}, 0, "", ""},
{"combined missing token", `[{"oauth":[],"key":[]}]`, map[string]interface{}{"X-API-Key": "key"}, 0, "", ""},
{"complete alternative after incomplete combined", `[{"oauth":[],"key":[]},{"key":[]}]`, map[string]interface{}{"X-API-Key": "key"}, 1, "", "key"},
{"anonymous alternative first", `[{}, {"oauth":[]}]`, map[string]interface{}{"Authorization": "Bearer token"}, 1, "", ""},
{"anonymous alternative last", `[{"oauth":[]},{}]`, nil, 1, "", ""},
{"empty security list", `[]`, nil, 1, "", ""},
{"unsupported alternative first", `[{"unsupported":[]},{"key":[]}]`, map[string]interface{}{"X-API-Key": "key"}, 1, "", "key"},
} {
for _, scope := range []string{"global", "operation"} {
for _, skip := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/%s/skip=%t", tc.name, scope, skip), func(t *testing.T) {
global, operation := `"security":`+tc.security+`,`, ""
if scope == "operation" {
global, operation = "", global
}
document := fmt.Sprintf(`{
"openapi":"3.0.3","info":{"title":"Security alternatives","version":"1"},
"servers":[{"url":"https://example.com"}],
"components":{"securitySchemes":{
"oauth":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{}}}},
"key":{"type":"apiKey","in":"header","name":"X-API-Key"},
"unsupported":{"type":"http","scheme":"digest"}
}},%s"paths":{"/protected":{"get":{%s"responses":{"200":{"description":"OK"}}}}}
}`, global, operation)
parser := New()
parser.SetOptions(formats.InputFormatOptions{SkipFormatValidation: skip, Variables: tc.variables})
var requests []*http.Request
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(rr.Request.Raw)))
require.NoError(t, err)
requests = append(requests, req)
return false
}, "")
require.NoError(t, err)
require.Len(t, requests, tc.wantCount)
for _, req := range requests {
t.Cleanup(func() { require.NoError(t, req.Body.Close()) })
require.Equal(t, tc.wantAuth, req.Header.Get("Authorization"))
require.Equal(t, tc.wantKey, req.Header.Get("X-API-Key"))
}
})
}
}
}
}
func TestGenerateRequestsOrdinaryParameterAuthDescription(t *testing.T) {
for _, skip := range []bool{false, true} {
t.Run(fmt.Sprintf("skip=%t", skip), func(t *testing.T) {
parser := New()
parser.SetOptions(formats.InputFormatOptions{SkipFormatValidation: skip})
const document = `{
"openapi":"3.0.3","info":{"title":"Public operation","version":"1"},
"servers":[{"url":"https://example.com"}],
"paths":{"/public":{"get":{
"parameters":[{"name":"display","in":"query","required":false,"description":"globalAuth","schema":{"type":"string","default":"compact"}}],
"responses":{"200":{"description":"OK"}}
}}}
}`
count := 0
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
count++
require.Equal(t, "display=compact", rr.URL.RawQuery)
return false
}, "")
require.NoError(t, err)
require.Equal(t, 1, count)
})
}
}
func TestGenerateRequestsPublicOverrideUnsupportedGlobalSecurity(t *testing.T) {
const document = `{
"openapi":"3.0.3","info":{"title":"Public override","version":"1"},
"servers":[{"url":"https://example.com"}],
"components":{"securitySchemes":{"auth":{"type":"http","scheme":"digest"}}},
"security":[{"auth":[]}],
"paths":{"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}}
}`
count := 0
err := New().Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
count++
return false
}, "")
require.NoError(t, err)
require.Equal(t, 1, count)
}
func TestSecuritySelectionMissingAndUnsupportedSchemes(t *testing.T) {
schema := &openapi3.T{Components: &openapi3.Components{SecuritySchemes: openapi3.SecuritySchemes{
"key": {Value: &openapi3.SecurityScheme{Type: "apiKey", In: "header", Name: "X-API-Key"}},
"oauth": {Value: &openapi3.SecurityScheme{Type: "oauth2"}},
}}}
for _, tc := range []struct {
name string
requirements openapi3.SecurityRequirements
wantNames []string
wantError bool
}{
{"missing combined credentials", openapi3.SecurityRequirements{{"oauth": nil, "key": nil}}, []string{"X-API-Key", "Authorization"}, false},
{"first alternative diagnostics", openapi3.SecurityRequirements{{"oauth": nil}, {"key": nil}}, []string{"Authorization"}, false},
{"unknown scheme", openapi3.SecurityRequirements{{"unknown": nil}}, nil, true},
{"known and unknown in same object", openapi3.SecurityRequirements{{"key": nil, "unknown": nil}}, nil, true},
{"known alternative after unknown", openapi3.SecurityRequirements{{"unknown": nil}, {"key": nil}}, []string{"X-API-Key"}, false},
} {
t.Run(tc.name, func(t *testing.T) {
params, err := selectSecurityParameters(schema, &tc.requirements, nil)
if tc.wantError {
require.Error(t, err)
return
}
require.NoError(t, err)
var names []string
for _, param := range params {
names = append(names, param.Value.Name)
}
require.Equal(t, tc.wantNames, names)
})
}
// Anonymous requirements do not need a components/securitySchemes section.
params, err := GetGlobalParamsForSecurityRequirement(&openapi3.T{}, &openapi3.SecurityRequirements{{}})
require.NoError(t, err)
require.Empty(t, params)
}
@@ -0,0 +1 @@
{"openapi":"3.0.0","info":{"title":"t","version":"1"},"servers":[{"url":"http://example.com"}],"paths":{"/a":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"k":{"type":"string"}}}}}},"responses":{"200":{"description":"ok"}}}}}}
@@ -0,0 +1,7 @@
openapi: 3.0.0
info: {title: t, version: "1"}
servers: [{url: "http://example.com"}]
paths:
/users:
get:
responses: {"200": {description: ok}}
@@ -0,0 +1,12 @@
openapi: 3.0.0
info: {title: t, version: "1"}
servers: [{url: "http://example.com"}]
paths:
/users:
post:
requestBody:
content:
application/xml:
application/json:
schema: {type: object, properties: {name: {type: string}}}
responses: {"200": {description: ok}}
@@ -0,0 +1,9 @@
openapi: 3.0.0
info: {title: t, version: "1"}
servers:
-
- {url: "http://example.com"}
paths:
/users:
get:
responses: {"200": {description: ok}}
+29
View File
@@ -3,7 +3,9 @@ package swagger
import (
"fmt"
"io"
"maps"
"path"
"slices"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
@@ -55,6 +57,7 @@ func (j *SwaggerFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCal
if err != nil {
return errors.Wrap(err, "could not decode openapi 2.0 schema")
}
dropNullEntries(schemav2)
schema, err := openapi2conv.ToV3(schemav2)
if err != nil {
return errors.Wrap(err, "could not convert openapi 2.0 schema to 3.0")
@@ -67,6 +70,32 @@ func (j *SwaggerFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCal
return openapi.GenerateRequestsFromSchema(schema, j.opts, resultsCb)
}
// dropNullEntries removes null entries from the maps and lists of a decoded
// OpenAPI 2.0 document. A null value such as `paths: {/x: }` or an empty list
// item under `parameters:` decodes to a nil pointer, which openapi2conv.ToV3
// dereferences without checking.
func dropNullEntries(doc *openapi2.T) {
maps.DeleteFunc(doc.Paths, func(_ string, v *openapi2.PathItem) bool { return v == nil })
maps.DeleteFunc(doc.Parameters, func(_ string, v *openapi2.Parameter) bool { return v == nil })
dropNullResponses(doc.Responses)
for _, pathItem := range doc.Paths {
pathItem.Parameters = slices.DeleteFunc(pathItem.Parameters, func(v *openapi2.Parameter) bool { return v == nil })
for _, op := range pathItem.Operations() {
op.Parameters = slices.DeleteFunc(op.Parameters, func(v *openapi2.Parameter) bool { return v == nil })
dropNullResponses(op.Responses)
}
}
}
// dropNullResponses removes null responses, and null headers of the remaining
// responses, which openapi2conv.ToV3Response also dereferences.
func dropNullResponses(responses map[string]*openapi2.Response) {
maps.DeleteFunc(responses, func(_ string, v *openapi2.Response) bool { return v == nil })
for _, response := range responses {
maps.DeleteFunc(response.Headers, func(_ string, v *openapi2.Header) bool { return v == nil })
}
}
func decodeYAML(data []byte, target interface{}) error {
var value interface{}
if err := yaml.Unmarshal(data, &value); err != nil {
+67
View File
@@ -2,6 +2,7 @@ package swagger
import (
"os"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
@@ -39,3 +40,69 @@ func TestSwaggerAPIParser(t *testing.T) {
}
require.ElementsMatch(t, gotMethodsToURLs, expectedURLs, "could not get swagger urls")
}
func TestSwaggerParseNullEntries(t *testing.T) {
// Each spec has a null map value or list item that decodes to a nil
// pointer and used to panic inside openapi2conv.ToV3.
specs := map[string]string{
"null-path-item": `
swagger: "2.0"
info: {title: t, version: "1"}
host: localhost
paths:
/x:
`,
"null-parameter": `
swagger: "2.0"
info: {title: t, version: "1"}
host: localhost
paths:
/x:
get:
parameters:
-
responses: {"200": {description: ok}}
`,
"null-response": `
swagger: "2.0"
info: {title: t, version: "1"}
host: localhost
paths:
/x:
get:
responses: {"200": }
`,
"null-response-header": `
swagger: "2.0"
info: {title: t, version: "1"}
host: localhost
responses:
Shared:
description: ok
headers:
X-Shared:
paths:
/x:
get:
responses:
"200":
description: ok
headers:
X-Rate-Limit:
`,
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
var count int
err := New().Parse(strings.NewReader(spec), func(request *types.RequestResponse) bool {
count++
return false
}, "spec.yaml")
require.NoError(t, err)
if name != "null-path-item" {
require.Equal(t, 1, count, "the operation should still produce a request")
}
})
}
}
+71
View File
@@ -0,0 +1,71 @@
{
"log": {
"version": "1.2",
"creator": { "name": "WebInspector", "version": "537.36" },
"entries": [
{
"request": {
"method": "GET",
"url": "https://ginandjuice.shop/catalog?category=Juice",
"httpVersion": "http/2.0",
"headers": [
{ "name": ":authority", "value": "ginandjuice.shop" },
{ "name": ":method", "value": "GET" },
{ "name": ":path", "value": "/catalog?category=Juice" },
{ "name": ":scheme", "value": "https" },
{ "name": "accept", "value": "text/html" },
{ "name": "cookie", "value": "session=abc123" }
],
"queryString": [{ "name": "category", "value": "Juice" }],
"cookies": [],
"headersSize": -1,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "http/2.0",
"headers": [{ "name": "content-type", "value": "text/html; charset=utf-8" }],
"content": { "size": 19, "mimeType": "text/html", "text": "PGgxPkp1aWNlPC9oMT4=", "encoding": "base64" }
}
},
{
"request": {
"method": "POST",
"url": "https://ginandjuice.shop/login",
"httpVersion": "HTTP/1.1",
"headers": [
{ "name": "Host", "value": "ginandjuice.shop" },
{ "name": "Content-Type", "value": "application/x-www-form-urlencoded" }
],
"postData": { "mimeType": "application/x-www-form-urlencoded", "text": "username=carlos&password=hunter2" }
},
"response": {
"status": 302,
"statusText": "Found",
"httpVersion": "HTTP/1.1",
"headers": [{ "name": "Location", "value": "/my-account" }],
"content": { "size": 0, "mimeType": "" }
}
},
{
"request": {
"method": "GET",
"url": "data:image/png;base64,iVBORw0KGgo=",
"httpVersion": "",
"headers": []
},
"response": { "status": 200, "statusText": "", "httpVersion": "", "headers": [], "content": { "size": 0 } }
},
{
"request": {
"method": "GET",
"url": "https://ginandjuice.shop/blocked.js",
"httpVersion": "",
"headers": []
},
"response": { "status": 0, "statusText": "", "httpVersion": "", "headers": [], "content": { "size": 0 } }
}
]
}
}
+9
View File
@@ -89,6 +89,12 @@ func (f *Fingerprinter) Enabled() bool {
// pattern. A value that is not a URL is always accepted: it has no path to
// group by, and guessing would lose targets.
func (f *Fingerprinter) Accept(rawURL string) bool {
return f.AcceptOn(rawURL, "")
}
// AcceptOn is Accept for a target connected to through ip. Each ip of a host
// is a server of its own, so it keeps its own count for every pattern.
func (f *Fingerprinter) AcceptOn(rawURL, ip string) bool {
if !f.Enabled() {
return true
}
@@ -96,6 +102,9 @@ func (f *Fingerprinter) Accept(rawURL string) bool {
if !ok {
return true
}
if ip != "" {
pattern += "@" + ip
}
f.mu.Lock()
defer f.mu.Unlock()
+8
View File
@@ -80,6 +80,14 @@ func TestAcceptKeepsUpToTheCap(t *testing.T) {
require.True(t, f.Accept("http://acme.test/admin"), "a different pattern has its own budget")
}
func TestAcceptOnCountsEachIPApart(t *testing.T) {
f := NewFingerprinter(1)
require.True(t, f.AcceptOn("http://acme.test/user/1", "192.0.2.10"))
require.True(t, f.AcceptOn("http://acme.test/user/1", "192.0.2.11"), "another ip is another server")
require.False(t, f.AcceptOn("http://acme.test/user/2", "192.0.2.10"), "past the cap of its ip")
require.True(t, f.Accept("http://acme.test/user/2"), "the host without an ip has its own budget")
}
// Off by default: dropping a target means never scanning it, so it has to be
// asked for.
func TestDisabledFingerprinterKeepsEverything(t *testing.T) {
+2
View File
@@ -10,6 +10,7 @@ import (
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/burp"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/har"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/json"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/openapi"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/raw"
@@ -142,6 +143,7 @@ func (i *HttpInputProvider) Close() {}
// Supported Providers
var providersList = []formats.Format{
burp.New(),
har.New(),
json.New(),
yaml.New(),
openapi.New(),
+99 -139
View File
@@ -54,6 +54,7 @@ type ListInputProvider struct {
hostMapStream *filekv.FileDB
hostMapStreamOnce sync.Once
profiles *targetprofile.Registry
pinned bool
// normalizeURLs collapses targets that address the same resource onto one
// form before they are deduplicated.
normalizeURLs bool
@@ -88,8 +89,9 @@ func New(opts *Options) (*ListInputProvider, error) {
patterns: normalize.NewFingerprinter(options.MaxURLsPerPattern),
ipOptions: &ipOptions{
ScanAllIPs: options.ScanAllIPs,
IPV4: sliceutil.Contains(options.IPVersion, "4"),
IPV6: sliceutil.Contains(options.IPVersion, "6"),
IPV4: len(options.IPVersion) == 0 || sliceutil.Contains(options.IPVersion, "4"),
IPV6: len(options.IPVersion) == 0 || sliceutil.Contains(options.IPVersion, "6"),
Auto: len(options.IPVersion) == 0,
},
excludedHosts: make(map[string]struct{}),
profiles: targetprofile.NewRegistry(configuration.TemplatesDirectory(options, config.DefaultConfig)),
@@ -167,6 +169,61 @@ func (i *ListInputProvider) TargetProfiles() *targetprofile.Registry {
return i.profiles
}
// selectedIPs returns address pins for a hostname. An empty pin delegates
// DNS and connection fallback to fastdialer, which races IPv4 and IPv6.
// Set and Del share this selection. With -iv omitted and without -sa, one
// unpinned target is returned. -sa returns every selected address.
func (i *ListInputProvider) selectedIPs(executionId, hostname string) []string {
if i.ipOptions == nil || (!i.ipOptions.ScanAllIPs && (i.ipOptions.Auto || !i.ipOptions.IPV6)) {
return []string{""}
}
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
gologger.Warning().Msgf("cannot resolve %s: dialer %s not found", hostname, executionId)
return nil
}
dnsData, err := dialers.Fastdialer.GetDNSData(hostname)
if err != nil {
gologger.Warning().Msgf("DNS resolution failed for %s: %v", hostname, err)
if i.ipOptions.IPV4 {
return []string{""}
}
return nil
}
if len(dnsData.A) == 0 && len(dnsData.AAAA) == 0 {
gologger.Warning().Msgf("DNS resolution returned no IP addresses for %s", hostname)
if i.ipOptions.IPV4 {
return []string{""}
}
return nil
}
if i.ipOptions.ScanAllIPs {
var ips []string
if i.ipOptions.IPV6 {
ips = append(ips, dnsData.AAAA...)
}
if i.ipOptions.IPV4 {
ips = append(ips, dnsData.A...)
}
if len(ips) == 0 {
gologger.Warning().Msgf("no DNS addresses matching -iv selection for %s", hostname)
}
return ips
}
var ips []string
if i.ipOptions.IPV6 && len(dnsData.AAAA) > 0 {
ips = append(ips, dnsData.AAAA[0])
}
// Explicit -iv 4,6 scans the first IPv6 address and one unpinned target.
if i.ipOptions.IPV4 {
ips = append(ips, "")
}
if len(ips) == 0 {
gologger.Warning().Msgf("no IPv6 DNS address found for %s", hostname)
}
return ips
}
// Set normalizes and stores passed input values
func (i *ListInputProvider) Set(executionId string, value string) {
i.set(executionId, value, nil)
@@ -201,73 +258,11 @@ func (i *ListInputProvider) set(executionId string, value string, selection *tar
return
}
if i.ipOptions.ScanAllIPs {
// scan all ips
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
panic("dialers with executionId " + executionId + " not found")
}
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
if err == nil {
if (len(dnsData.A) + len(dnsData.AAAA)) > 0 {
var ips []string
if i.ipOptions.IPV4 {
ips = append(ips, dnsData.A...)
}
if i.ipOptions.IPV6 {
ips = append(ips, dnsData.AAAA...)
}
for _, ip := range ips {
if ip == "" {
continue
}
metaInput := contextargs.NewMetaInput()
metaInput.Input = URL
metaInput.CustomIP = ip
i.setItem(metaInput, selection)
}
return
} else {
gologger.Debug().Msgf("scanAllIps: no ip's found reverting to default")
}
} else {
// failed to scanallips falling back to defaults
gologger.Debug().Msgf("scanAllIps: dns resolution failed: %v", err)
}
}
ips := []string{}
// only scan the target but ipv6 if it has one
if i.ipOptions.IPV6 {
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
panic("dialers with executionId " + executionId + " not found")
}
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
if err == nil && len(dnsData.AAAA) > 0 {
// pick/ prefer 1st
ips = append(ips, dnsData.AAAA[0])
} else {
gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %v\n", err)
}
}
if i.ipOptions.IPV4 {
// if IPV4 is enabled do not specify ip let dialer handle it
ips = append(ips, "")
}
for _, ip := range ips {
for _, ip := range i.selectedIPs(executionId, urlx.Hostname()) {
metaInput := contextargs.NewMetaInput()
if ip != "" {
metaInput.Input = URL
metaInput.CustomIP = ip
i.setItem(metaInput, selection)
} else {
metaInput.Input = URL
i.setItem(metaInput, selection)
}
metaInput.Input = URL
metaInput.CustomIP = ip
i.setItem(metaInput, selection)
}
}
@@ -403,11 +398,15 @@ func (i *ListInputProvider) scanInputFromReader(executionId string, reader io.Re
func (i *ListInputProvider) addInput(executionId string, item string) error {
var selection *targetprofile.Selection
if targetprofile.IsLine(item) {
target, parsed, err := i.profiles.ParseLine(item)
line, err := i.profiles.ParseLine(item)
if err != nil {
return errors.Wrap(err, "could not parse target line")
}
item, selection = target, parsed
if line.IP != "" {
i.setPinned(executionId, line)
return nil
}
item, selection = line.Target, line.Selection
}
switch {
@@ -421,6 +420,29 @@ func (i *ListInputProvider) addInput(executionId string, item string) error {
return nil
}
// setPinned stores a target pinned to an ip. Its hostname is kept for the Host
// header, SNI and template variables, and connections go to the ip, so the ip
// is checked against the network policy (-eh, -lna) here.
func (i *ListInputProvider) setPinned(executionId string, line targetprofile.Line) {
if dialers := protocolstate.GetDialersWithId(executionId); dialers != nil && dialers.NetworkPolicy != nil {
if !dialers.NetworkPolicy.Validate(line.IP) {
gologger.Warning().Msgf("Skipping %s: ip %s is not allowed by the network policy", line.Target, line.IP)
i.skippedCount++
return
}
}
metaInput := contextargs.NewMetaInput()
metaInput.Input = line.Target
metaInput.CustomIP = line.IP
i.setItem(metaInput, line.Selection)
i.pinned = true
}
// PinnedTargets reports whether any target was pinned to an ip.
func (i *ListInputProvider) PinnedTargets() bool {
return i.pinned
}
// isExcluded checks if a URL is in the exclusion list
func (i *ListInputProvider) isExcluded(URL string) bool {
metaInput := contextargs.NewMetaInput()
@@ -463,73 +485,11 @@ func (i *ListInputProvider) Del(executionId string, value string) {
return
}
if i.ipOptions.ScanAllIPs {
// scan all ips
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
panic("dialers with executionId " + executionId + " not found")
}
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
if err == nil {
if (len(dnsData.A) + len(dnsData.AAAA)) > 0 {
var ips []string
if i.ipOptions.IPV4 {
ips = append(ips, dnsData.A...)
}
if i.ipOptions.IPV6 {
ips = append(ips, dnsData.AAAA...)
}
for _, ip := range ips {
if ip == "" {
continue
}
metaInput := contextargs.NewMetaInput()
metaInput.Input = value
metaInput.CustomIP = ip
i.delItem(metaInput)
}
return
} else {
gologger.Debug().Msgf("scanAllIps: no ip's found reverting to default")
}
} else {
// failed to scanallips falling back to defaults
gologger.Debug().Msgf("scanAllIps: dns resolution failed: %v", err)
}
}
ips := []string{}
// only scan the target but ipv6 if it has one
if i.ipOptions.IPV6 {
dialers := protocolstate.GetDialersWithId(executionId)
if dialers == nil {
panic("dialers with executionId " + executionId + " not found")
}
dnsData, err := dialers.Fastdialer.GetDNSData(urlx.Hostname())
if err == nil && len(dnsData.AAAA) > 0 {
// pick/ prefer 1st
ips = append(ips, dnsData.AAAA[0])
} else {
gologger.Warning().Msgf("target does not have ipv6 address falling back to ipv4 %v\n", err)
}
}
if i.ipOptions.IPV4 {
// if IPV4 is enabled do not specify ip let dialer handle it
ips = append(ips, "")
}
for _, ip := range ips {
for _, ip := range i.selectedIPs(executionId, urlx.Hostname()) {
metaInput := contextargs.NewMetaInput()
if ip != "" {
metaInput.Input = URL
metaInput.CustomIP = ip
i.delItem(metaInput)
} else {
metaInput.Input = URL
i.delItem(metaInput)
}
metaInput.Input = URL
metaInput.CustomIP = ip
i.delItem(metaInput)
}
}
@@ -556,19 +516,19 @@ func (i *ListInputProvider) setItem(metaInput *contextargs.MetaInput, selection
if _, ok := i.hostMap.Get(dedupKey); ok {
i.dupeCount++
if i.profiles != nil {
i.profiles.Merge(metaInput.Input, selection)
i.profiles.Merge(metaInput, selection)
}
return
}
// Past the per-pattern cap the shape is already covered by the targets kept
// for it, so scanning another of the same shape buys nothing.
if i.patterns != nil && !i.patterns.Accept(metaInput.Input) {
if i.patterns != nil && !i.patterns.AcceptOn(metaInput.Input, metaInput.CustomIP) {
i.patternSkippedCount++
return
}
if i.profiles != nil {
i.profiles.Bind(metaInput.Input, selection)
i.profiles.Bind(metaInput, selection)
}
i.inputCount++ // tracks target count
// the entry is keyed by the collapsed form and carries the original, which
Loaded 100 of 359 files, more files were not shown because too many files have changed in this diff. Show more