mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 17:39:09 -10:00
Compare commits
181
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
746e92e675 | ||
|
|
3f88aad2cc | ||
|
|
0eb52d1a07 | ||
|
|
b0eeadc554 | ||
|
|
5670af4c66 | ||
|
|
6c4fa11c96 | ||
|
|
88f169d56d | ||
|
|
7c0cb98e9b | ||
|
|
f6f8786531 | ||
|
|
c041c5b1e4 | ||
|
|
f3b817b818 | ||
|
|
be87581d1e | ||
|
|
e647e92e67 | ||
|
|
68049c5985 | ||
|
|
6cdfcffc6d | ||
|
|
151115fad2 | ||
|
|
6b79ff1a68 | ||
|
|
e78e9ddea8 | ||
|
|
339578d415 | ||
|
|
bdcbf5d1ae | ||
|
|
a3109229fd | ||
|
|
190fb46436 | ||
|
|
97f09fa509 | ||
|
|
f51ae596be | ||
|
|
91456915e9 | ||
|
|
9666293e9f | ||
|
|
a5555a72c2 | ||
|
|
dfe70666a3 | ||
|
|
69f4fe915b | ||
|
|
c8a0d45d38 | ||
|
|
10f1967cdc | ||
|
|
767655e306 | ||
|
|
0bd50f957a | ||
|
|
e264bd82f9 | ||
|
|
6a42a7bf52 | ||
|
|
788973d974 | ||
|
|
7989011cdf | ||
|
|
a151a1d54f | ||
|
|
1db63fdb42 | ||
|
|
af61439dc5 | ||
|
|
9a13e64144 | ||
|
|
a5715288aa | ||
|
|
cc5645e674 | ||
|
|
40075b0b20 | ||
|
|
90a0cce807 | ||
|
|
1caa955c77 | ||
|
|
830e012a10 | ||
|
|
9b368f3eaa | ||
|
|
59e6ffa6fb | ||
|
|
b870cdcd67 | ||
|
|
3f776b5ff0 | ||
|
|
ad74f0094c | ||
|
|
4137bedd31 | ||
|
|
a48a58ef59 | ||
|
|
0de51237b7 | ||
|
|
4857883667 | ||
|
|
bccaa4410d | ||
|
|
d8253b97e9 | ||
|
|
65f60dd0a6 | ||
|
|
1115f18b68 | ||
|
|
458367feb7 | ||
|
|
0af8c98679 | ||
|
|
0217339d5d | ||
|
|
d23849e996 | ||
|
|
59dad1b619 | ||
|
|
889c8ff9c5 | ||
|
|
c990724404 | ||
|
|
50e0afd188 | ||
|
|
04d0caa646 | ||
|
|
80a001ca0f | ||
|
|
1b080a7d19 | ||
|
|
a847374f2c | ||
|
|
9c9247895d | ||
|
|
37052a7e3f | ||
|
|
c4825e98c9 | ||
|
|
0c0e886c2c | ||
|
|
b3e02efa7f | ||
|
|
aa3c7a361c | ||
|
|
afb2afaf4f | ||
|
|
6fde18d311 | ||
|
|
62721e589a | ||
|
|
66ef296067 | ||
|
|
496589e7d0 | ||
|
|
bd510ac6bf | ||
|
|
71457b4f69 | ||
|
|
97c8233b72 | ||
|
|
6bb5b5dbd1 | ||
|
|
8f23a8633f | ||
|
|
28d5b3e8c0 | ||
|
|
c2d38083dc | ||
|
|
1884ed920f | ||
|
|
d28dd2c745 | ||
|
|
3336556187 | ||
|
|
2c8f4a0318 | ||
|
|
8c8a1a9d78 | ||
|
|
81749cf750 | ||
|
|
846f4425b0 | ||
|
|
fa876a4d92 | ||
|
|
0ae69a7d53 | ||
|
|
80b58e6e9b | ||
|
|
4d7e91e4e0 | ||
|
|
6b9208cdbf | ||
|
|
26b28c69a4 | ||
|
|
a14b3de2a4 | ||
|
|
3c434fb73a | ||
|
|
f40fa195b6 | ||
|
|
a2b6d2abc7 | ||
|
|
b87b28d7ad | ||
|
|
4d9f2f9867 | ||
|
|
4177ca752b | ||
|
|
8ffccaf639 | ||
|
|
fd1da307b6 | ||
|
|
5674792a7c | ||
|
|
a80d0b881a | ||
|
|
0e243e3edb | ||
|
|
54f9310945 | ||
|
|
3a827b8c8b | ||
|
|
97e13fae6e | ||
|
|
1b8e3515bf | ||
|
|
0de1d12cc9 | ||
|
|
47f74706a5 | ||
|
|
dad93f0ed6 | ||
|
|
3adff6347b | ||
|
|
c5611b9ac4 | ||
|
|
77d3b6d314 | ||
|
|
c36316de36 | ||
|
|
010ee044b2 | ||
|
|
71f9782b2c | ||
|
|
536e427c01 | ||
|
|
ae203e7aad | ||
|
|
2b6b1d3371 | ||
|
|
3fdd8ca091 | ||
|
|
af77e344f0 | ||
|
|
1b244b2330 | ||
|
|
fb1a318c6a | ||
|
|
dbc69f7bf0 | ||
|
|
aed2cb4643 | ||
|
|
15e2d88ca5 | ||
|
|
4d967cba5c | ||
|
|
dece1c5ea9 | ||
|
|
5e5758b7d3 | ||
|
|
06fb71ebf2 | ||
|
|
c50f92fdd3 | ||
|
|
5b6510fb79 | ||
|
|
a5b59a9a4e | ||
|
|
14283b0e13 | ||
|
|
322a40e1db | ||
|
|
aa457f1c44 | ||
|
|
bd0c398887 | ||
|
|
b4a5cdab02 | ||
|
|
1981edbaac | ||
|
|
0e2fb326d6 | ||
|
|
ef0faa7d6f | ||
|
|
e8b53505e5 | ||
|
|
1d6b532bbc | ||
|
|
b6cea874d3 | ||
|
|
36dafa988b | ||
|
|
d7bdb9c058 | ||
|
|
f3c12c43eb | ||
|
|
4dad173f0a | ||
|
|
1b557852c4 | ||
|
|
e5052eb771 | ||
|
|
56411349d2 | ||
|
|
77dda01e67 | ||
|
|
a7faddf748 | ||
|
|
d47ace7701 | ||
|
|
37c06098ad | ||
|
|
12fd62163b | ||
|
|
748ff1381e | ||
|
|
026a1890b8 | ||
|
|
e14933f765 | ||
|
|
bd639a5aff | ||
|
|
fcba75d191 | ||
|
|
33067508b1 | ||
|
|
d4a40d2b04 | ||
|
|
c063d77d2d | ||
|
|
ddb2edb2e9 | ||
|
|
fe14cf5d2c | ||
|
|
89db6cf43a | ||
|
|
a78ea81854 | ||
|
|
e30c800e8c |
No files matched your search
@@ -17,6 +17,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# lint gates every other job; none of them uses another's output, so they run
|
||||
# in parallel once it passes instead of queueing behind the unit tests
|
||||
lint:
|
||||
name: "Lint"
|
||||
if: ${{ !endsWith(github.actor, '[bot]') }}
|
||||
@@ -45,16 +47,6 @@ jobs:
|
||||
- uses: actions/[email protected]
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- uses: projectdiscovery/actions/free-disk-space@v1
|
||||
with:
|
||||
llvm: 'false'
|
||||
php: 'false'
|
||||
mongodb: 'false'
|
||||
mysql: 'false'
|
||||
misc-packages: 'false'
|
||||
docker-images: 'false'
|
||||
tools-cache: 'false'
|
||||
- run: make build
|
||||
# The data-race detector is OS-independent, so we only pay its ~2-3x cost
|
||||
# on ubuntu; windows/macOS run the same suite without instrumentation.
|
||||
- name: "Unit tests (race)"
|
||||
@@ -85,7 +77,7 @@ jobs:
|
||||
|
||||
sdk:
|
||||
name: "Run example SDK"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
@@ -106,7 +98,7 @@ jobs:
|
||||
|
||||
integration:
|
||||
name: "Integration tests"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
strategy:
|
||||
@@ -119,6 +111,18 @@ jobs:
|
||||
- uses: projectdiscovery/actions/setup/python@v1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
# The suite launches one nuclei process per case, ~175 of them, and the
|
||||
# binary is around 200MB. This job takes roughly four times longer on
|
||||
# windows than on linux for the same work; Defender scanning an executable
|
||||
# on every launch is the leading suspect for the difference.
|
||||
- name: Exclude the workspace from Defender scanning
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath "${{ github.workspace }}"
|
||||
Add-MpPreference -ExclusionPath "$env:USERPROFILE\go"
|
||||
Add-MpPreference -ExclusionProcess "nuclei.exe"
|
||||
Add-MpPreference -ExclusionProcess "go.exe"
|
||||
- run: make integration
|
||||
env:
|
||||
PDCP_API_KEY: "${{ secrets.PDCP_API_KEY }}"
|
||||
@@ -126,7 +130,7 @@ jobs:
|
||||
|
||||
functional:
|
||||
name: "Functional tests"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
strategy:
|
||||
@@ -147,7 +151,7 @@ jobs:
|
||||
|
||||
validate:
|
||||
name: "Template validate"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
@@ -159,7 +163,7 @@ jobs:
|
||||
|
||||
codeql:
|
||||
name: "CodeQL analysis"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -201,7 +205,7 @@ jobs:
|
||||
|
||||
release:
|
||||
name: "Release test"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
@@ -210,7 +214,7 @@ jobs:
|
||||
|
||||
flamegraph:
|
||||
name: "Flamegraph"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
uses: ./.github/workflows/flamegraph.yaml
|
||||
|
||||
# perf-regression:
|
||||
|
||||
@@ -94,7 +94,7 @@ test:
|
||||
# Keep this under the GitHub Actions step timeout (50m) so a hung nuclei process
|
||||
# is reported by go test instead of the runner killing the job with no test name.
|
||||
integration:
|
||||
$(GOTEST) -tags=integration -timeout 40m ./internal/tests/integration
|
||||
$(GOTEST) -tags=integration -timeout 40m -v ./internal/tests/integration
|
||||
|
||||
integration-debug:
|
||||
$(GOTEST) -tags=integration ./internal/tests/integration -v $(GO_TEST_ARGS) -args $(INTEGRATION_ARGS)
|
||||
@@ -112,7 +112,7 @@ functional: build
|
||||
exit 1; \
|
||||
fi; \
|
||||
RELEASE_BINARY="$$release_binary" DEV_BINARY="$(PWD)/bin/nuclei" \
|
||||
$(GOTEST) -tags=functional -timeout 1h ./internal/tests/functional
|
||||
$(GOTEST) -tags=functional -timeout 1h -v ./internal/tests/functional
|
||||
|
||||
tidy:
|
||||
$(GOMOD) tidy
|
||||
|
||||
@@ -145,6 +145,8 @@ TARGET:
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
@@ -173,6 +175,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
@@ -284,6 +294,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)
|
||||
@@ -327,6 +339,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
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (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)
|
||||
|
||||
@@ -2475,6 +2475,136 @@ xpath:
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>prompt</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Prompt is the natural-language question for an llm matcher. The model
|
||||
judges the selected response part and returns a verdict; the matcher
|
||||
fires when the verdict equals Expect with at least MinConfidence.
|
||||
|
||||
{{...}} placeholders are interpolated from the template variables,
|
||||
-var and target. Response derived values are not interpolated, since
|
||||
the response is attacker controlled.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
prompt: Is this a working admin login form rather than a marketing page?
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>expect</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Expect is the verdict that counts as a match for an llm matcher.
|
||||
Defaults to "yes".
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>options</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Options restricts the model to a fixed set of verdicts for an llm
|
||||
matcher. Defaults to yes/no.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>min-confidence</code> <i>float64</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MinConfidence is the minimum confidence (0-1) an llm verdict needs to
|
||||
count as a match. Defaults to 0.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>inputs</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Inputs compares several responses instead of a single part, for
|
||||
checks that are about the difference between responses (blind
|
||||
boolean injection, user enumeration, IDOR). Each entry is resolved
|
||||
against the runtime values, so {{body_1}} and {{body_2}} work with
|
||||
req-condition, and each response is framed separately for the model.
|
||||
Part is ignored when Inputs is set.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
- '{{body_1}}'
|
||||
- '{{body_2}}'
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>max-input-tokens</code> <i>int</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MaxInputTokens caps how much of the response part is sent to the model
|
||||
for an llm matcher, as an approximate token count.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>allow-sole</code> <i>bool</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
AllowSole permits an llm matcher to be the only matcher on a high or
|
||||
critical template. Without it such a template is rejected, so a model is
|
||||
never the sole arbiter of a severe finding by accident.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -2600,6 +2730,8 @@ Enum Values:
|
||||
- <code>dsl</code>
|
||||
|
||||
- <code>xpath</code>
|
||||
|
||||
- <code>llm</code>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -273,6 +273,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
|
||||
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
|
||||
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("target-format", "Target-Format",
|
||||
@@ -304,6 +306,14 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableSelfContainedTemplates, "enable-self-contained", "esc", false, "enable loading self-contained templates"),
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
flagSet.IntVar(&options.LLMMaxCalls, "llm-max-calls", 5000, "maximum llm calls per scan (0 for unlimited)"),
|
||||
flagSet.IntVar(&options.LLMConcurrency, "llm-concurrency", 4, "maximum concurrent llm calls"),
|
||||
flagSet.BoolVar(&options.LLMCache, "llm-cache", true, "cache llm responses within a scan"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("filters", "Filtering",
|
||||
@@ -422,6 +432,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"),
|
||||
@@ -472,6 +484,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"),
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/itchyny/gojq v0.12.17
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/fastdialer v0.5.20
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/interactsh v1.3.1
|
||||
github.com/projectdiscovery/rawhttp v0.1.92
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/weppos/publicsuffix-go v0.50.3 // indirect
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/text v0.41.0
|
||||
)
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
carvel.dev/ytt v0.52.0
|
||||
@@ -48,11 +13,13 @@ require (
|
||||
github.com/FalconOpsLLC/goexec v0.3.0
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415
|
||||
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
github.com/alitto/pond v1.9.2
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/antchfx/xpath v1.3.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
@@ -71,19 +38,28 @@ require (
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/getkin/kin-openapi v0.144.0
|
||||
github.com/getsops/sops/v3 v3.9.2
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-pdf/fpdf v0.9.0
|
||||
github.com/go-pg/pg/v10 v10.15.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/go-github/v30 v30.1.0
|
||||
github.com/google/go-github/v92 v92.0.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/hdm/jarm-go v0.0.8
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/itchyny/gojq v0.12.17
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4
|
||||
github.com/jhump/protoreflect v1.17.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kitabisa/go-ci v1.0.3
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
|
||||
github.com/lib/pq v1.12.3
|
||||
@@ -92,45 +68,72 @@ require (
|
||||
github.com/maypok86/otter/v2 v2.2.1
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/oiweiwei/go-msrpc v1.2.12
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.8.24
|
||||
github.com/projectdiscovery/fastdialer v0.5.26
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/goflags v0.2.1
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
|
||||
github.com/projectdiscovery/gologger v1.1.73
|
||||
github.com/projectdiscovery/gostruct v0.0.2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/httpx v1.12.0
|
||||
github.com/projectdiscovery/interactsh v1.4.1
|
||||
github.com/projectdiscovery/mapcidr v1.1.97
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55
|
||||
github.com/projectdiscovery/ratelimit v0.0.94
|
||||
github.com/projectdiscovery/rawhttp v0.2.0
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31
|
||||
github.com/projectdiscovery/sarif v0.1.0
|
||||
github.com/projectdiscovery/tlsx v1.4.0
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/projectdiscovery/utils v0.11.11-0.20261020211236-cde16c1381ec
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sandrolain/httpcache v1.4.0
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
|
||||
github.com/trivago/tgo v1.0.7
|
||||
github.com/yassinebenaid/godump v0.11.1
|
||||
github.com/zmap/zgrab2 v0.1.8
|
||||
gitlab.com/gitlab-org/api/client-go v1.9.1
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/term v0.45.0
|
||||
google.golang.org/grpc v1.83.1
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
moul.io/http2curl v1.0.0
|
||||
)
|
||||
@@ -149,7 +152,6 @@ require (
|
||||
cloud.google.com/go/storage v1.56.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect
|
||||
@@ -162,6 +164,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.13.0 // indirect
|
||||
@@ -204,8 +207,8 @@ require (
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.2 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.3 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/censys/censys-sdk-go v0.19.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -216,6 +219,7 @@ require (
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.5 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -227,7 +231,7 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
@@ -238,6 +242,8 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emersion/go-smtp v0.24.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
@@ -247,7 +253,7 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gaissmai/bart v0.29.0 // indirect
|
||||
github.com/gaissmai/bart v0.30.0 // indirect
|
||||
github.com/geoffgarside/ber v1.2.0 // indirect
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -267,6 +273,10 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goburrow/cache v0.1.4 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
@@ -275,6 +285,7 @@ require (
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
@@ -296,7 +307,6 @@ require (
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.15.0 // indirect
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
|
||||
github.com/hdm/jarm-go v0.0.8 // indirect
|
||||
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/indece-official/go-ebcdic v1.2.0 // indirect
|
||||
@@ -308,6 +318,7 @@ require (
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
|
||||
github.com/kataras/jwt v0.1.10 // indirect
|
||||
@@ -318,6 +329,7 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
@@ -326,7 +338,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.3 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
@@ -363,22 +375,24 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/projectdiscovery/asnmap v1.1.1 // indirect
|
||||
github.com/projectdiscovery/blackrock v0.0.2 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.0 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.2 // indirect
|
||||
github.com/projectdiscovery/freeport v0.0.7 // indirect
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 // indirect
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
|
||||
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sashabaranov/go-openai v1.37.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||
@@ -404,6 +418,7 @@ require (
|
||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -415,11 +430,17 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.17 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zcalusic/sysinfo v1.1.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
@@ -429,54 +450,26 @@ require (
|
||||
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.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
google.golang.org/api v0.264.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/goburrow/cache v0.1.4 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/trivago/tgo v1.0.7
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/mod v0.39.0 // indirect
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
// https://go.dev/ref/mod#go-mod-file-retract
|
||||
retract v3.2.0 // retract due to broken js protocol issue
|
||||
@@ -64,6 +64,8 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34=
|
||||
code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@@ -71,8 +73,6 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
@@ -124,13 +124,12 @@ github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb888350
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 h1:lpSZPcbowbxvKFaFvE1reLTCStezWXcRVk0zzBtUatg=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d h1:xX93NZHxxrHcmN4nn1owvJdr1E/l1kp1Rv7rMr9ly+U=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
|
||||
@@ -169,6 +168,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY=
|
||||
github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
|
||||
github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
@@ -265,17 +266,16 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.3 h1:P3akjLPBtV/i6bHC6LbcLjY3KuoOvfiqF8wFHeP5IhY=
|
||||
github.com/bytedance/sonic v1.15.3/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
|
||||
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
|
||||
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
||||
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -310,17 +310,19 @@ github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0V
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
|
||||
github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
@@ -349,8 +351,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
@@ -382,6 +382,10 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -419,8 +423,8 @@ github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1Zky
|
||||
github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
|
||||
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc=
|
||||
github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
|
||||
@@ -585,9 +589,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
|
||||
github.com/google/go-github/v92 v92.0.0 h1:4vW4RVffwIvoEfIA4RX09mRSB47qQX557/2+HgeWRRg=
|
||||
github.com/google/go-github/v92 v92.0.0/go.mod h1:w3CH62ZcmRfvW1cdXpyTztSOVMtdjxtKpgo0GouLmjY=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
@@ -639,6 +642,8 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
|
||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -773,6 +778,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa h1:KQKuQDgA3DZX6C396lt3WDYB9Um1gLITLbvficVbqXk=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa/go.mod h1:HbwNE4XGwjgtUELkvQaAOjWrpianHYZdQVNqSdYW3UM=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
@@ -808,8 +817,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
|
||||
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
|
||||
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
|
||||
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -922,14 +931,14 @@ github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kI
|
||||
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
|
||||
github.com/projectdiscovery/blackrock v0.0.2 h1:mxXdu0uM8P2L2Qi210COlU8QiICPFW/Rxk5QUhlPO2k=
|
||||
github.com/projectdiscovery/blackrock v0.0.2/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
|
||||
github.com/projectdiscovery/cdncheck v1.3.0 h1:/bWdNXIU1P5ASZuW8QA+tAw1FNHhmWlWXk/qiUHyx2c=
|
||||
github.com/projectdiscovery/cdncheck v1.3.0/go.mod h1:GIAA0SqjDfxzDQjHR/X+bK05BGmUGfsVplP5Zdv/kHA=
|
||||
github.com/projectdiscovery/cdncheck v1.3.2 h1:I453ZWNBeG0WWIy3wi9412l4PTzorLgebEnqmbcpSfo=
|
||||
github.com/projectdiscovery/cdncheck v1.3.2/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
|
||||
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
|
||||
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8 h1:zaAfFHjyLPq9Aoii1mXPzXkITvXyPJTSZo6nNhzC1xI=
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8/go.mod h1:TYqDNtD4hU8bGFgVxPqhYQ9K1z5rMLV/YFyvQgcU5TM=
|
||||
github.com/projectdiscovery/fastdialer v0.5.20 h1:MlXoVXQERJY4ILqMNYfuoTKVR4Ix4XonJLkFaoHzgM0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.20/go.mod h1:zdtIbBt5ELNygMEPDSyQ9Nr9KpP9FuAvJwzNIrZZoJA=
|
||||
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
|
||||
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
|
||||
github.com/projectdiscovery/fastdialer v0.5.26 h1:wCCsopLsO1EF1V1+9Wqkxj1bMm6sRo8NdlzwE2Ib2Ss=
|
||||
github.com/projectdiscovery/fastdialer v0.5.26/go.mod h1:dZzEPnpsxqCiD3IkM5CN3RglW3l1gXJNyTkxqjPZIbs=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
|
||||
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
|
||||
@@ -938,8 +947,10 @@ github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c h1:s+lLAlr
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c/go.mod h1:rN35/D3lVx2YDeENFFz06uj8j3XIqK1Ym9XcISF5fzg=
|
||||
github.com/projectdiscovery/goflags v0.2.1 h1:XnSYvsCjUwHbZaaJUZ3YznCJPoXRk/IhTFP9Gecmx6M=
|
||||
github.com/projectdiscovery/goflags v0.2.1/go.mod h1:7nAP1r2Dqgn/rwmOE3EWbZWUCEJKNIhVSBGpuzJAIns=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534 h1:hYd1zQA/dxO2ASyQ6Re73TcJkW1LjLQvt4+86Hxefz8=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb h1:+L1yW2PblRHSSHQRTmvxLkGDPZnM52N62oeBd2xHKF0=
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb/go.mod h1:bfgULaYf+6pjjcLCekpXIYElnLJbmhvTttxMAbQJvhY=
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c h1:4H/CfVNlue9PIwsCM1IGxOynDI7cir+emXuDvGJTLJs=
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d h1:fqqH9LHpN2WDz9QuxFrhKNxXSRtzk+Sa6jAhbB7tXcQ=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d/go.mod h1:Ezmbgdaw4EunGGBU4MQViLoGMJc37LA3ip55YV3KeRI=
|
||||
github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwapyM7ukGU1jFY=
|
||||
@@ -954,8 +965,8 @@ github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP
|
||||
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
|
||||
github.com/projectdiscovery/httpx v1.12.0 h1:NN5iDrw7NjPBDlACdjac4lePTQmZ5aOmgOzptMpP44c=
|
||||
github.com/projectdiscovery/httpx v1.12.0/go.mod h1:OgtyibNGdu9BrJxRdNKm280Davw1Z4s2FAhfPNNtLB4=
|
||||
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
|
||||
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
|
||||
github.com/projectdiscovery/interactsh v1.4.1 h1:EGHPJ1rnuxvILWfcoaRgb/Ykz67WiDXABRCSJN666GA=
|
||||
github.com/projectdiscovery/interactsh v1.4.1/go.mod h1:5g207F6MvIydlKWo4ZzplXkhaCwSq2veuUTNvEFjqps=
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 h1:GUHQYLllWhoca+C4UAT0+TTB1PrUT05klj39V6/KpK8=
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6/go.mod h1:f8P5X5+jEkUqK9z3oNWtSH1g23jMfw3nuEqmtA1jXA4=
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
|
||||
@@ -966,32 +977,32 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
|
||||
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50 h1:z/ZMuNAtPH6Q7BlMZ4DLA1ytm+7TYPAdBCNLjNqaV0U=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50/go.mod h1:QAAMLSm1YsbGeZTFNGBMtyob28B1VUVblNT56wPhDJc=
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294 h1:d5fkf/st46mg+QpOkzOuFBBCYfV8ljO3pmi8CChwIyE=
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294/go.mod h1:pQg2urKomS0WkMmLCF5HhWyuresBXosh1kIfn1Tlwug=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92 h1:38GRsVLNllES/dZcEcA6s6P+rYBp34H9mDphMJMDvvg=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92/go.mod h1:spfXTPjxCj+bfWLynMVB7uOb+LS3PDNAI9HexMEV1Zg=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55 h1:D30pipxOdWnSq3r7sX0kQvGyb18sUgopuda1j9NAmnQ=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.55/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94 h1:2qciSuUxarn73YGICZeprZtjrwyAH+0iVpdKzKCXoXE=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94/go.mod h1:GAerX6s+eWRuACedHRv2/xmMtlW+VahZmbRstQh0sS0=
|
||||
github.com/projectdiscovery/rawhttp v0.2.0 h1:TGiHC5hydL97/zoi0WjEuNrfO6Gjtg9LeXndUf1Cq3M=
|
||||
github.com/projectdiscovery/rawhttp v0.2.0/go.mod h1:Waz+K5y6r44ElpqAoOfYtY03zrGGZ12nx0yDKq7Ot1E=
|
||||
github.com/projectdiscovery/rdap v0.9.0 h1:wPhHx5pQ2QI+WGhyNb2PjhTl0NtB39Nk7YFZ9cp8ZGA=
|
||||
github.com/projectdiscovery/rdap v0.9.0/go.mod h1:zk4yrJFQ2Hy36Aqk+DvotYQxYAeALaCJ5ORySkff36Q=
|
||||
github.com/projectdiscovery/retryabledns v1.0.116 h1:tK28PJ4tLO7bgM89RpIFjkfTPMSbRegwvg9mN/q6z78=
|
||||
github.com/projectdiscovery/retryabledns v1.0.116/go.mod h1:r3vH0AKYtJ9fculcqRFrdVYe2ABcHwZeR4C6j3rqCbY=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26 h1:BkVqMUIVPhx+3vOoo5CxYGNnMqQQYXOedu89XQB1KTg=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26/go.mod h1:poffUOX+jvS0XbaPCYess221jXvSO7R/QlE7zhs3+9Q=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31 h1:0hSEZ5+jj6Oo1IL8qrytD36TK/qCxfe543jEaRqSTjs=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31/go.mod h1:/WEa7YejxTCXEMLCGfGYaC3JsgXgzuqw8rv8cb4buEc=
|
||||
github.com/projectdiscovery/sarif v0.1.0 h1:O541T+a448nSJsmIMnXXSOeDQEzpnCAYvRfe0eG5h74=
|
||||
github.com/projectdiscovery/sarif v0.1.0/go.mod h1:LBC+reM3bkI3qIIhE0rZaINaYX6VG+En6u2hHa5mA7E=
|
||||
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
|
||||
github.com/projectdiscovery/stringsutil v0.0.2/go.mod h1:EJ3w6bC5fBYjVou6ryzodQq37D5c6qbAYQpGmAy+DC0=
|
||||
github.com/projectdiscovery/tlsx v1.4.0 h1:OU2mNK6TOB/YiFX3jhmgwOepFWxPoj5rn8npWiCCz74=
|
||||
github.com/projectdiscovery/tlsx v1.4.0/go.mod h1:WCrUXAHjdKbDNjsu5EbVBS7qvzap3fxi/E85v/Flyx8=
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75 h1:3qJgfWzzwtFXc/vdUIurSzPzyjBH9NQe0wATkZMoQGI=
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75/go.mod h1:8jqB9aHT6U8NkUsENvaSjD/iBEGB4Ta+CbJBMcvzP9c=
|
||||
github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLvh5+UjzATE=
|
||||
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
|
||||
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
|
||||
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668 h1:umwLd2H4q5hfHPdAHSQWdrmBvECZs2qsF9hskvo1jw8=
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668/go.mod h1:DzaUDLQ+Tqsn2nOL47M1mRBZSVyCmwsAlR2E9dxHB4Y=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0 h1:2AsEkjnKgFZK4tjaiNhwC0zfj04rpckSvIOZINeTGyQ=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
|
||||
github.com/projectdiscovery/utils v0.11.11-0.20261020211236-cde16c1381ec h1:VhJiivJR8ljv5Qk3QZQC9og50wspEBB5qkNfdyqNhws=
|
||||
github.com/projectdiscovery/utils v0.11.11-0.20261020211236-cde16c1381ec/go.mod h1:T7KzyElivWJiPP+B4sXqkp1CcFe6O/P0qBn/MjDNa74=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -1018,8 +1029,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
||||
@@ -1046,8 +1057,6 @@ github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQ
|
||||
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
|
||||
github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
@@ -1066,9 +1075,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
@@ -1174,9 +1182,8 @@ github.com/vulncheck-oss/go-exploit v1.51.0/go.mod h1:J28w0dLnA6DnCrnBm9Sbt6smX8
|
||||
github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
|
||||
github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko=
|
||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb h1:QIW0GHVTSubm3VxkKpn1FnFAgins6N8FiGTvUQwpXzw=
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
@@ -1231,6 +1238,8 @@ github.com/yuin/goldmark v1.7.17 h1:p36OVWwRb246iHxA/U4p8OPEpOTESm4n+g+8t0EE5uA=
|
||||
github.com/yuin/goldmark v1.7.17/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
|
||||
@@ -1241,16 +1250,17 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30=
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
|
||||
github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk=
|
||||
github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
|
||||
github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
|
||||
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w=
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 h1:DCz0McWRVJNICkHdu2XpETqeLvPtZXs315OZyUs1BDk=
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77/go.mod h1:aSvf+uTU222mUYq/KQj3oiEU7ajhCZe8RRSLHIoM4EM=
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 h1:B9sBGvtHjiGOOemAh0cCGTzub76DXlxmOh3Qtx3TkRs=
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87/go.mod h1:9O2aSz1l9fUHTFxJMYCoFsZBy6yGlz3nrOp4dHue5qw=
|
||||
github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo=
|
||||
github.com/zmap/zgrab2 v0.1.8 h1:PFnXrIBcGjYFec1JNbxMKQuSXXzS+SbqE89luuF4ORY=
|
||||
github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFnegE=
|
||||
@@ -1288,12 +1298,14 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk
|
||||
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.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
@@ -1326,10 +1338,9 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1367,8 +1378,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1416,10 +1427,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1428,7 +1438,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1447,8 +1456,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1495,7 +1504,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1513,10 +1521,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1528,10 +1535,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1547,10 +1553,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1682,8 +1687,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1699,7 +1704,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module nuclei-tools
|
||||
|
||||
go 1.26.1
|
||||
go 1.27.1
|
||||
|
||||
tool (
|
||||
github.com/dvyukov/go-fuzz/go-fuzz
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
utilsllm "github.com/projectdiscovery/utils/llm"
|
||||
)
|
||||
|
||||
// llmMatcherClient adapts the shared provider layer to the minimal interface
|
||||
// the matchers package needs, keeping matchers free of any provider SDK import.
|
||||
type llmMatcherClient struct {
|
||||
client *utilsllm.Client
|
||||
model string
|
||||
}
|
||||
|
||||
// Model names the model behind a verdict so it can be recorded in the finding.
|
||||
func (a *llmMatcherClient) Model() string {
|
||||
return a.model
|
||||
}
|
||||
|
||||
func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON bool) (string, error) {
|
||||
return a.client.Complete(ctx, utilsllm.Request{
|
||||
Prompt: prompt,
|
||||
Format: utilsllm.Format{JSON: asJSON},
|
||||
})
|
||||
}
|
||||
|
||||
// configureLLM builds the scan's llm client from options. It returns nil unless
|
||||
// -llm is set, so nothing reaches a model until the user opts in. The API key is
|
||||
// read from the environment by the provider layer, never from options.
|
||||
//
|
||||
// The client is returned rather than installed globally so two scans in one
|
||||
// process cannot overwrite each other's provider, cache, budget, or timeout.
|
||||
func configureLLM(options *types.Options) (llmclient.Client, error) {
|
||||
if !options.EnableLLM {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
client, err := utilsllm.New(utilsllm.Config{
|
||||
Provider: options.LLMProvider,
|
||||
BaseURL: options.LLMBaseURL,
|
||||
Model: options.LLMModel,
|
||||
Timeout: time.Duration(options.LLMTimeout) * time.Second,
|
||||
Cache: options.LLMCache,
|
||||
MaxCalls: options.LLMMaxCalls,
|
||||
MaxConcurrency: options.LLMConcurrency,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
|
||||
}
|
||||
@@ -56,10 +56,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)
|
||||
@@ -171,6 +179,9 @@ func ValidateOptions(options *types.Options) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -59,3 +59,24 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,3 +65,21 @@ func loadProxyServers(options *types.Options) error {
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -46,6 +47,7 @@ import (
|
||||
fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
parsers "github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/projectfile"
|
||||
@@ -54,6 +56,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"
|
||||
@@ -109,6 +112,7 @@ type Runner struct {
|
||||
httpApiEndpoint *httpapi.Server
|
||||
fuzzStats *fuzzStats.Tracker
|
||||
dastServer *server.DASTServer
|
||||
llmClient llmclient.Client
|
||||
}
|
||||
|
||||
// New creates a new client for running the enumeration process.
|
||||
@@ -122,6 +126,12 @@ func New(options *types.Options) (*Runner, error) {
|
||||
return nil, fmt.Errorf("initialize nuclei configuration: %w", err)
|
||||
}
|
||||
|
||||
llmClient, err := configureLLM(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure llm: %w", err)
|
||||
}
|
||||
runner.llmClient = llmClient
|
||||
|
||||
if options.HealthCheck {
|
||||
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
|
||||
os.Exit(0)
|
||||
@@ -657,6 +667,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts := &protocols.ExecutorOptions{
|
||||
Output: r.output,
|
||||
Options: r.options,
|
||||
LLMClient: r.llmClient,
|
||||
Progress: r.progress,
|
||||
Catalog: r.catalog,
|
||||
IssuesClient: r.issuesClient,
|
||||
@@ -717,6 +728,10 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if r.options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
|
||||
}
|
||||
|
||||
executorEngine := core.New(r.options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -728,6 +743,13 @@ func (r *Runner) RunEnumeration() error {
|
||||
|
||||
// If using input-file flags, only load http fuzzing based templates.
|
||||
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
|
||||
profiles := r.targetProfiles()
|
||||
if profiles != nil {
|
||||
if r.options.AutomaticScan {
|
||||
return errors.New("per-target profiles cannot be combined with automatic scan")
|
||||
}
|
||||
loaderConfig.TargetFilter = profiles.LoadFilter()
|
||||
}
|
||||
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
r.options.DAST = true
|
||||
@@ -772,6 +794,10 @@ func (r *Runner) RunEnumeration() error {
|
||||
if err := store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
if profiles != nil {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
}
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
)
|
||||
|
||||
// targetProfiles returns the per-target profiles read from input, or nil when
|
||||
// no target has one.
|
||||
func (r *Runner) targetProfiles() *targetprofile.Registry {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
TargetProfiles() *targetprofile.Registry
|
||||
})
|
||||
if !ok || !provider.TargetProfiles().Scoped() {
|
||||
return nil
|
||||
}
|
||||
return provider.TargetProfiles()
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
@@ -106,6 +107,10 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if opts.Options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
|
||||
}
|
||||
|
||||
executorEngine := core.New(opts.Options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package functional_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -280,6 +281,12 @@ func trimMatchingQuotes(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func TestIsRetryableFunctionalError(t *testing.T) {
|
||||
if isRetryableFunctionalError(nil) || isRetryableFunctionalError(fmt.Errorf("load mismatch")) {
|
||||
t.Fatal("non-exit errors should not retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitFunctionalArgs(t *testing.T) {
|
||||
t.Run("quoted values", func(t *testing.T) {
|
||||
args := splitFunctionalArgs(`{{binary}} -tags "cve,exposure" -author "geeknik,pdteam" -severity high,critical`)
|
||||
@@ -338,6 +345,29 @@ func runFunctionalCase(args []string, debug bool) error {
|
||||
return fmt.Errorf("functional runners are not initialized")
|
||||
}
|
||||
|
||||
const attempts = 2
|
||||
var last error
|
||||
for attempt := 1; attempt <= attempts; attempt++ {
|
||||
last = compareFunctionalLoad(args, debug)
|
||||
if last == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRetryableFunctionalError(last) {
|
||||
return last
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
func isRetryableFunctionalError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
return errors.As(err, &exitErr)
|
||||
}
|
||||
|
||||
func compareFunctionalLoad(args []string, debug bool) error {
|
||||
var (
|
||||
releaseOutput string
|
||||
currentOutput string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,17 +73,27 @@ func TestMain(m *testing.M) {
|
||||
templatesDir := filepath.Join(homeDir, config.NucleiTemplatesDirName)
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
|
||||
baseEnv := []string{
|
||||
"NUCLEI_TEMPLATES_DIR=" + templatesDir,
|
||||
"XDG_CONFIG_HOME=" + filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME=" + filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME=" + filepath.Join(tempDir, "cache"),
|
||||
}
|
||||
// cases run nuclei with -duc, so the official templates some of them read
|
||||
// are installed once here rather than by whichever cases start first, which
|
||||
// queued them behind each other's downloads inside their own timeouts
|
||||
if err := installTemplates(binaryPath, baseEnv); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
previousRunner := testutils.DefaultRunner()
|
||||
runner := testutils.NewRunner(
|
||||
testutils.WithBinaryPath(binaryPath),
|
||||
testutils.WithWorkingDir(workingFixturesDir),
|
||||
testutils.WithExtraArgs(integrationExtraArgs...),
|
||||
testutils.WithBaseEnv(
|
||||
"NUCLEI_TEMPLATES_DIR="+templatesDir,
|
||||
"XDG_CONFIG_HOME="+filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME="+filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(tempDir, "cache"),
|
||||
),
|
||||
testutils.WithBaseEnv(baseEnv...),
|
||||
)
|
||||
testutils.SetDefaultRunner(runner)
|
||||
|
||||
@@ -124,6 +134,16 @@ func buildNucleiBinary(repoRoot, binaryPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installTemplates(binaryPath string, env []string) error {
|
||||
cmd := exec.Command(binaryPath, "-update-templates", "-no-color")
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install nuclei templates: %w\n%s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDebugMode() bool {
|
||||
for _, envVar := range []string{"DEBUG", "ACTIONS_STEP_DEBUG", "ACTIONS_RUNNER_DEBUG", "RUNNER_DEBUG"} {
|
||||
if envTruthy(envVar) {
|
||||
|
||||
@@ -37,6 +37,8 @@ var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
|
||||
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
|
||||
{Path: "protocols/javascript/websocket-echo.yaml", TestCase: &javascriptWebSocketEcho{}},
|
||||
{Path: "protocols/javascript/websocket-denied.yaml", TestCase: &javascriptWebSocketDenied{}},
|
||||
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
|
||||
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
|
||||
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
)
|
||||
|
||||
// startWebSocketEchoServer starts a WebSocket server on a random loopback port
|
||||
// that accepts the "chat" subprotocol, greets, then echoes every message.
|
||||
func startWebSocketEchoServer() (string, func(), error) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("websocket listen: %w", err)
|
||||
}
|
||||
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := ws.HTTPUpgrader{Protocol: func(protocol string) bool { return protocol == "chat" }}
|
||||
conn, _, _, err := upgrader.Upgrade(r, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if err := wsutil.WriteServerText(conn, []byte("welcome")); err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
payload, op, err := wsutil.ReadClientData(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerMessage(conn, op, payload); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})}
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return lis.Addr().String(), func() { _ = srv.Close() }, nil
|
||||
}
|
||||
|
||||
// javascriptWebSocketEcho exercises the nuclei/websocket library end to end
|
||||
// against a real WebSocket server.
|
||||
type javascriptWebSocketEcho struct{}
|
||||
|
||||
func (j *javascriptWebSocketEcho) Execute(filePath string) error {
|
||||
address, stop, err := startWebSocketEchoServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stop()
|
||||
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// javascriptWebSocketDenied asserts the nuclei/websocket library refuses a host
|
||||
// on the exclude list (network-policy enforcement) before connecting.
|
||||
type javascriptWebSocketDenied struct{}
|
||||
|
||||
func (j *javascriptWebSocketDenied) Execute(filePath string) error {
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type multiProtoDynamicExtractor struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *multiProtoDynamicExtractor) Execute(templatePath string) error {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(templatePath, "docs.projectdiscovery.io", debug)
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(templatePath, "https://docs.projectdiscovery.io/", debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
id: websocket-denied
|
||||
|
||||
info:
|
||||
name: WebSocket Network Policy Denial
|
||||
author: pdteam
|
||||
severity: info
|
||||
description: |
|
||||
Asserts the nuclei/websocket javascript library refuses a host on the
|
||||
exclude list before connecting.
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
const websocket = require('nuclei/websocket');
|
||||
let out = "";
|
||||
try {
|
||||
const client = new websocket.Client('ws://203.0.113.10/socket');
|
||||
client.Connect();
|
||||
} catch (e) {
|
||||
out = "" + e;
|
||||
}
|
||||
out;
|
||||
args:
|
||||
Host: "{{Host}}"
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "success == true"
|
||||
- type: word
|
||||
part: response
|
||||
words:
|
||||
- "network policy"
|
||||
- "203.0.113.10"
|
||||
condition: and
|
||||
@@ -0,0 +1,33 @@
|
||||
id: websocket-echo
|
||||
|
||||
info:
|
||||
name: WebSocket Echo
|
||||
author: pdteam
|
||||
severity: info
|
||||
description: |
|
||||
Exercises the nuclei/websocket javascript library end to end: dial through
|
||||
the network policy, negotiate a subprotocol, read the server greeting and
|
||||
round-trip a text message.
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
const websocket = require('nuclei/websocket');
|
||||
const opts = new websocket.Options();
|
||||
opts.Protocols = ['chat'];
|
||||
const client = new websocket.Client('ws://' + address + '/echo', opts);
|
||||
const greeting = client.Receive();
|
||||
client.Send('nuclei-websocket');
|
||||
const echo = client.Receive();
|
||||
client.Close();
|
||||
greeting + ' ' + client.Protocol + ' ' + echo;
|
||||
args:
|
||||
address: "{{Host}}:{{Port}}"
|
||||
matchers-condition: and
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "success == true"
|
||||
- type: word
|
||||
part: response
|
||||
words:
|
||||
- "welcome chat nuclei-websocket"
|
||||
@@ -24,6 +24,6 @@ http:
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- contains(body,'home') # check for http string
|
||||
- location == '/home' # the docs host redirects here
|
||||
- blogid == 'cname' # check for cname (extracted information from dns response)
|
||||
condition: and
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
@@ -261,6 +261,9 @@ func (r *Runner) ArgsResults(debug bool, args ...string) ([]string, error) {
|
||||
output, err := r.runCommand(false, "", func(cmd *exec.Cmd) {
|
||||
cmd.Args = append(cmd.Args[:1], append(append([]string{}, args...), r.ExtraArgs...)...)
|
||||
cmd.Env = r.buildEnv(nil)
|
||||
if r.DisableAutoUpdate {
|
||||
cmd.Args = append(cmd.Args, "-duc")
|
||||
}
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -324,6 +327,10 @@ func (r *Runner) LoadedTemplates(binaryPath string, debug bool, args []string) (
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
out := strings.TrimSpace(string(data))
|
||||
if out != "" {
|
||||
return "", fmt.Errorf("%w: %s", err, out)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
@@ -40,43 +40,46 @@ func Cleanup(options *types.Options) {
|
||||
|
||||
// DefaultOptions is the default options structure for nuclei during mocking.
|
||||
var DefaultOptions = &types.Options{
|
||||
Debug: false,
|
||||
DebugRequests: false,
|
||||
DebugResponse: false,
|
||||
Silent: false,
|
||||
Verbose: false,
|
||||
NoColor: true,
|
||||
UpdateTemplates: false,
|
||||
JSONL: false,
|
||||
OmitRawRequests: false,
|
||||
EnableProgressBar: false,
|
||||
TemplateList: false,
|
||||
Stdin: false,
|
||||
StopAtFirstMatch: false,
|
||||
NoMeta: false,
|
||||
Project: false,
|
||||
MetricsPort: 0,
|
||||
BulkSize: 25,
|
||||
TemplateThreads: 10,
|
||||
Timeout: 5,
|
||||
Retries: 1,
|
||||
RateLimit: 150,
|
||||
RateLimitDuration: time.Second,
|
||||
ProbeConcurrency: 50,
|
||||
ProjectPath: "",
|
||||
Severities: severity.Severities{},
|
||||
Targets: []string{},
|
||||
TargetsFilePath: "",
|
||||
Output: "",
|
||||
Proxy: []string{},
|
||||
TraceLogFile: "",
|
||||
Templates: []string{},
|
||||
ExcludedTemplates: []string{},
|
||||
CustomHeaders: []string{},
|
||||
InteractshURL: "https://oast.fun",
|
||||
InteractionsCacheSize: 5000,
|
||||
InteractionsEviction: 60,
|
||||
InteractionsCoolDownPeriod: 5,
|
||||
Debug: false,
|
||||
DebugRequests: false,
|
||||
DebugResponse: false,
|
||||
Silent: false,
|
||||
Verbose: false,
|
||||
NoColor: true,
|
||||
UpdateTemplates: false,
|
||||
JSONL: false,
|
||||
OmitRawRequests: false,
|
||||
EnableProgressBar: false,
|
||||
TemplateList: false,
|
||||
Stdin: false,
|
||||
StopAtFirstMatch: false,
|
||||
NoMeta: false,
|
||||
Project: false,
|
||||
MetricsPort: 0,
|
||||
BulkSize: 25,
|
||||
TemplateThreads: 10,
|
||||
Timeout: 5,
|
||||
Retries: 1,
|
||||
RateLimit: 150,
|
||||
RateLimitDuration: time.Second,
|
||||
ProbeConcurrency: 50,
|
||||
ProjectPath: "",
|
||||
Severities: severity.Severities{},
|
||||
Targets: []string{},
|
||||
TargetsFilePath: "",
|
||||
Output: "",
|
||||
Proxy: []string{},
|
||||
TraceLogFile: "",
|
||||
Templates: []string{},
|
||||
ExcludedTemplates: []string{},
|
||||
CustomHeaders: []string{},
|
||||
InteractshURL: "https://oast.fun",
|
||||
InteractionsCacheSize: 5000,
|
||||
InteractionsEviction: 60,
|
||||
// Closing an interactsh client sleeps for the cooldown, waiting on callbacks
|
||||
// that a unit test never receives, so every test generating an oast url paid
|
||||
// five seconds for nothing.
|
||||
InteractionsCoolDownPeriod: 0,
|
||||
InteractionsPollDuration: 5,
|
||||
GitHubTemplateRepo: []string{},
|
||||
GitHubToken: "",
|
||||
|
||||
@@ -18,6 +18,9 @@ var knownLeaks = []goleak.Option{
|
||||
// net/http transport maintains idle keep-alive connections whose goroutines
|
||||
// exit on idle timeout or explicit close - not real leaks.
|
||||
goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
|
||||
// go 1.27 moved the bundled http2 transport out of net/http, which renamed
|
||||
// the same idle connection goroutine
|
||||
goleak.IgnoreAnyFunction("net/http/internal/http2.(*ClientConn).readLoop"),
|
||||
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
|
||||
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
|
||||
// expirable LRU cache creates a background goroutine for TTL expiration that persists
|
||||
|
||||
+60
-2
@@ -386,6 +386,21 @@
|
||||
"type": "boolean",
|
||||
"title": "use case insensitive extract",
|
||||
"description": "use case insensitive extract"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"title": "llm extraction prompt",
|
||||
"description": "Natural language instruction describing what to extract; supports {{...}} interpolation of variables and target values"
|
||||
},
|
||||
"schema": {
|
||||
"$ref": "#/$defs/map[string]string",
|
||||
"title": "llm extraction schema",
|
||||
"description": "Fields to extract mapped to their type"
|
||||
},
|
||||
"max-input-tokens": {
|
||||
"type": "integer",
|
||||
"title": "max llm input tokens",
|
||||
"description": "Approximate cap on response tokens sent to the model"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -401,7 +416,8 @@
|
||||
"kval",
|
||||
"xpath",
|
||||
"json",
|
||||
"dsl"
|
||||
"dsl",
|
||||
"llm"
|
||||
],
|
||||
"title": "type of the extractor",
|
||||
"description": "Type of the extractor"
|
||||
@@ -1159,6 +1175,47 @@
|
||||
"title": "xpath queries to match in response",
|
||||
"description": "xpath are the XPath queries that will be evaluated against the response part of nuclei matching rules"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"title": "llm prompt",
|
||||
"description": "Natural language question the model answers about the response part; supports {{...}} interpolation of variables and target values"
|
||||
},
|
||||
"expect": {
|
||||
"type": "string",
|
||||
"title": "expected llm verdict",
|
||||
"description": "Verdict that counts as a match; defaults to yes"
|
||||
},
|
||||
"options": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "allowed llm verdicts",
|
||||
"description": "Fixed set of verdicts the model may return; defaults to yes and no"
|
||||
},
|
||||
"min-confidence": {
|
||||
"type": "number",
|
||||
"title": "minimum llm confidence",
|
||||
"description": "Minimum confidence between 0 and 1 for an llm verdict to match"
|
||||
},
|
||||
"inputs": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "llm inputs to compare",
|
||||
"description": "Responses to compare instead of a single part; each entry supports {{...}} interpolation"
|
||||
},
|
||||
"max-input-tokens": {
|
||||
"type": "integer",
|
||||
"title": "max llm input tokens",
|
||||
"description": "Approximate cap on response tokens sent to the model"
|
||||
},
|
||||
"allow-sole": {
|
||||
"type": "boolean",
|
||||
"title": "allow llm as sole matcher",
|
||||
"description": "Permit an llm matcher to be the only matcher on a high or critical template"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -1198,7 +1255,8 @@
|
||||
"status",
|
||||
"size",
|
||||
"dsl",
|
||||
"xpath"
|
||||
"xpath",
|
||||
"llm"
|
||||
],
|
||||
"title": "type of the matcher",
|
||||
"description": "Type of the matcher"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
|
||||
)
|
||||
|
||||
type cookieAuthContextKey struct{}
|
||||
|
||||
// IsAuthCookie reports whether an auth strategy installed the named cookie.
|
||||
// HTTP cookie reuse must preserve these explicit credentials over jar values.
|
||||
func IsAuthCookie(ctx context.Context, name string) bool {
|
||||
names, _ := ctx.Value(cookieAuthContextKey{}).(map[string]struct{})
|
||||
_, ok := names[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func markCookieAuth(req *http.Request, names []string) {
|
||||
if len(names) == 0 {
|
||||
return
|
||||
}
|
||||
existing, _ := req.Context().Value(cookieAuthContextKey{}).(map[string]struct{})
|
||||
// Contexts can be shared by clones and redirects. Publish a new set rather
|
||||
// than modifying the names recorded by an earlier strategy.
|
||||
protected := make(map[string]struct{}, len(existing)+len(names))
|
||||
// A Header strategy can replace the entire Cookie header. Keep earlier
|
||||
// ownership only for cookie names still present in the request.
|
||||
for _, header := range req.Header.Values("Cookie") {
|
||||
for _, part := range httputil.SplitCookieHeader(header) {
|
||||
name, _, _ := strings.Cut(part, "=")
|
||||
name = strings.TrimSpace(name)
|
||||
if _, ok := existing[name]; ok {
|
||||
protected[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
protected[name] = struct{}{}
|
||||
}
|
||||
*req = *req.WithContext(context.WithValue(req.Context(), cookieAuthContextKey{}, protected))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCookieAuthOwnershipIsRequestLocal(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "session", Value: "configured"}}}).ApplyOnRR(req)
|
||||
cloned := req.Clone(req.Context())
|
||||
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "other", Value: "configured"}}}).ApplyOnRR(cloned)
|
||||
require.True(t, IsAuthCookie(req.Context(), "session"))
|
||||
require.False(t, IsAuthCookie(req.Context(), "other"))
|
||||
require.True(t, IsAuthCookie(cloned.Context(), "session"))
|
||||
require.True(t, IsAuthCookie(cloned.Context(), "other"))
|
||||
require.False(t, IsAuthCookie(cloned.Context(), "Session"))
|
||||
|
||||
NewHeadersAuthStrategy(&Secret{Headers: []KV{{Key: "Cookie", Value: "replacement=configured"}}}).ApplyOnRR(cloned)
|
||||
require.True(t, IsAuthCookie(cloned.Context(), "replacement"))
|
||||
require.False(t, IsAuthCookie(cloned.Context(), "session"))
|
||||
require.False(t, IsAuthCookie(cloned.Context(), "other"))
|
||||
require.True(t, IsAuthCookie(req.Context(), "session"))
|
||||
}
|
||||
|
||||
func TestHeaderAuthCookieOwnershipIgnoresQuotedFragments(t *testing.T) {
|
||||
for _, header := range []string{`payload="a;session=probe"`, `payload="a\";session=probe"`, `payload="a;session=probe`} {
|
||||
t.Run(header, func(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
NewCookiesAuthStrategy(&Secret{Cookies: []Cookie{{Key: "session", Value: "configured"}}}).ApplyOnRR(req)
|
||||
NewHeadersAuthStrategy(&Secret{Headers: []KV{{Key: "Cookie", Value: header}}}).ApplyOnRR(req)
|
||||
require.Equal(t, header, req.Header.Get("Cookie"))
|
||||
require.True(t, IsAuthCookie(req.Context(), "payload"))
|
||||
require.False(t, IsAuthCookie(req.Context(), "session"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,16 @@ func NewCookiesAuthStrategy(data *Secret) *CookiesAuthStrategy {
|
||||
|
||||
// Apply applies the cookies auth strategy to the request
|
||||
func (s *CookiesAuthStrategy) Apply(req *http.Request) {
|
||||
var names []string
|
||||
for _, cookie := range s.Data.Cookies {
|
||||
c := &http.Cookie{
|
||||
Name: cookie.Key,
|
||||
Value: cookie.Value,
|
||||
}
|
||||
req.AddCookie(c)
|
||||
names = append(names, cookie.Key)
|
||||
}
|
||||
markCookieAuth(req, names)
|
||||
}
|
||||
|
||||
// ApplyOnRR applies the cookies auth strategy to the retryable request
|
||||
@@ -51,10 +54,13 @@ func (s *CookiesAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
// Add new cookies
|
||||
var names []string
|
||||
for _, cookie := range s.Data.Cookies {
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: cookie.Key,
|
||||
Value: cookie.Value,
|
||||
})
|
||||
names = append(names, cookie.Key)
|
||||
}
|
||||
markCookieAuth(req.Request, names)
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package authx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,14 @@ func NewHeadersAuthStrategy(data *Secret) *HeadersAuthStrategy {
|
||||
func (s *HeadersAuthStrategy) Apply(req *http.Request) {
|
||||
for _, header := range s.Data.Headers {
|
||||
req.Header[header.Key] = []string{header.Value}
|
||||
if strings.EqualFold(header.Key, "Cookie") {
|
||||
var names []string
|
||||
for _, part := range httputil.SplitCookieHeader(header.Value) {
|
||||
name, _, _ := strings.Cut(part, "=")
|
||||
names = append(names, strings.TrimSpace(name))
|
||||
}
|
||||
markCookieAuth(req, names)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +43,5 @@ func (s *HeadersAuthStrategy) Apply(req *http.Request) {
|
||||
// NOTE: This preserves exact header casing (e.g., barAuthToken stays as barAuthToken)
|
||||
// This is useful for APIs that require case-sensitive header names
|
||||
func (s *HeadersAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
|
||||
for _, header := range s.Data.Headers {
|
||||
req.Header[header.Key] = []string{header.Value}
|
||||
}
|
||||
s.Apply(req.Request)
|
||||
}
|
||||
@@ -59,6 +59,35 @@ func (c *Config) ReadTemplatesConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadTemplateVersion refreshes the installed templates version from disk.
|
||||
// Concurrent processes read the version once at startup, so one that waited on
|
||||
// another's install or update must reload it to see the result instead of
|
||||
// downloading the same release again. State recorded for a different templates
|
||||
// directory is ignored.
|
||||
func (c *Config) ReloadTemplateVersion() error {
|
||||
statePath := c.getTemplatesConfigFilePath()
|
||||
data, err := os.ReadFile(statePath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("read templates state %q: %w", statePath, err)
|
||||
}
|
||||
|
||||
var state templatesState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return fmt.Errorf("decode templates state %q: %w", statePath, err)
|
||||
}
|
||||
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
// compare the resolved directory, so state written through a symlink is
|
||||
// visible to a process using the real path, and the other way around
|
||||
if CanonicalTemplatesPath(state.TemplatesDirectory) == CanonicalTemplatesPath(c.TemplatesDirectory) {
|
||||
c.TemplateVersion = state.TemplateVersion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTemplatesConfig atomically writes restart-persistent templates state.
|
||||
func (c *Config) WriteTemplatesConfig() error {
|
||||
state := templatesState{
|
||||
|
||||
@@ -139,3 +139,94 @@ func writeTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
||||
t.Fatalf("write test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadTemplateVersion(t *testing.T) {
|
||||
newConfig := func(t *testing.T, templatesDir string) *Config {
|
||||
t.Helper()
|
||||
cfg := &Config{stateDir: t.TempDir()}
|
||||
cfg.setTemplatesDir(templatesDir)
|
||||
return cfg
|
||||
}
|
||||
|
||||
t.Run("adopts the version another process installed", func(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
cfg := newConfig(t, templatesDir)
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(templatesDir)
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v2.0.0" {
|
||||
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ignores state of another templates directory", func(t *testing.T) {
|
||||
cfg := newConfig(t, t.TempDir())
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(t.TempDir())
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v1.0.0" {
|
||||
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps the loaded version without state", func(t *testing.T) {
|
||||
cfg := newConfig(t, t.TempDir())
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v1.0.0" {
|
||||
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("adopts the version written through the other name of a symlink", func(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
link := filepath.Join(t.TempDir(), "templates-link")
|
||||
if err := os.Symlink(templatesDir, link); err != nil {
|
||||
t.Skipf("symlinks are unavailable: %v", err)
|
||||
}
|
||||
|
||||
cfg := newConfig(t, link)
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(templatesDir)
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v2.0.0" {
|
||||
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
cfg.setTemplatesDir(templatesDir)
|
||||
writer := &Config{stateDir: cfg.stateDir, TemplateVersion: "v3.0.0"}
|
||||
writer.setTemplatesDir(link)
|
||||
if err := writer.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state through the link: %v", err)
|
||||
}
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v3.0.0" {
|
||||
t.Fatalf("template version = %q, want v3.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CanonicalTemplatesPath is the identity of a templates directory. A symlink
|
||||
// and its target are the same directory, including while the target does not
|
||||
// exist yet, so a lock and a reloaded version follow either name.
|
||||
func CanonicalTemplatesPath(path string) string {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return ""
|
||||
}
|
||||
return resolveTemplatesPath(path, map[string]struct{}{})
|
||||
}
|
||||
|
||||
func resolveTemplatesPath(path string, seen map[string]struct{}) string {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
abs = filepath.Clean(path)
|
||||
}
|
||||
if _, ok := seen[abs]; ok {
|
||||
return abs
|
||||
}
|
||||
seen[abs] = struct{}{}
|
||||
|
||||
dir := abs
|
||||
var missing []string
|
||||
for {
|
||||
info, err := os.Lstat(dir)
|
||||
if err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
return filepath.Join(append([]string{resolved}, missing...)...)
|
||||
}
|
||||
target, readErr := os.Readlink(dir)
|
||||
if readErr != nil {
|
||||
return filepath.Join(append([]string{dir}, missing...)...)
|
||||
}
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(filepath.Dir(dir), target)
|
||||
}
|
||||
resolvedTarget := resolveTemplatesPath(target, seen)
|
||||
return filepath.Join(append([]string{resolvedTarget}, missing...)...)
|
||||
}
|
||||
resolved := dir
|
||||
if r, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
resolved = r
|
||||
}
|
||||
return filepath.Join(append([]string{resolved}, missing...)...)
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return filepath.Join(append([]string{dir}, missing...)...)
|
||||
}
|
||||
missing = append([]string{filepath.Base(dir)}, missing...)
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
|
||||
// IndexVersion is the schema version for cache invalidation on breaking
|
||||
// changes.
|
||||
IndexVersion = 2
|
||||
IndexVersion = 3
|
||||
|
||||
// DefaultMaxSize is the default maximum number of templates to cache.
|
||||
DefaultMaxSize = 50000
|
||||
@@ -70,6 +70,7 @@ func NewIndex(cacheDir string) (*Index, error) {
|
||||
weight += len(value.Name)
|
||||
weight += len(value.Severity)
|
||||
weight += len(value.ProtocolType)
|
||||
weight += len(value.Product)
|
||||
weight += len(value.TemplateVerifier)
|
||||
weight += len(value.VerifierFingerprint)
|
||||
weight += len(value.ContentDigest)
|
||||
|
||||
@@ -905,6 +905,7 @@ func TestNewMetadataFromTemplate(t *testing.T) {
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Low,
|
||||
},
|
||||
Metadata: map[string]any{"product": " Tomcat "},
|
||||
},
|
||||
Verified: true,
|
||||
TemplateVerifier: "verifier",
|
||||
@@ -922,4 +923,5 @@ func TestNewMetadataFromTemplate(t *testing.T) {
|
||||
require.Equal(t, tmpl.Type().String(), metadata.ProtocolType)
|
||||
require.Equal(t, tmpl.Verified, metadata.Verified)
|
||||
require.Equal(t, tmpl.TemplateVerifier, metadata.TemplateVerifier)
|
||||
require.Equal(t, "tomcat", metadata.Product)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package index
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
@@ -48,6 +49,9 @@ type Metadata struct {
|
||||
// ProtocolType is the primary protocol type of the template.
|
||||
ProtocolType string `gob:"protocol_type"`
|
||||
|
||||
// Product is the lowercased info.metadata.product of the template.
|
||||
Product string `gob:"product,omitempty"`
|
||||
|
||||
// Verified indicates whether the template is verified.
|
||||
Verified bool `gob:"verified"`
|
||||
|
||||
@@ -114,6 +118,7 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
|
||||
Severity: tpl.Info.SeverityHolder.Severity.String(),
|
||||
|
||||
ProtocolType: tpl.Type().String(),
|
||||
Product: templateProduct(tpl),
|
||||
|
||||
Verified: tpl.Verified,
|
||||
TemplateVerifier: tpl.TemplateVerifier,
|
||||
@@ -122,6 +127,11 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
func templateProduct(tpl *templates.Template) string {
|
||||
product, _ := tpl.Info.Metadata["product"].(string)
|
||||
return strings.ToLower(strings.TrimSpace(product))
|
||||
}
|
||||
|
||||
// IsValid checks if the cached metadata is still valid by comparing the file
|
||||
// modification time.
|
||||
func (m *Metadata) IsValid() bool {
|
||||
|
||||
@@ -76,6 +76,10 @@ type Config struct {
|
||||
// MetadataIndex is an optional shared index borrowed by the store. The
|
||||
// caller remains responsible for persisting it.
|
||||
MetadataIndex *index.Index
|
||||
|
||||
// TargetFilter narrows loading to the templates per-target profiles
|
||||
// select for at least one target; nil loads every filtered template.
|
||||
TargetFilter index.FilterFunc
|
||||
}
|
||||
|
||||
// Store is a storage for loaded nuclei templates
|
||||
@@ -387,6 +391,11 @@ func (store *Store) buildIndexFilter() *index.Filter {
|
||||
}
|
||||
}
|
||||
|
||||
// selectedByTargets reports whether some target's profile selects the template.
|
||||
func (store *Store) selectedByTargets(metadata *index.Metadata) bool {
|
||||
return store.config.TargetFilter == nil || store.config.TargetFilter(metadata)
|
||||
}
|
||||
|
||||
func (store *Store) loadTemplatesIndex() *index.Index {
|
||||
var metadataIdx *index.Index
|
||||
|
||||
@@ -882,6 +891,10 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
|
||||
return
|
||||
}
|
||||
|
||||
if !store.selectedByTargets(metadata) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tags) > 0 && !slices.ContainsFunc(tags, metadata.HasTag) {
|
||||
return
|
||||
}
|
||||
@@ -916,6 +929,10 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
|
||||
noteExcludedByTag(templatePath, metadata)
|
||||
return
|
||||
}
|
||||
|
||||
if metadata != nil && !store.selectedByTargets(metadata) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
+7
-1
@@ -1,6 +1,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
@@ -40,7 +42,11 @@ type TemplateExecutionCallback func(TemplateExecutionEvent)
|
||||
// templates to leading to the final execution by the work pool, it is
|
||||
// handled by the engine.
|
||||
type Engine struct {
|
||||
workPool *WorkPool
|
||||
workPool *WorkPool
|
||||
// originScoped tracks origin-scoped template attempts. A pair is finished
|
||||
// only after a successful execution, so a failed first target does not
|
||||
// suppress every later URL on that origin.
|
||||
originScoped sync.Map
|
||||
options *types.Options
|
||||
executerOpts *protocols.ExecutorOptions
|
||||
Callback func(*output.ResultEvent) // Executed on results
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -67,7 +68,7 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), int64(totalReqAfterClustering))
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), e.expectedRequests(finalTemplates, target, totalReqAfterClustering))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
@@ -179,8 +180,33 @@ 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 {
|
||||
scope := e.executerOpts.TargetScope
|
||||
if scope == nil {
|
||||
return int64(unscoped)
|
||||
}
|
||||
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))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// returns total requests count
|
||||
func getRequestCount(templates []*templates.Template) int {
|
||||
return getSelectedRequestCount(templates, nil)
|
||||
}
|
||||
|
||||
// getSelectedRequestCount returns the requests of the templates selection
|
||||
// selects; a nil selection selects every template.
|
||||
func getSelectedRequestCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
|
||||
count := 0
|
||||
for _, template := range templates {
|
||||
// ignore requests in workflows as total requests in workflow
|
||||
@@ -188,6 +214,9 @@ func getRequestCount(templates []*templates.Template) int {
|
||||
if len(template.Workflows) > 0 {
|
||||
continue
|
||||
}
|
||||
if selection != nil && !template.SelectedBy(selection) {
|
||||
continue
|
||||
}
|
||||
count += template.TotalRequests
|
||||
}
|
||||
return count
|
||||
|
||||
+92
-5
@@ -6,6 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
@@ -154,12 +155,18 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
skip = false
|
||||
}
|
||||
|
||||
// out of scope pairs keep their index so resume positions stay stable
|
||||
inScope := e.inScope(template, scannedValue)
|
||||
if !inScope {
|
||||
skip = true
|
||||
}
|
||||
|
||||
currentInfo.Lock()
|
||||
currentInfo.InFlight[index] = struct{}{}
|
||||
currentInfo.Unlock()
|
||||
|
||||
// Skip if the host has had errors
|
||||
if e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
if inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: template.ID,
|
||||
TemplatePath: template.Path,
|
||||
@@ -210,6 +217,10 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
default:
|
||||
}
|
||||
|
||||
if !e.inScope(tpl, target) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check whether the target has already been marked as permanently
|
||||
// unresponsive by HostErrorsCache before spawning another goroutine.
|
||||
if e.executerOpts.HostErrorsCache != nil &&
|
||||
@@ -266,8 +277,27 @@ 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)
|
||||
return selection == nil || template.SelectedBy(selection)
|
||||
}
|
||||
|
||||
// executeTemplateOnInput performs template execution for a single input and returns match status and error
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (bool, error) {
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (matched bool, err error) {
|
||||
// A template fixed to the target's origin makes the same requests for every
|
||||
// target that shares one, which a crawled list produces by the hundred.
|
||||
// Running it once per origin sends exactly the same traffic, minus the
|
||||
// repeats. The reservation is kept only when execution succeeds, so a
|
||||
// failed attempt leaves the origin free for a later target.
|
||||
finish, run := e.claimOriginScoped(template, value)
|
||||
if !run {
|
||||
return false, nil
|
||||
}
|
||||
defer func() { finish(err == nil) }()
|
||||
finished := e.templateExecutionStarted(template, value.Input)
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
@@ -279,9 +309,9 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
results, err := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
results, execErr := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if execErr != nil {
|
||||
return false, execErr
|
||||
}
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
@@ -291,3 +321,60 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
return template.Executer.Execute(scanCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// originAttempt is one origin-scoped execution. done is set only after a
|
||||
// successful run. While inflight is set, other callers wait, then retry if
|
||||
// that attempt failed.
|
||||
type originAttempt struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
inflight bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newOriginAttempt() *originAttempt {
|
||||
attempt := &originAttempt{}
|
||||
attempt.cond = sync.NewCond(&attempt.mu)
|
||||
return attempt
|
||||
}
|
||||
|
||||
// claimOriginScoped reports whether this caller should execute an origin-scoped
|
||||
// template. finish(true) records a successful run. finish(false) releases the
|
||||
// claim so a later target, including one that waited, can try again. Templates
|
||||
// whose requests depend on the target path always run. CustomIP is part of the
|
||||
// key so each address selected for one URL is tested on its own.
|
||||
func (e *Engine) claimOriginScoped(template *templates.Template, value *contextargs.MetaInput) (finish func(bool), run bool) {
|
||||
noop := func(bool) {}
|
||||
if template == nil || value == nil || !template.IsOriginScoped() {
|
||||
return noop, true
|
||||
}
|
||||
origin := normalize.Origin(value.Input)
|
||||
if origin == "" {
|
||||
return noop, true
|
||||
}
|
||||
key := template.ID + "\x00" + origin + "\x00" + value.CustomIP
|
||||
actual, _ := e.originScoped.LoadOrStore(key, newOriginAttempt())
|
||||
slot := actual.(*originAttempt)
|
||||
|
||||
slot.mu.Lock()
|
||||
for {
|
||||
if slot.done {
|
||||
slot.mu.Unlock()
|
||||
return noop, false
|
||||
}
|
||||
if !slot.inflight {
|
||||
slot.inflight = true
|
||||
slot.mu.Unlock()
|
||||
return func(success bool) {
|
||||
slot.mu.Lock()
|
||||
slot.inflight = false
|
||||
if success {
|
||||
slot.done = true
|
||||
}
|
||||
slot.cond.Broadcast()
|
||||
slot.mu.Unlock()
|
||||
}, true
|
||||
}
|
||||
slot.cond.Wait()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -140,6 +142,289 @@ func TestExecuteTemplateOnInputReportsCancellationOnFinish(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type scriptedExecuter struct {
|
||||
calls int
|
||||
errs []error
|
||||
miss bool
|
||||
}
|
||||
|
||||
func (s *scriptedExecuter) Compile() error { return nil }
|
||||
func (s *scriptedExecuter) Requests() int { return 1 }
|
||||
func (s *scriptedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
s.calls++
|
||||
if len(s.errs) == 0 {
|
||||
return !s.miss, nil
|
||||
}
|
||||
err := s.errs[0]
|
||||
s.errs = s.errs[1:]
|
||||
return false, err
|
||||
}
|
||||
func (s *scriptedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
_, err := s.Execute(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func originScopedTemplate(path string) *templates.Template {
|
||||
return &templates.Template{
|
||||
ID: "root-health",
|
||||
RequestsHTTP: []*httpprotocol.Request{{
|
||||
Path: []string{path},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedRetriesAfterFailure(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
|
||||
if err == nil {
|
||||
t.Fatal("expected the first attempt to fail")
|
||||
}
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("later target should retry the origin, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedSkipsAfterSuccess(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b?q=1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedKeepsEachCustomIP(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, ip := range []string{"203.0.113.1", "203.0.113.2"} {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
|
||||
Input: "https://example.com/a",
|
||||
CustomIP: ip,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseURLTemplateIsNotOriginScoped(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{BaseURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedWaiterRetriesAfterFailedAttempt(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = &gatedExecuter{started: started, release: release, calls: &calls}
|
||||
|
||||
firstDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
|
||||
firstDone <- err
|
||||
}()
|
||||
<-started
|
||||
secondDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
|
||||
secondDone <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-secondDone:
|
||||
t.Fatalf("second target returned while the first attempt was still running: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
close(release)
|
||||
if err := <-firstDone; err == nil {
|
||||
t.Fatal("expected the first attempt to fail")
|
||||
}
|
||||
if err := <-secondDone; err != nil {
|
||||
t.Fatalf("waiter should run after the failed attempt: %v", err)
|
||||
}
|
||||
if calls.Load() != 2 {
|
||||
t.Fatalf("calls = %d, want 2", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedKeepsDistinctOriginsAndTemplates(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
first := originScopedTemplate("{{RootURL}}/health")
|
||||
first.Executer = exec
|
||||
second := originScopedTemplate("{{RootURL}}/health")
|
||||
second.ID = "other-template"
|
||||
second.Executer = exec
|
||||
|
||||
inputs := []*contextargs.MetaInput{
|
||||
{Input: "https://example.com/a"},
|
||||
{Input: "https://other.test/a"},
|
||||
{Input: "http://example.com/a"},
|
||||
{Input: "https://example.com:8443/a"},
|
||||
}
|
||||
for _, value := range inputs {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), first, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), second, inputs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 5 {
|
||||
t.Fatalf("calls = %d, want 5", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedFoldsDefaultPort(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com:443/b"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedSameCustomIPSkips(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for range 2 {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
|
||||
Input: "https://example.com/a",
|
||||
CustomIP: "203.0.113.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedMissStillCounts(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{miss: true}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw})
|
||||
if err != nil || ok {
|
||||
t.Fatalf("non-match ok=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedCallbackErrorRetries(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
e.Callback = func(*output.ResultEvent) {}
|
||||
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err == nil {
|
||||
t.Fatal("expected the callback path to fail")
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedLeavesValuesWithoutAHost(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for range 2 {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "not a url"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{"example.com", "other.test", "example.com"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 4 {
|
||||
t.Fatalf("calls = %d, want 4", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type gatedExecuter struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
calls *atomic.Int32
|
||||
}
|
||||
|
||||
func (g *gatedExecuter) Compile() error { return nil }
|
||||
func (g *gatedExecuter) Requests() int { return 1 }
|
||||
func (g *gatedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
if g.calls.Add(1) == 1 {
|
||||
close(g.started)
|
||||
<-g.release
|
||||
return false, errors.New("timeout")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
func (g *gatedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
ok, err := g.Execute(ctx)
|
||||
if err != nil || !ok {
|
||||
return nil, err
|
||||
}
|
||||
return []*output.ResultEvent{{}}, nil
|
||||
}
|
||||
|
||||
type fakeTargetProvider struct {
|
||||
values []*contextargs.MetaInput
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// allowList selects the templates it lists.
|
||||
type allowList struct{ paths []string }
|
||||
|
||||
func (a *allowList) Allows(templatePath string) bool { return slices.Contains(a.paths, templatePath) }
|
||||
|
||||
// scopeByInput maps inputs to selections; unlisted inputs are unrestricted.
|
||||
type scopeByInput map[string]*allowList
|
||||
|
||||
func (s scopeByInput) For(input *contextargs.MetaInput) protocols.TemplateSelection {
|
||||
if selection, ok := s[input.Input]; ok {
|
||||
return selection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordingExecuter records the inputs it runs on.
|
||||
type recordingExecuter struct {
|
||||
path string
|
||||
mu *sync.Mutex
|
||||
runs *[]string
|
||||
}
|
||||
|
||||
func (r *recordingExecuter) Compile() error { return nil }
|
||||
func (r *recordingExecuter) Requests() int { return 1 }
|
||||
func (r *recordingExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
*r.runs = append(*r.runs, r.path+"@"+ctx.Input.MetaInput.Input)
|
||||
return false, nil
|
||||
}
|
||||
func (r *recordingExecuter) ExecuteWithResults(*scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// progressRecorder captures the expected request count; the engine calls no
|
||||
// other progress method on this path.
|
||||
type progressRecorder struct {
|
||||
progress.Progress
|
||||
requests int64
|
||||
}
|
||||
|
||||
func (p *progressRecorder) Init(_ int64, _ int, requests int64) { p.requests = requests }
|
||||
|
||||
func TestExecuteScanRunsOnlyScopedPairs(t *testing.T) {
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
t.Run(strategy.String(), func(t *testing.T) {
|
||||
options := &types.Options{
|
||||
BulkSize: 2,
|
||||
TemplateThreads: 2,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
ScanStrategy: strategy.String(),
|
||||
}
|
||||
progress := &progressRecorder{}
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: progress,
|
||||
TargetScope: scopeByInput{
|
||||
"php": {paths: []string{"thinkphp.yaml"}},
|
||||
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
|
||||
"none": {},
|
||||
},
|
||||
})
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{
|
||||
{Input: "php"}, {Input: "tomcat"}, {Input: "none"}, {Input: "plain"},
|
||||
}}
|
||||
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targets, true)
|
||||
|
||||
require.ElementsMatch(t, []string{
|
||||
"thinkphp.yaml@php",
|
||||
"tomcat.yaml@tomcat", "generic.yaml@tomcat",
|
||||
"thinkphp.yaml@plain", "tomcat.yaml@plain", "generic.yaml@plain",
|
||||
}, runs)
|
||||
require.EqualValues(t, len(runs), progress.requests, "progress counts only scoped pairs")
|
||||
})
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -2,13 +2,12 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"context"
|
||||
httpclient "net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/google/go-github/v30/github"
|
||||
"github.com/google/go-github/v92/github"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
@@ -89,7 +88,10 @@ func (customTemplate *customTemplateGitHubRepo) logPullError(err error) {
|
||||
// NewGitHubProviders returns new instance of GitHub providers for downloading custom templates
|
||||
func NewGitHubProviders(options *types.Options) ([]*customTemplateGitHubRepo, error) {
|
||||
providers := []*customTemplateGitHubRepo{}
|
||||
gitHubClient := getGHClientIncognito()
|
||||
gitHubClient, err := github.NewClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create github client")
|
||||
}
|
||||
|
||||
if options.GitHubTemplateDisableDownload {
|
||||
return providers, nil
|
||||
@@ -150,9 +152,16 @@ getRepo:
|
||||
repo, _, err := gitHubClient.Repositories.Get(context.Background(), repoOwner, repoName)
|
||||
if err != nil {
|
||||
// retry with authentication
|
||||
if gitHubClient = getGHClientWithToken(githubToken); gitHubClient != nil && !retried {
|
||||
retried = true
|
||||
goto getRepo
|
||||
if !retried {
|
||||
authClient, authErr := getGHClientWithToken(githubToken)
|
||||
if authErr != nil {
|
||||
return nil, errors.Wrap(authErr, "could not create authenticated github client")
|
||||
}
|
||||
if authClient != nil {
|
||||
gitHubClient = authClient
|
||||
retried = true
|
||||
goto getRepo
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -233,20 +242,15 @@ func getAuth(username, password string) *http.BasicAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGHClientWithToken(token string) *github.Client {
|
||||
func getGHClientWithToken(token string) (*github.Client, error) {
|
||||
if token != "" {
|
||||
ctx := context.Background()
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
oauthClient := oauth2.NewClient(ctx, ts)
|
||||
return github.NewClient(oauthClient)
|
||||
return github.NewClient(github.WithHTTPClient(oauthClient))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGHClientIncognito() *github.Client {
|
||||
var tc *httpclient.Client
|
||||
return github.NewClient(tc)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -55,18 +55,28 @@ func (b *Body) Parse(req *retryablehttp.Request) (bool, error) {
|
||||
|
||||
b.value = NewValue(dataStr)
|
||||
tmp := b.value.Parsed()
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(contentType, "application/json") && tmp.IsNIL():
|
||||
case strings.Contains(contentType, "application/json"):
|
||||
// GraphQL-over-HTTP uses application/json; prefer the GraphQL decoder
|
||||
// so variables / inline args become fuzz points instead of raw JSON.
|
||||
if graphql := dataformat.Get(dataformat.GraphqlDataFormat); graphql != nil && graphql.IsType(dataStr) {
|
||||
return b.parseBody(dataformat.GraphqlDataFormat, req)
|
||||
}
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
return b.parseBody(dataformat.JSONDataFormat, req)
|
||||
case strings.Contains(contentType, "application/xml") && tmp.IsNIL():
|
||||
return b.parseBody(dataformat.XMLDataFormat, req)
|
||||
case strings.Contains(contentType, "multipart/form-data") && tmp.IsNIL():
|
||||
return b.parseBody(dataformat.MultiPartFormDataFormat, req)
|
||||
}
|
||||
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
parsed, err := b.parseBody(dataformat.FormDataFormat, req)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("Could not parse body as form data: %s\n", err)
|
||||
|
||||
@@ -244,3 +244,109 @@ func TestMultiPartFormConcurrentParse(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyGraphqlVariablesComponent(t *testing.T) {
|
||||
raw := `{
|
||||
"query": "mutation ImportPaste($host: String!, $path: String!) { importPaste(host: $host, path: $path) { result } }",
|
||||
"variables": {
|
||||
"host": "example.com",
|
||||
"path": "/robots.txt"
|
||||
}
|
||||
}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
keys := map[string]struct{}{}
|
||||
_ = body.Iterate(func(key string, value interface{}) error {
|
||||
keys[key] = struct{}{}
|
||||
return nil
|
||||
})
|
||||
require.Contains(t, keys, "host")
|
||||
require.Contains(t, keys, "path")
|
||||
require.NotContains(t, keys, "query")
|
||||
|
||||
require.NoError(t, body.SetValue("path", "/robots.txt; id"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), `/robots.txt; id`)
|
||||
require.Contains(t, string(newBody), `"host":"example.com"`)
|
||||
require.Contains(t, string(newBody), "mutation ImportPaste")
|
||||
}
|
||||
|
||||
func TestBodyGraphqlInlineArgsComponent(t *testing.T) {
|
||||
raw := `{"query":"query { jobs(jobType: \"front-end\") { id } }"}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("jobType", "canary"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), "canary")
|
||||
require.NotContains(t, string(newBody), "front-end")
|
||||
}
|
||||
|
||||
func TestBodyGraphqlNestedVariablesComponent(t *testing.T) {
|
||||
raw := `{
|
||||
"query": "mutation ($input: ImportInput!) { importPaste(input: $input) { result } }",
|
||||
"variables": {
|
||||
"input": {
|
||||
"host": "example.com",
|
||||
"path": "/robots.txt"
|
||||
}
|
||||
}
|
||||
}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("input~path", "/etc/passwd"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), `/etc/passwd`)
|
||||
require.Contains(t, string(newBody), `"host":"example.com"`)
|
||||
}
|
||||
|
||||
func TestBodyRegularJSONNotForcedToGraphql(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com", strings.NewReader(`{"foo":"bar"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("foo", "baz"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"foo":"baz"}`, string(newBody))
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package component
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
|
||||
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
mapsutil "github.com/projectdiscovery/utils/maps"
|
||||
)
|
||||
@@ -85,14 +86,17 @@ func (c *Cookie) Rebuild() (*retryablehttp.Request, error) {
|
||||
cloned := c.req.Clone(context.Background())
|
||||
|
||||
cloned.Header.Del("Cookie")
|
||||
var fields []string
|
||||
c.value.parsed.Iterate(func(key string, value any) bool {
|
||||
cookie := &http.Cookie{
|
||||
Name: key,
|
||||
Value: fmt.Sprint(value), // Assume the value is always a string for cookies
|
||||
}
|
||||
cloned.AddCookie(cookie)
|
||||
fields = append(fields, key+"="+fmt.Sprint(value)) // Assume the value is always a string for cookies
|
||||
return true
|
||||
})
|
||||
// Cookie.String sanitizes bytes that can be intentional fuzzing payloads.
|
||||
// Header validation still applies when the HTTP transport sends the request.
|
||||
if len(fields) > 0 {
|
||||
cloned.Header.Set("Cookie", strings.Join(fields, "; "))
|
||||
}
|
||||
httputil.SetCookieFields(cloned.Request, fields)
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,55 @@ package component
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCookieComponentPreservesRawPayload(t *testing.T) {
|
||||
for _, payload := range []string{"old'", `old"`, `old";probe=value`} {
|
||||
t.Run(payload, func(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Cookie", "session=old; preference=dark")
|
||||
cookies := NewCookie()
|
||||
parsed, err := cookies.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
require.NoError(t, cookies.SetValue("session", payload))
|
||||
rebuilt, err := cookies.Rebuild()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "session="+payload+"; preference=dark", rebuilt.Header.Get("Cookie"))
|
||||
require.Equal(t, "session=old; preference=dark", req.Header.Get("Cookie"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieComponentRejectsHeaderNewlines(t *testing.T) {
|
||||
var requests atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { requests.Add(1) }))
|
||||
defer server.Close()
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, server.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Cookie", "session=old")
|
||||
cookies := NewCookie()
|
||||
parsed, err := cookies.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
require.NoError(t, cookies.SetValue("session", "old\r\nX-Injected: yes"))
|
||||
rebuilt, err := cookies.Rebuild()
|
||||
require.NoError(t, err)
|
||||
resp, err := server.Client().Do(rebuilt.Request)
|
||||
if resp != nil {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
}
|
||||
require.ErrorContains(t, err, "invalid header field value")
|
||||
require.Equal(t, int32(0), requests.Load())
|
||||
}
|
||||
|
||||
func TestCookieComponent(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
if err != nil {
|
||||
@@ -55,3 +98,21 @@ func TestCookieComponent(t *testing.T) {
|
||||
newCookie, _ := rebuilt.Cookie("session")
|
||||
require.Equal(t, "new-session", newCookie.Value, "unexpected cookie value")
|
||||
}
|
||||
|
||||
func BenchmarkCookieComponentRebuild(b *testing.B) {
|
||||
req, err := retryablehttp.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(b, err)
|
||||
req.Header.Set("Cookie", "session=old; preference=dark")
|
||||
cookies := NewCookie()
|
||||
parsed, err := cookies.Parse(req)
|
||||
require.NoError(b, err)
|
||||
require.True(b, parsed)
|
||||
require.NoError(b, cookies.SetValue("session", "old'"))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := cookies.Rebuild(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ func init() {
|
||||
dataformats = make(map[string]DataFormat)
|
||||
|
||||
// register the default data formats
|
||||
RegisterDataFormat(NewGraphql())
|
||||
RegisterDataFormat(NewJSON())
|
||||
RegisterDataFormat(NewXML())
|
||||
RegisterDataFormat(NewRaw())
|
||||
@@ -26,6 +27,8 @@ func init() {
|
||||
}
|
||||
|
||||
const (
|
||||
// GraphqlDataFormat is the name of the GraphQL-over-HTTP JSON data format
|
||||
GraphqlDataFormat = "graphql"
|
||||
// JSONDataFormat is the name of the JSON data format
|
||||
JSONDataFormat = "json"
|
||||
// XMLDataFormat is the name of the XML data format
|
||||
@@ -38,6 +41,17 @@ const (
|
||||
MultiPartFormDataFormat = "multipart/form-data"
|
||||
)
|
||||
|
||||
// decodePriority prefers more specific formats before generic ones (GraphQL
|
||||
// bodies are valid JSON, so GraphQL must win over JSON).
|
||||
var decodePriority = []string{
|
||||
GraphqlDataFormat,
|
||||
JSONDataFormat,
|
||||
XMLDataFormat,
|
||||
MultiPartFormDataFormat,
|
||||
FormDataFormat,
|
||||
RawDataFormat,
|
||||
}
|
||||
|
||||
// Get returns the dataformat by name
|
||||
func Get(name string) DataFormat {
|
||||
return dataformats[name]
|
||||
@@ -70,22 +84,53 @@ type Decoded struct {
|
||||
|
||||
// Decode decodes the data from a format
|
||||
func Decode(data string) (*Decoded, error) {
|
||||
for _, dataformat := range dataformats {
|
||||
if dataformat.IsType(data) {
|
||||
decoded, err := dataformat.Decode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := &Decoded{
|
||||
DataFormat: dataformat.Name(),
|
||||
Data: decoded,
|
||||
}
|
||||
return value, nil
|
||||
tryDecode := func(dataformat DataFormat) (*Decoded, error) {
|
||||
if dataformat == nil || !dataformat.IsType(data) {
|
||||
return nil, nil
|
||||
}
|
||||
decoded, err := dataformat.Decode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Decoded{
|
||||
DataFormat: dataformat.Name(),
|
||||
Data: decoded,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, name := range decodePriority {
|
||||
decoded, err := tryDecode(dataformats[name])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decoded != nil {
|
||||
return decoded, nil
|
||||
}
|
||||
}
|
||||
for name, dataformat := range dataformats {
|
||||
if containsString(decodePriority, name) {
|
||||
continue
|
||||
}
|
||||
decoded, err := tryDecode(dataformat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decoded != nil {
|
||||
return decoded, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func containsString(items []string, want string) bool {
|
||||
for _, item := range items {
|
||||
if item == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Encode encodes the data into a format
|
||||
func Encode(data KV, dataformat string) (string, error) {
|
||||
if dataformat == "" {
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
package dataformat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/graphql-go/graphql/language/ast"
|
||||
"github.com/graphql-go/graphql/language/kinds"
|
||||
"github.com/graphql-go/graphql/language/lexer"
|
||||
"github.com/graphql-go/graphql/language/parser"
|
||||
"github.com/graphql-go/graphql/language/printer"
|
||||
"github.com/graphql-go/graphql/language/source"
|
||||
"github.com/graphql-go/graphql/language/visitor"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
const (
|
||||
graphqlMetaQuery = "#_query"
|
||||
graphqlMetaOperationName = "#_operationName"
|
||||
graphqlMetaHasVariables = "#_hasVariables"
|
||||
graphqlMetaExtensions = "#_extensions"
|
||||
)
|
||||
|
||||
// Graphql encodes and decodes GraphQL-over-HTTP JSON bodies
|
||||
// ({"query","variables","operationName"}) so DAST fuzzing can target
|
||||
// variables and inline field arguments instead of the raw query string.
|
||||
type Graphql struct{}
|
||||
|
||||
var _ DataFormat = &Graphql{}
|
||||
|
||||
// NewGraphql returns a new GraphQL data format encoder/decoder.
|
||||
func NewGraphql() *Graphql {
|
||||
return &Graphql{}
|
||||
}
|
||||
|
||||
// Name returns the name of the data format.
|
||||
func (g *Graphql) Name() string {
|
||||
return GraphqlDataFormat
|
||||
}
|
||||
|
||||
// IsType reports whether data is a GraphQL HTTP request body.
|
||||
func (g *Graphql) IsType(data string) bool {
|
||||
_, ok := parseGraphQLHTTPBody(data, true)
|
||||
return ok
|
||||
}
|
||||
|
||||
type graphQLHTTPBody struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
OperationName string `json:"operationName,omitempty"`
|
||||
Variables *map[string]any `json:"variables,omitempty"`
|
||||
Extensions json.Message `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
func parseGraphQLHTTPBody(data string, validateQuery bool) (graphQLHTTPBody, bool) {
|
||||
trimmed := strings.TrimSpace(data)
|
||||
if !strings.HasPrefix(trimmed, "{") || !strings.HasSuffix(trimmed, "}") {
|
||||
return graphQLHTTPBody{}, false
|
||||
}
|
||||
|
||||
var body graphQLHTTPBody
|
||||
if err := json.Unmarshal([]byte(trimmed), &body); err != nil {
|
||||
return graphQLHTTPBody{}, false
|
||||
}
|
||||
if strings.TrimSpace(body.Query) == "" {
|
||||
return graphQLHTTPBody{}, false
|
||||
}
|
||||
if !validateQuery {
|
||||
return body, true
|
||||
}
|
||||
if _, err := parseQueryAST(body.Query); err != nil {
|
||||
return graphQLHTTPBody{}, false
|
||||
}
|
||||
return body, true
|
||||
}
|
||||
|
||||
func parseQueryAST(query string) (*ast.Document, error) {
|
||||
doc, err := parser.Parse(parser.ParseParams{
|
||||
Source: &source.Source{Body: []byte(query)},
|
||||
})
|
||||
if err == nil {
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// graphql-go v0.8.1 predates GraphQL null literal support. Replace null
|
||||
// name tokens with an enum sentinel for parsing, then restore the AST.
|
||||
rewritten, sentinel, replaced, rewriteErr := rewriteGraphQLNullTokens(query)
|
||||
if rewriteErr != nil || !replaced {
|
||||
return nil, err
|
||||
}
|
||||
doc, rewriteErr = parser.Parse(parser.ParseParams{
|
||||
Source: &source.Source{Body: []byte(rewritten)},
|
||||
})
|
||||
if rewriteErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
visitor.Visit(doc, &visitor.VisitorOptions{
|
||||
Enter: func(params visitor.VisitFuncParams) (string, interface{}) {
|
||||
switch node := params.Node.(type) {
|
||||
case *ast.Name:
|
||||
if node.Value == sentinel {
|
||||
node.Value = "null"
|
||||
}
|
||||
case *ast.EnumValue:
|
||||
if node.Value == sentinel {
|
||||
node.Value = "null"
|
||||
}
|
||||
}
|
||||
return visitor.ActionNoChange, nil
|
||||
},
|
||||
}, nil)
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func rewriteGraphQLNullTokens(query string) (string, string, bool, error) {
|
||||
sentinel := "__NUCLEI_GRAPHQL_NULL__"
|
||||
for strings.Contains(query, sentinel) {
|
||||
sentinel += "_"
|
||||
}
|
||||
|
||||
src := &source.Source{Body: []byte(query)}
|
||||
next := lexer.Lex(src)
|
||||
var out strings.Builder
|
||||
last := 0
|
||||
replaced := false
|
||||
for {
|
||||
token, err := next(0)
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
if token.Kind == lexer.EOF {
|
||||
break
|
||||
}
|
||||
if token.Kind != lexer.NAME || token.Value != "null" {
|
||||
continue
|
||||
}
|
||||
out.WriteString(query[last:token.Start])
|
||||
out.WriteString(sentinel)
|
||||
last = token.End
|
||||
replaced = true
|
||||
}
|
||||
if !replaced {
|
||||
return query, sentinel, false, nil
|
||||
}
|
||||
out.WriteString(query[last:])
|
||||
return out.String(), sentinel, true, nil
|
||||
}
|
||||
|
||||
// Decode extracts fuzzable GraphQL variables / inline arguments.
|
||||
func (g *Graphql) Decode(data string) (KV, error) {
|
||||
body, ok := parseGraphQLHTTPBody(data, true)
|
||||
if !ok {
|
||||
return KV{}, fmt.Errorf("not a graphql http body")
|
||||
}
|
||||
doc, err := parseQueryAST(body.Query)
|
||||
if err != nil {
|
||||
return KV{}, fmt.Errorf("could not parse graphql query: %w", err)
|
||||
}
|
||||
|
||||
kv := KVMap(map[string]any{})
|
||||
kv.Set(graphqlMetaQuery, body.Query)
|
||||
if body.OperationName != "" {
|
||||
kv.Set(graphqlMetaOperationName, body.OperationName)
|
||||
}
|
||||
if body.Extensions != nil {
|
||||
kv.Set(graphqlMetaExtensions, body.Extensions)
|
||||
}
|
||||
|
||||
hasVariables := body.Variables != nil
|
||||
if hasVariables {
|
||||
kv.Set(graphqlMetaHasVariables, true)
|
||||
for key, value := range *body.Variables {
|
||||
kv.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Inline arguments are only exposed when variables are absent; otherwise
|
||||
// variables are the stable fuzz surface (and rewrites stay in JSON).
|
||||
if !hasVariables {
|
||||
for key, value := range collectInlineArguments(doc) {
|
||||
if kv.Get(key) != nil {
|
||||
continue
|
||||
}
|
||||
kv.Set(key, value)
|
||||
}
|
||||
}
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// Encode rebuilds a GraphQL HTTP JSON body from fuzzed KV values.
|
||||
func (g *Graphql) Encode(data KV) (string, error) {
|
||||
queryVal := data.Get(graphqlMetaQuery)
|
||||
if queryVal == nil {
|
||||
return "", fmt.Errorf("graphql query metadata missing")
|
||||
}
|
||||
query := types.ToString(queryVal)
|
||||
|
||||
body := graphQLHTTPBody{Query: query}
|
||||
if op := data.Get(graphqlMetaOperationName); op != nil {
|
||||
body.OperationName = types.ToString(op)
|
||||
}
|
||||
if extensions, ok := data.Get(graphqlMetaExtensions).(json.Message); ok {
|
||||
body.Extensions = extensions
|
||||
}
|
||||
|
||||
hasVariables, _ := data.Get(graphqlMetaHasVariables).(bool)
|
||||
if hasVariables {
|
||||
variables := make(map[string]any)
|
||||
body.Variables = &variables
|
||||
data.Iterate(func(key string, value any) bool {
|
||||
if strings.HasPrefix(key, "#_") {
|
||||
return true
|
||||
}
|
||||
variables[key] = value
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
doc, err := parseQueryAST(query)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse graphql query: %w", err)
|
||||
}
|
||||
data.Iterate(func(key string, value any) bool {
|
||||
if strings.HasPrefix(key, "#_") {
|
||||
return true
|
||||
}
|
||||
applyInlineArgument(doc, key, value)
|
||||
return true
|
||||
})
|
||||
body.Query = types.ToString(printer.Print(doc))
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(encoded), nil
|
||||
}
|
||||
|
||||
func collectInlineArguments(doc *ast.Document) map[string]any {
|
||||
args := make(map[string]any)
|
||||
for _, ref := range inlineArgumentRefs(doc) {
|
||||
if containsVariable(ref.argument.Value) {
|
||||
continue
|
||||
}
|
||||
args[ref.key] = astValueToGo(ref.argument.Value)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func applyInlineArgument(doc *ast.Document, key string, value any) {
|
||||
for _, ref := range inlineArgumentRefs(doc) {
|
||||
if ref.key == key && !containsVariable(ref.argument.Value) {
|
||||
ref.argument.Value = goValueToAST(value, ref.argument.Value)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containsVariable(value ast.Value) bool {
|
||||
switch typed := value.(type) {
|
||||
case *ast.Variable:
|
||||
return true
|
||||
case *ast.ListValue:
|
||||
for _, item := range typed.Values {
|
||||
if containsVariable(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case *ast.ObjectValue:
|
||||
for _, field := range typed.Fields {
|
||||
if containsVariable(field.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type inlineArgumentRef struct {
|
||||
key string
|
||||
path string
|
||||
argument *ast.Argument
|
||||
}
|
||||
|
||||
func inlineArgumentRefs(doc *ast.Document) []inlineArgumentRef {
|
||||
var refs []inlineArgumentRef
|
||||
walkFields(doc, func(path string, field *ast.Field) {
|
||||
for _, argument := range field.Arguments {
|
||||
if argument.Name == nil {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, inlineArgumentRef{
|
||||
path: path,
|
||||
argument: argument,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
counts := make(map[string]int)
|
||||
for _, ref := range refs {
|
||||
counts[ref.argument.Name.Value]++
|
||||
}
|
||||
for index := range refs {
|
||||
name := refs[index].argument.Name.Value
|
||||
refs[index].key = name
|
||||
if counts[name] > 1 {
|
||||
refs[index].key = refs[index].path + "." + name
|
||||
}
|
||||
}
|
||||
|
||||
keyCounts := make(map[string]int)
|
||||
for _, ref := range refs {
|
||||
keyCounts[ref.key]++
|
||||
}
|
||||
keyIndexes := make(map[string]int)
|
||||
for index := range refs {
|
||||
key := refs[index].key
|
||||
keyIndexes[key]++
|
||||
if keyCounts[key] > 1 {
|
||||
refs[index].key = fmt.Sprintf("%s[%d]", key, keyIndexes[key])
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func walkFields(doc *ast.Document, fn func(string, *ast.Field)) {
|
||||
if doc == nil {
|
||||
return
|
||||
}
|
||||
for _, def := range doc.Definitions {
|
||||
switch typed := def.(type) {
|
||||
case *ast.OperationDefinition:
|
||||
prefix := ""
|
||||
if typed.Name != nil {
|
||||
prefix = typed.Name.Value
|
||||
}
|
||||
walkSelectionSet(typed.SelectionSet, prefix, fn)
|
||||
case *ast.FragmentDefinition:
|
||||
prefix := "fragment"
|
||||
if typed.Name != nil {
|
||||
prefix += "." + typed.Name.Value
|
||||
}
|
||||
walkSelectionSet(typed.SelectionSet, prefix, fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func walkSelectionSet(set *ast.SelectionSet, parentPath string, fn func(string, *ast.Field)) {
|
||||
if set == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fieldCounts := make(map[string]int)
|
||||
for _, selection := range set.Selections {
|
||||
if field, ok := selection.(*ast.Field); ok {
|
||||
fieldCounts[fieldResponseName(field)]++
|
||||
}
|
||||
}
|
||||
fieldIndexes := make(map[string]int)
|
||||
|
||||
for _, selection := range set.Selections {
|
||||
switch typed := selection.(type) {
|
||||
case *ast.Field:
|
||||
name := fieldResponseName(typed)
|
||||
fieldIndexes[name]++
|
||||
if fieldCounts[name] > 1 {
|
||||
name = fmt.Sprintf("%s[%d]", name, fieldIndexes[name])
|
||||
}
|
||||
path := name
|
||||
if parentPath != "" {
|
||||
path = parentPath + "." + name
|
||||
}
|
||||
fn(path, typed)
|
||||
walkSelectionSet(typed.SelectionSet, path, fn)
|
||||
case *ast.InlineFragment:
|
||||
walkSelectionSet(typed.SelectionSet, parentPath, fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fieldResponseName(field *ast.Field) string {
|
||||
if field.Alias != nil {
|
||||
return field.Alias.Value
|
||||
}
|
||||
if field.Name != nil {
|
||||
return field.Name.Value
|
||||
}
|
||||
return "field"
|
||||
}
|
||||
|
||||
func goValueToAST(value any, original ast.Value) ast.Value {
|
||||
if enum, ok := original.(*ast.EnumValue); ok && enum.Value == "null" && value == nil {
|
||||
return &ast.EnumValue{Kind: kinds.EnumValue, Value: "null"}
|
||||
}
|
||||
|
||||
switch original.(type) {
|
||||
case *ast.IntValue:
|
||||
return &ast.IntValue{Kind: kinds.IntValue, Value: types.ToString(value)}
|
||||
case *ast.FloatValue:
|
||||
return &ast.FloatValue{Kind: kinds.FloatValue, Value: types.ToString(value)}
|
||||
case *ast.EnumValue:
|
||||
return &ast.EnumValue{Kind: kinds.EnumValue, Value: types.ToString(value)}
|
||||
case *ast.StringValue:
|
||||
return &ast.StringValue{Kind: kinds.StringValue, Value: types.ToString(value)}
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return &ast.StringValue{Kind: kinds.StringValue, Value: v}
|
||||
case bool:
|
||||
return &ast.BooleanValue{Kind: kinds.BooleanValue, Value: v}
|
||||
case int:
|
||||
return &ast.IntValue{Kind: kinds.IntValue, Value: fmt.Sprintf("%d", v)}
|
||||
case int32:
|
||||
return &ast.IntValue{Kind: kinds.IntValue, Value: fmt.Sprintf("%d", v)}
|
||||
case int64:
|
||||
return &ast.IntValue{Kind: kinds.IntValue, Value: fmt.Sprintf("%d", v)}
|
||||
case float32:
|
||||
return &ast.FloatValue{Kind: kinds.FloatValue, Value: fmt.Sprintf("%v", v)}
|
||||
case float64:
|
||||
// JSON numbers decode as float64; keep integers looking like ints when possible.
|
||||
if v == float64(int64(v)) {
|
||||
return &ast.IntValue{Kind: kinds.IntValue, Value: fmt.Sprintf("%d", int64(v))}
|
||||
}
|
||||
return &ast.FloatValue{Kind: kinds.FloatValue, Value: fmt.Sprintf("%v", v)}
|
||||
case []any:
|
||||
originalList, _ := original.(*ast.ListValue)
|
||||
values := make([]ast.Value, 0, len(v))
|
||||
for index, item := range v {
|
||||
var originalItem ast.Value
|
||||
if originalList != nil && index < len(originalList.Values) {
|
||||
originalItem = originalList.Values[index]
|
||||
}
|
||||
values = append(values, goValueToAST(item, originalItem))
|
||||
}
|
||||
return &ast.ListValue{Kind: kinds.ListValue, Values: values}
|
||||
case map[string]any:
|
||||
originalObject, _ := original.(*ast.ObjectValue)
|
||||
fields := make([]*ast.ObjectField, 0, len(v))
|
||||
used := make(map[string]struct{}, len(v))
|
||||
if originalObject != nil {
|
||||
for _, field := range originalObject.Fields {
|
||||
if field.Name == nil {
|
||||
continue
|
||||
}
|
||||
name := field.Name.Value
|
||||
value, ok := v[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, &ast.ObjectField{
|
||||
Kind: kinds.ObjectField,
|
||||
Name: &ast.Name{Kind: kinds.Name, Value: name},
|
||||
Value: goValueToAST(value, field.Value),
|
||||
})
|
||||
used[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(v)-len(used))
|
||||
for name := range v {
|
||||
if _, ok := used[name]; !ok {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
fields = append(fields, &ast.ObjectField{
|
||||
Kind: kinds.ObjectField,
|
||||
Name: &ast.Name{Kind: kinds.Name, Value: name},
|
||||
Value: goValueToAST(v[name], nil),
|
||||
})
|
||||
}
|
||||
return &ast.ObjectValue{Kind: kinds.ObjectValue, Fields: fields}
|
||||
default:
|
||||
return &ast.StringValue{Kind: kinds.StringValue, Value: types.ToString(v)}
|
||||
}
|
||||
}
|
||||
|
||||
func astValueToGo(value ast.Value) any {
|
||||
switch v := value.(type) {
|
||||
case *ast.StringValue:
|
||||
return v.Value
|
||||
case *ast.IntValue:
|
||||
return v.Value
|
||||
case *ast.FloatValue:
|
||||
return v.Value
|
||||
case *ast.BooleanValue:
|
||||
return v.Value
|
||||
case *ast.EnumValue:
|
||||
if v.Value == "null" {
|
||||
return nil
|
||||
}
|
||||
return v.Value
|
||||
case *ast.ListValue:
|
||||
out := make([]any, 0, len(v.Values))
|
||||
for _, item := range v.Values {
|
||||
out = append(out, astValueToGo(item))
|
||||
}
|
||||
return out
|
||||
case *ast.ObjectValue:
|
||||
out := make(map[string]any, len(v.Fields))
|
||||
for _, field := range v.Fields {
|
||||
if field.Name == nil {
|
||||
continue
|
||||
}
|
||||
out[field.Name.Value] = astValueToGo(field.Value)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package dataformat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
graphqlInlineArgsBody = `{
|
||||
"query": "query { jobs(jobType: \"front-end\") { id name } }"
|
||||
}`
|
||||
|
||||
graphqlVariablesBody = `{
|
||||
"query": "mutation ImportPaste($host: String!, $port: Int!, $path: String!, $scheme: String!) { importPaste(host: $host, port: $port, path: $path, scheme: $scheme) { result } }",
|
||||
"variables": {
|
||||
"host": "example.com",
|
||||
"port": 80,
|
||||
"path": "/robots.txt",
|
||||
"scheme": "http"
|
||||
}
|
||||
}`
|
||||
|
||||
graphqlNestedVariablesBody = `{
|
||||
"query": "mutation ($input: ImportInput!) { importPaste(input: $input) { result } }",
|
||||
"operationName": "Import",
|
||||
"variables": {
|
||||
"input": {
|
||||
"host": "example.com",
|
||||
"path": "/robots.txt"
|
||||
}
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
func fuzzableKV(t *testing.T, decoded KV) map[string]any {
|
||||
t.Helper()
|
||||
out := make(map[string]any)
|
||||
decoded.Iterate(func(key string, value any) bool {
|
||||
if strings.HasPrefix(key, "#_") {
|
||||
return true
|
||||
}
|
||||
out[key] = value
|
||||
return true
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func TestGraphqlIsType(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
|
||||
require.True(t, g.IsType(graphqlInlineArgsBody))
|
||||
require.True(t, g.IsType(graphqlVariablesBody))
|
||||
require.False(t, g.IsType("not-json"))
|
||||
require.False(t, g.IsType(`{"foo":"bar"}`))
|
||||
require.False(t, g.IsType(`{"query":"not a graphql query"}`))
|
||||
require.False(t, g.IsType(`{"variables":{"host":"example.com"}}`))
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodeVariables(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
|
||||
decoded, err := g.Decode(graphqlVariablesBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{
|
||||
"host": "example.com",
|
||||
"port": float64(80),
|
||||
"path": "/robots.txt",
|
||||
"scheme": "http",
|
||||
}, fuzzableKV(t, decoded))
|
||||
|
||||
decoded.Set("path", "/robots.txt; cat /etc/passwd")
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/robots.txt; cat /etc/passwd", fuzzableKV(t, roundTrip)["path"])
|
||||
require.Contains(t, encoded, `"variables"`)
|
||||
require.Contains(t, encoded, `/robots.txt; cat /etc/passwd`)
|
||||
// Injection stays in variables JSON, query text still references $path only.
|
||||
require.Contains(t, encoded, `path: $path`)
|
||||
require.NotContains(t, encoded, `path: "/robots.txt; cat /etc/passwd"`)
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodeEmptyVariables(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{
|
||||
"query": "query Lookup($input: String) { lookup(input: $input) { id } }",
|
||||
"variables": {}
|
||||
}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, fuzzableKV(t, decoded))
|
||||
require.Equal(t, true, decoded.Get(graphqlMetaHasVariables))
|
||||
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, `"variables":{}`)
|
||||
require.Contains(t, encoded, `input: $input`)
|
||||
require.NotContains(t, encoded, `input: ""`)
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, true, roundTrip.Get(graphqlMetaHasVariables))
|
||||
require.Empty(t, fuzzableKV(t, roundTrip))
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodeInlineArgs(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
|
||||
decoded, err := g.Decode(graphqlInlineArgsBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{"jobType": "front-end"}, fuzzableKV(t, decoded))
|
||||
|
||||
decoded.Set("jobType", "canary-payload")
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{"jobType": "canary-payload"}, fuzzableKV(t, roundTrip))
|
||||
require.Contains(t, encoded, "canary-payload")
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodeNullLiteral(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{"query":"query { jobs(filter: null, nested: {value: null}) { id } }"}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, fuzzableKV(t, decoded), "filter")
|
||||
require.Nil(t, decoded.Get("filter"))
|
||||
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, "filter: null")
|
||||
require.Contains(t, encoded, "nested: {value: null}")
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, fuzzableKV(t, roundTrip), "filter")
|
||||
require.Nil(t, roundTrip.Get("filter"))
|
||||
}
|
||||
|
||||
func TestGraphqlInlineArgsPreserveDefaultedVariable(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{"query":"query Jobs($limit: Int = 10) { jobs(limit: $limit, status: ACTIVE) { id } }"}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{"status": "ACTIVE"}, fuzzableKV(t, decoded))
|
||||
|
||||
decoded.Set("status", "PAUSED")
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, "$limit: Int = 10")
|
||||
require.Contains(t, encoded, "limit: $limit")
|
||||
require.Contains(t, encoded, "status: PAUSED")
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{"status": "PAUSED"}, fuzzableKV(t, roundTrip))
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodePreservesExtensions(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{"query":"query { viewer { id } }","extensions":{"persistedQuery":{"version":1,"sha256Hash":"abc123"}}}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
var roundTrip map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(encoded), &roundTrip))
|
||||
require.Equal(t, map[string]any{
|
||||
"persistedQuery": map[string]any{
|
||||
"version": float64(1),
|
||||
"sha256Hash": "abc123",
|
||||
},
|
||||
}, roundTrip["extensions"])
|
||||
}
|
||||
|
||||
func TestGraphqlInlineArgumentKeysIdentifyLocations(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{"query":"query { users(first: 10) { id } posts(first: 20) { id } }"}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]any{
|
||||
"users.first": "10",
|
||||
"posts.first": "20",
|
||||
}, fuzzableKV(t, decoded))
|
||||
|
||||
decoded.Set("users.first", "99")
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, "users(first: 99)")
|
||||
require.Contains(t, encoded, "posts(first: 20)")
|
||||
|
||||
roundTrip, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "99", roundTrip.Get("users.first"))
|
||||
require.Equal(t, "20", roundTrip.Get("posts.first"))
|
||||
}
|
||||
|
||||
func TestGraphqlInlineArgumentsPreserveLiteralTypes(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
body := `{"query":"query { jobs(name: \"example\", status: ACTIVE, limit: 10, ratio: 1.5, flags: [ACTIVE, PAUSED], filter: {retries: 3, state: READY}) { id } }"}`
|
||||
|
||||
decoded, err := g.Decode(body)
|
||||
require.NoError(t, err)
|
||||
decoded.Set("name", "canary")
|
||||
|
||||
flags, ok := decoded.Get("flags").([]any)
|
||||
require.True(t, ok)
|
||||
flags[0] = "DISABLED"
|
||||
decoded.Set("flags", flags)
|
||||
|
||||
filter, ok := decoded.Get("filter").(map[string]any)
|
||||
require.True(t, ok)
|
||||
filter["retries"] = "4"
|
||||
decoded.Set("filter", filter)
|
||||
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, `name: \"canary\"`)
|
||||
require.Contains(t, encoded, "status: ACTIVE")
|
||||
require.Contains(t, encoded, "limit: 10")
|
||||
require.Contains(t, encoded, "ratio: 1.5")
|
||||
require.Contains(t, encoded, "flags: [DISABLED, PAUSED]")
|
||||
require.Contains(t, encoded, "filter: {retries: 4, state: READY}")
|
||||
require.NotContains(t, encoded, `status: \"ACTIVE\"`)
|
||||
require.NotContains(t, encoded, `limit: \"10\"`)
|
||||
}
|
||||
|
||||
func TestGraphqlDecodeEncodeNestedVariables(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
|
||||
decoded, err := g.Decode(graphqlNestedVariablesBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Import", decoded.Get(graphqlMetaOperationName))
|
||||
|
||||
input, ok := fuzzableKV(t, decoded)["input"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "example.com", input["host"])
|
||||
require.Equal(t, "/robots.txt", input["path"])
|
||||
|
||||
input["path"] = "/robots.txt' OR 1=1--"
|
||||
decoded.Set("input", input)
|
||||
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, encoded, `/robots.txt' OR 1=1--`)
|
||||
require.Contains(t, encoded, `"operationName":"Import"`)
|
||||
}
|
||||
|
||||
func TestGraphqlDoesNotTreatRegularJSONAsGraphql(t *testing.T) {
|
||||
decoded, err := Decode(`{"foo":"bar","query":123}`)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decoded)
|
||||
require.Equal(t, JSONDataFormat, decoded.DataFormat)
|
||||
}
|
||||
|
||||
func TestGraphqlPreferredOverJSONInDecode(t *testing.T) {
|
||||
decoded, err := Decode(graphqlVariablesBody)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decoded)
|
||||
require.Equal(t, GraphqlDataFormat, decoded.DataFormat)
|
||||
require.Equal(t, "example.com", decoded.Data.Get("host"))
|
||||
}
|
||||
|
||||
func TestGraphqlEncodeRoundTripPreservesQueryWhenFuzzingVariables(t *testing.T) {
|
||||
g := NewGraphql()
|
||||
decoded, err := g.Decode(graphqlVariablesBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalQuery := decoded.Get(graphqlMetaQuery)
|
||||
decoded.Set("host", "evil.example")
|
||||
encoded, err := g.Encode(decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
again, err := g.Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalQuery, again.Get(graphqlMetaQuery))
|
||||
require.Equal(t, "evil.example", again.Get("host"))
|
||||
}
|
||||
+9
-3
@@ -76,6 +76,8 @@ type GeneratedRequest struct {
|
||||
|
||||
// Key is the key for the request
|
||||
Key string
|
||||
// FuzzedKeys contains the keys under test, including multiple mode.
|
||||
FuzzedKeys []string
|
||||
// Value is the value for the request
|
||||
Value string
|
||||
// OriginalValue is the original value for the request
|
||||
@@ -309,8 +311,9 @@ func (rule *Rule) evaluateVars(input string) (string, error) {
|
||||
}
|
||||
|
||||
result, err := render.Render(render.Input{
|
||||
Text: input,
|
||||
Values: data,
|
||||
Options: rule.options.GetOptions(),
|
||||
Text: input,
|
||||
Values: data,
|
||||
})
|
||||
if err != nil {
|
||||
return input, err
|
||||
@@ -339,6 +342,7 @@ func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, intera
|
||||
}
|
||||
|
||||
got, err := render.Render(render.Input{
|
||||
Options: rule.options.GetOptions(),
|
||||
Text: fmt.Sprint(v),
|
||||
Values: data,
|
||||
Interactsh: rule.options.Interactsh,
|
||||
@@ -412,6 +416,7 @@ func (rule *Rule) executeRuleValues(input *ExecuteRuleInput, ruleComponent compo
|
||||
|
||||
// if mode is multiple now build and execute it
|
||||
if rule.modeType == multipleModeType {
|
||||
var fuzzedKeys []string
|
||||
rule.Fuzz.KV.Iterate(func(key, value string) bool {
|
||||
var evaluated string
|
||||
var err error
|
||||
@@ -426,6 +431,7 @@ func (rule *Rule) executeRuleValues(input *ExecuteRuleInput, ruleComponent compo
|
||||
if err := ruleComponent.SetValue(key, evaluated); err != nil {
|
||||
return true
|
||||
}
|
||||
fuzzedKeys = append(fuzzedKeys, key)
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -439,7 +445,7 @@ func (rule *Rule) executeRuleValues(input *ExecuteRuleInput, ruleComponent compo
|
||||
return err
|
||||
}
|
||||
|
||||
if gotErr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", "", "", "", "", ""); gotErr != nil {
|
||||
if gotErr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", "", "", "", "", "", fuzzedKeys...); gotErr != nil {
|
||||
return gotErr
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -50,6 +50,7 @@ func (rule *Rule) executePartComponent(input *ExecuteRuleInput, payload ValueOrK
|
||||
// this supports both single and multiple [ruleType] modes
|
||||
// i.e if component has multiple values, they can be replaced once or all depending on mode
|
||||
func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadStr, originalPayload string, ruleComponent component.Component) error {
|
||||
var fuzzedKeys []string
|
||||
finalErr := ruleComponent.Iterate(func(key string, value interface{}) error {
|
||||
valueStr := types.ToString(value)
|
||||
if !rule.matchKeyOrValue(key, valueStr) {
|
||||
@@ -77,6 +78,9 @@ func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadS
|
||||
// gologger.Warning().Msgf("could not set value due to format restriction original(%s, %s[%T]) , new(%s,%s[%T])", key, valueStr, value, key, evaluated, evaluated)
|
||||
return nil
|
||||
}
|
||||
if rule.modeType == multipleModeType {
|
||||
fuzzedKeys = append(fuzzedKeys, key)
|
||||
}
|
||||
|
||||
if rule.modeType == singleModeType {
|
||||
req, err := ruleComponent.Rebuild()
|
||||
@@ -109,7 +113,7 @@ func (rule *Rule) executePartComponentOnValues(input *ExecuteRuleInput, payloadS
|
||||
return err
|
||||
}
|
||||
|
||||
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", "", "", "", "", ""); qerr != nil {
|
||||
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, "", "", "", "", "", "", fuzzedKeys...); qerr != nil {
|
||||
err = qerr
|
||||
return err
|
||||
}
|
||||
@@ -149,7 +153,7 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
|
||||
return err
|
||||
}
|
||||
|
||||
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key, value, "", "", "", ""); qerr != nil {
|
||||
if qerr := rule.execWithInput(input, req, input.InteractURLs, ruleComponent, key, value, "", "", "", "", key); qerr != nil {
|
||||
return qerr
|
||||
}
|
||||
|
||||
@@ -168,7 +172,7 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
|
||||
}
|
||||
|
||||
// execWithInput executes a rule with input via callback
|
||||
func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.Request, interactURLs []string, component component.Component, parameter, parameterValue, originalPayload, originalValue, key, value string) error {
|
||||
func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.Request, interactURLs []string, component component.Component, parameter, parameterValue, originalPayload, originalValue, key, value string, fuzzedKeys ...string) error {
|
||||
// If the parameter is a number, replace it with the parameter value
|
||||
// or if the parameter is empty and the parameter value is not empty
|
||||
// replace it with the parameter value
|
||||
@@ -188,6 +192,9 @@ func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(fuzzedKeys) == 0 && key != "" {
|
||||
fuzzedKeys = []string{key}
|
||||
}
|
||||
request := GeneratedRequest{
|
||||
Request: httpReq,
|
||||
InteractURLs: interactURLs,
|
||||
@@ -195,6 +202,7 @@ func (rule *Rule) execWithInput(input *ExecuteRuleInput, httpReq *retryablehttp.
|
||||
Component: component,
|
||||
Parameter: actualParameter,
|
||||
Key: key,
|
||||
FuzzedKeys: append([]string(nil), fuzzedKeys...),
|
||||
Value: value,
|
||||
OriginalValue: originalValue,
|
||||
OriginalPayload: originalPayload,
|
||||
@@ -214,6 +222,7 @@ func (rule *Rule) executeEvaluate(input *ExecuteRuleInput, _, value, payload str
|
||||
}, rule.options.Options.Vars.AsMap(), input.Values)
|
||||
|
||||
result, err := render.Render(render.Input{
|
||||
Options: rule.options.GetOptions(),
|
||||
Text: payload,
|
||||
Values: values,
|
||||
Interactsh: rule.options.Interactsh,
|
||||
|
||||
@@ -28,6 +28,32 @@ func NewInputProvider(opts InputOptions) (InputProvider, error)
|
||||
This function returns a InputProvider based by appropriately selecting input provider based on the input format (i.e. either list or http) and returns the provider that can handle that input format.
|
||||
|
||||
|
||||
## Per-target profiles
|
||||
|
||||
A list input line starting with `{` is a target line: a JSON object binding one target to its own template selection, so a single scan runs a different profile on each target. The keys mirror template profiles and CLI flags:
|
||||
|
||||
```jsonl
|
||||
{"target": "https://a.example", "tags": ["thinkphp"]}
|
||||
{"target": "https://b.example", "profile": "wordpress", "severity": ["critical", "high"]}
|
||||
{"target": "https://c.example", "tech": ["Apache Tomcat", "Java"]}
|
||||
{"target": "10.0.0.0/24", "type": "http", "exclude-tags": "dos"}
|
||||
https://d.example
|
||||
```
|
||||
|
||||
| Key | Selects |
|
||||
| --- | --- |
|
||||
| `target` | the target (URL, host, IP, CIDR or ASN), required |
|
||||
| `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 |
|
||||
| `tech` | technologies the target runs: templates bound to another product (`metadata.product`) are skipped, every other template runs |
|
||||
|
||||
- Global flags apply first; a target line only narrows them. Plain targets run every template.
|
||||
- A target listed more than once runs the union of its selections, and is scanned once.
|
||||
- When every target has a selection, only the templates some target selects are loaded.
|
||||
- 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.
|
||||
|
||||
## 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`:
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
func TestOpenAPIDownloader_SupportedExtensions(t *testing.T) {
|
||||
@@ -181,7 +183,9 @@ func TestOpenAPIDownloader_Download_InvalidJSON(t *testing.T) {
|
||||
func TestOpenAPIDownloader_Download_Timeout(t *testing.T) {
|
||||
// Create mock server with delay
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(35 * time.Second) // Longer than 30 second timeout
|
||||
// Outlasts the client timeout below. Waiting out the real 30s default
|
||||
// cost this test 35s, which was a seventh of the whole unit suite.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"test": "data"}); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
@@ -200,9 +204,17 @@ func TestOpenAPIDownloader_Download_Timeout(t *testing.T) {
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
client := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
client.HTTPClient.Timeout = 100 * time.Millisecond
|
||||
_, err = downloader.Download(server.URL+"/openapi.json", tmpDir, client)
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error, but got none")
|
||||
t.Fatal("Expected timeout error, but got none")
|
||||
}
|
||||
// Assert it timed out rather than merely erroring: the spec this server
|
||||
// returns is invalid, so a test that only checks for any error passes even
|
||||
// when the timeout never fires.
|
||||
if !os.IsTimeout(err) && !strings.Contains(err.Error(), "Timeout") {
|
||||
t.Fatalf("Expected a timeout error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -268,7 +270,9 @@ func TestSwaggerDownloader_Download_InvalidYAML(t *testing.T) {
|
||||
func TestSwaggerDownloader_Download_Timeout(t *testing.T) {
|
||||
// Create mock server with delay
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(35 * time.Second) // Longer than 30 second timeout
|
||||
// Outlasts the client timeout below. Waiting out the real 30s default
|
||||
// cost this test 35s, which was a seventh of the whole unit suite.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"test": "data"}); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
@@ -287,9 +291,17 @@ func TestSwaggerDownloader_Download_Timeout(t *testing.T) {
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
client := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
client.HTTPClient.Timeout = 100 * time.Millisecond
|
||||
_, err = downloader.Download(server.URL+"/swagger.json", tmpDir, client)
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error, but got none")
|
||||
t.Fatal("Expected timeout error, but got none")
|
||||
}
|
||||
// Assert it timed out rather than merely erroring: the spec this server
|
||||
// returns is invalid, so a test that only checks for any error passes even
|
||||
// when the timeout never fires.
|
||||
if !os.IsTimeout(err) && !strings.Contains(err.Error(), "Timeout") {
|
||||
t.Fatalf("Expected a timeout error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
// Placeholders a structurally variable path segment collapses to. They are
|
||||
// spelled out rather than hashed so a skipped-target log reads plainly.
|
||||
const (
|
||||
placeholderNum = "{num}"
|
||||
placeholderUUID = "{uuid}"
|
||||
placeholderHash = "{hash}"
|
||||
placeholderDate = "{date}"
|
||||
placeholderVar = "{var}"
|
||||
)
|
||||
|
||||
var (
|
||||
numericSegment = regexp.MustCompile(`^\d+$`)
|
||||
uuidSegment = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
hashSegment = regexp.MustCompile(`(?i)^[0-9a-f]{16,}$`)
|
||||
// a date split across path positions is already caught as numeric segments
|
||||
dateSegment = regexp.MustCompile(`^\d{4}-\d{2}(-\d{2})?$`)
|
||||
)
|
||||
|
||||
// classifySegment replaces a path segment that identifies a particular record
|
||||
// with a placeholder, leaving segments that name a route alone.
|
||||
func classifySegment(segment string) string {
|
||||
switch {
|
||||
case segment == "":
|
||||
return segment
|
||||
case numericSegment.MatchString(segment):
|
||||
return placeholderNum
|
||||
case uuidSegment.MatchString(segment):
|
||||
return placeholderUUID
|
||||
case dateSegment.MatchString(segment):
|
||||
return placeholderDate
|
||||
case hashSegment.MatchString(segment):
|
||||
return placeholderHash
|
||||
}
|
||||
return segment
|
||||
}
|
||||
|
||||
// DefaultCollapseAfter is how many distinct values a single path position may
|
||||
// hold on one host before the position itself is treated as an identifier. It
|
||||
// catches slugs and names, which no pattern can recognise by shape alone.
|
||||
const DefaultCollapseAfter = 20
|
||||
|
||||
// Fingerprinter groups URLs by the shape of their path and query, so a crawl
|
||||
// that produced ten thousand product pages is recognised as one pattern.
|
||||
//
|
||||
// It is stateful by design: whether a path position is an identifier depends on
|
||||
// how many distinct values that position has taken on that host, which is only
|
||||
// knowable across the whole input.
|
||||
type Fingerprinter struct {
|
||||
// MaxPerPattern is how many targets to keep per pattern. Zero keeps every
|
||||
// target, which is the default: dropping one is a target never scanned.
|
||||
MaxPerPattern int
|
||||
// CollapseAfter is the distinct-value threshold for a path position.
|
||||
CollapseAfter int
|
||||
|
||||
mu sync.Mutex
|
||||
kept map[string]int
|
||||
position map[string]map[string]struct{}
|
||||
}
|
||||
|
||||
// NewFingerprinter returns a fingerprinter keeping at most maxPerPattern
|
||||
// targets for each pattern. A maxPerPattern of zero disables grouping.
|
||||
func NewFingerprinter(maxPerPattern int) *Fingerprinter {
|
||||
return &Fingerprinter{
|
||||
MaxPerPattern: maxPerPattern,
|
||||
CollapseAfter: DefaultCollapseAfter,
|
||||
kept: make(map[string]int),
|
||||
position: make(map[string]map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled reports whether the fingerprinter drops anything.
|
||||
func (f *Fingerprinter) Enabled() bool {
|
||||
return f != nil && f.MaxPerPattern > 0
|
||||
}
|
||||
|
||||
// Accept reports whether a target should be scanned, recording it against its
|
||||
// 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 {
|
||||
if !f.Enabled() {
|
||||
return true
|
||||
}
|
||||
pattern, ok := f.pattern(rawURL)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.kept[pattern] >= f.MaxPerPattern {
|
||||
return false
|
||||
}
|
||||
f.kept[pattern]++
|
||||
return true
|
||||
}
|
||||
|
||||
// Pattern returns the structural fingerprint of a URL, for tests and logging.
|
||||
func (f *Fingerprinter) Pattern(rawURL string) string {
|
||||
pattern, _ := f.pattern(rawURL)
|
||||
return pattern
|
||||
}
|
||||
|
||||
func (f *Fingerprinter) pattern(rawURL string) (string, bool) {
|
||||
trimmed := strings.TrimSpace(rawURL)
|
||||
if idx := strings.Index(trimmed, "://"); idx > 0 {
|
||||
trimmed = strings.ToLower(trimmed[:idx]) + trimmed[idx:]
|
||||
}
|
||||
parsed, err := urlParse(trimmed)
|
||||
if err != nil || parsed == nil || parsed.Host == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
host := strings.ToLower(parsed.Host)
|
||||
segments := strings.Split(strings.Trim(parsed.Path, "/"), "/")
|
||||
shaped := make([]string, 0, len(segments))
|
||||
for index, segment := range segments {
|
||||
parent := strings.Join(segments[:index], "/")
|
||||
shaped = append(shaped, f.shapeSegment(host, parent, index, segment))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(host)
|
||||
builder.WriteString("/")
|
||||
builder.WriteString(strings.Join(shaped, "/"))
|
||||
if keys := queryKeys(parsed); len(keys) > 0 {
|
||||
// names without values: two searches differ by what was searched for,
|
||||
// not by the shape of the request
|
||||
builder.WriteString("?")
|
||||
builder.WriteString(strings.Join(keys, "&"))
|
||||
}
|
||||
return builder.String(), true
|
||||
}
|
||||
|
||||
// shapeSegment classifies a segment by shape, then by how many distinct values
|
||||
// the same position has already taken under this parent path on this host.
|
||||
// The parent is part of the key so a busy /blog/<slug> does not also collapse
|
||||
// /admin/<name> just because both sit at the same index.
|
||||
func (f *Fingerprinter) shapeSegment(host, parent string, index int, segment string) string {
|
||||
if shaped := classifySegment(segment); shaped != segment {
|
||||
return shaped
|
||||
}
|
||||
if f.CollapseAfter <= 0 || segment == "" {
|
||||
return segment
|
||||
}
|
||||
|
||||
key := host + "\x00" + parent + "\x00" + itoa(index)
|
||||
f.mu.Lock()
|
||||
values, ok := f.position[key]
|
||||
if !ok {
|
||||
values = make(map[string]struct{})
|
||||
f.position[key] = values
|
||||
}
|
||||
if len(values) < f.CollapseAfter {
|
||||
values[segment] = struct{}{}
|
||||
}
|
||||
collapsed := len(values) >= f.CollapseAfter
|
||||
f.mu.Unlock()
|
||||
|
||||
if collapsed {
|
||||
return placeholderVar
|
||||
}
|
||||
return segment
|
||||
}
|
||||
|
||||
// urlParse and queryKeys keep the url library dependency in one place.
|
||||
func urlParse(raw string) (*urlutil.URL, error) {
|
||||
return urlutil.Parse(raw)
|
||||
}
|
||||
|
||||
// queryKeys returns the query parameter names, sorted, with values dropped.
|
||||
func queryKeys(parsed *urlutil.URL) []string {
|
||||
if parsed.Params == nil || parsed.Params.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
parsed.Params.Iterate(func(key string, _ []string) bool {
|
||||
if !IsTrackingParam(key) {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func itoa(value int) string { return strconv.Itoa(value) }
|
||||
@@ -0,0 +1,101 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPatternGroupsIdentifiers(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
base := f.Pattern("http://acme.test/user/1/profile")
|
||||
for _, raw := range []string{
|
||||
"http://acme.test/user/2/profile",
|
||||
"http://acme.test/user/99999/profile",
|
||||
} {
|
||||
require.Equal(t, base, f.Pattern(raw), "numeric ids are the same shape")
|
||||
}
|
||||
require.Contains(t, base, placeholderNum)
|
||||
}
|
||||
|
||||
func TestPatternGroupsUUIDsHashesAndDates(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
require.Contains(t, f.Pattern("http://acme.test/o/3f2504e0-4f89-11d3-9a0c-0305e82c3301"), placeholderUUID)
|
||||
require.Contains(t, f.Pattern("http://acme.test/a/d41d8cd98f00b204e9800998ecf8427e"), placeholderHash)
|
||||
require.Contains(t, f.Pattern("http://acme.test/posts/2024-05-01"), placeholderDate)
|
||||
// a date spread across positions is already grouped as numeric segments
|
||||
require.Equal(t, "acme.test/posts/{num}/{num}", f.Pattern("http://acme.test/posts/2024/05"))
|
||||
}
|
||||
|
||||
func TestPatternKeepsRouteNames(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
require.NotEqual(t, f.Pattern("http://acme.test/admin"), f.Pattern("http://acme.test/login"),
|
||||
"route names are the structure, not identifiers within it")
|
||||
}
|
||||
|
||||
func TestPatternDropsQueryValuesButKeepsSortedNames(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
a := f.Pattern("http://acme.test/search?q=admin&page=2")
|
||||
b := f.Pattern("http://acme.test/search?page=9&q=root")
|
||||
require.Equal(t, a, b, "same parameters in a different order with different values is one shape")
|
||||
require.Contains(t, a, "page&q")
|
||||
require.NotContains(t, a, "admin")
|
||||
|
||||
require.NotEqual(t, a, f.Pattern("http://acme.test/search?q=admin"), "a different parameter set is a different shape")
|
||||
}
|
||||
|
||||
func TestPatternCollapsesHighCardinalityPosition(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
f.CollapseAfter = 5
|
||||
// slugs have no recognisable shape, so the position itself is what gives
|
||||
// them away once it has held enough distinct values
|
||||
for i := 0; i < 5; i++ {
|
||||
f.Pattern(fmt.Sprintf("http://acme.test/blog/%s-post", []string{"alpha", "beta", "gamma", "delta", "epsilon"}[i]))
|
||||
}
|
||||
require.Contains(t, f.Pattern("http://acme.test/blog/zeta-post"), placeholderVar)
|
||||
// a different host is tracked separately
|
||||
require.NotContains(t, f.Pattern("http://other.test/blog/zeta-post"), placeholderVar)
|
||||
}
|
||||
|
||||
func TestCollapseStaysInsideTheParentPath(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
f.CollapseAfter = 3
|
||||
require.Equal(t, "acme.test/blog/one", f.Pattern("http://acme.test/blog/one"))
|
||||
require.Equal(t, "acme.test/blog/two", f.Pattern("http://acme.test/blog/two"))
|
||||
require.Equal(t, "acme.test/blog/{var}", f.Pattern("http://acme.test/blog/three"))
|
||||
|
||||
// same index, different parent: a busy /blog/<slug> must not collapse /admin/<name>
|
||||
require.Equal(t, "acme.test/admin/a", f.Pattern("http://acme.test/admin/a"))
|
||||
require.Equal(t, "acme.test/admin/b", f.Pattern("http://acme.test/admin/b"))
|
||||
require.True(t, f.Accept("http://acme.test/admin/a"))
|
||||
require.True(t, f.Accept("http://acme.test/admin/b"), "a different parent still has its own cap")
|
||||
}
|
||||
|
||||
func TestAcceptKeepsUpToTheCap(t *testing.T) {
|
||||
f := NewFingerprinter(2)
|
||||
require.True(t, f.Accept("http://acme.test/user/1"))
|
||||
require.True(t, f.Accept("http://acme.test/user/2"))
|
||||
require.False(t, f.Accept("http://acme.test/user/3"), "past the cap the pattern is already covered")
|
||||
require.True(t, f.Accept("http://acme.test/admin"), "a different pattern 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) {
|
||||
f := NewFingerprinter(0)
|
||||
require.False(t, f.Enabled())
|
||||
for i := 0; i < 100; i++ {
|
||||
require.True(t, f.Accept(fmt.Sprintf("http://acme.test/user/%d", i)))
|
||||
}
|
||||
var nilF *Fingerprinter
|
||||
require.False(t, nilF.Enabled())
|
||||
require.True(t, nilF.Accept("http://acme.test/user/1"))
|
||||
}
|
||||
|
||||
func TestAcceptKeepsNonURLTargets(t *testing.T) {
|
||||
f := NewFingerprinter(1)
|
||||
require.True(t, f.Accept("acme.test"))
|
||||
require.True(t, f.Accept("not a url"))
|
||||
require.True(t, f.Accept("not a url"), "a value with no pattern is never grouped away")
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Package normalize collapses URLs that address the same resource onto one
|
||||
// form, so a target list built by a crawler does not run every template several
|
||||
// times over for what is effectively the same target.
|
||||
//
|
||||
// Only differences that cannot change the response are removed: host case, the
|
||||
// fragment (never sent to the server), a default port, a trailing slash, and
|
||||
// well known tracking parameters. Path case is left alone, because paths are
|
||||
// case sensitive on most servers.
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
// trackingParams are query keys added by ad and analytics platforms. They are
|
||||
// echoed back at most and never select a different resource, so two URLs that
|
||||
// differ only by these are the same target. The list is deliberately limited to
|
||||
// vendor prefixed names: a generic key like "ref" or "id" can be functional.
|
||||
var trackingParams = map[string]struct{}{
|
||||
"utm_source": {},
|
||||
"utm_medium": {},
|
||||
"utm_campaign": {},
|
||||
"utm_term": {},
|
||||
"utm_content": {},
|
||||
"utm_id": {},
|
||||
"utm_reader": {},
|
||||
"utm_name": {},
|
||||
"gclid": {},
|
||||
"gbraid": {},
|
||||
"wbraid": {},
|
||||
"dclid": {},
|
||||
"fbclid": {},
|
||||
"msclkid": {},
|
||||
"mc_cid": {},
|
||||
"mc_eid": {},
|
||||
"igshid": {},
|
||||
"twclid": {},
|
||||
"ttclid": {},
|
||||
"yclid": {},
|
||||
"_ga": {},
|
||||
"_gl": {},
|
||||
}
|
||||
|
||||
// IsTrackingParam reports whether a query key is a known tracking parameter.
|
||||
func IsTrackingParam(key string) bool {
|
||||
_, ok := trackingParams[strings.ToLower(key)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// URL returns the canonical form of raw. A value that cannot be parsed, or that
|
||||
// carries no host, is returned unchanged: the input provider accepts things
|
||||
// that are not URLs at all, and guessing at those would lose targets.
|
||||
func URL(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return raw
|
||||
}
|
||||
|
||||
// The scheme is case insensitive per RFC 3986, but the parser rejects one
|
||||
// that is not lowercase, so fold it before handing the value over.
|
||||
if idx := strings.Index(trimmed, "://"); idx > 0 {
|
||||
trimmed = strings.ToLower(trimmed[:idx]) + trimmed[idx:]
|
||||
}
|
||||
|
||||
parsed, err := urlutil.Parse(trimmed)
|
||||
if err != nil || parsed == nil || parsed.Host == "" {
|
||||
return raw
|
||||
}
|
||||
|
||||
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
||||
parsed.Host = strings.ToLower(parsed.Host)
|
||||
parsed.Fragment = ""
|
||||
|
||||
if host, port, ok := splitHostPort(parsed.Host); ok && isDefaultPort(parsed.Scheme, port) {
|
||||
parsed.Host = host
|
||||
}
|
||||
|
||||
stripTrackingParams(parsed)
|
||||
|
||||
// A trailing slash addresses the same resource as its absence for every
|
||||
// path but the root, where "/" is the path.
|
||||
if path := parsed.Path; len(path) > 1 && strings.HasSuffix(path, "/") {
|
||||
parsed.Path = strings.TrimRight(path, "/")
|
||||
}
|
||||
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
// stripTrackingParams removes known tracking keys, leaving every other
|
||||
// parameter and its value untouched.
|
||||
func stripTrackingParams(parsed *urlutil.URL) {
|
||||
if parsed.Params == nil || parsed.Params.IsEmpty() {
|
||||
return
|
||||
}
|
||||
// collected first: deleting while iterating would mutate the order under us
|
||||
var drop []string
|
||||
parsed.Params.Iterate(func(key string, _ []string) bool {
|
||||
if IsTrackingParam(key) {
|
||||
drop = append(drop, key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
for _, key := range drop {
|
||||
parsed.Params.Del(key)
|
||||
}
|
||||
parsed.Update()
|
||||
}
|
||||
|
||||
// splitHostPort separates a host and port, tolerating bracketed IPv6 literals.
|
||||
func splitHostPort(host string) (string, string, bool) {
|
||||
if strings.HasPrefix(host, "[") {
|
||||
end := strings.LastIndex(host, "]")
|
||||
if end < 0 {
|
||||
return host, "", false
|
||||
}
|
||||
rest := host[end+1:]
|
||||
if !strings.HasPrefix(rest, ":") {
|
||||
return host, "", false
|
||||
}
|
||||
return host[:end+1], rest[1:], true
|
||||
}
|
||||
idx := strings.LastIndex(host, ":")
|
||||
if idx < 0 {
|
||||
return host, "", false
|
||||
}
|
||||
return host[:idx], host[idx+1:], true
|
||||
}
|
||||
|
||||
func isDefaultPort(scheme, port string) bool {
|
||||
return (scheme == "http" && port == "80") || (scheme == "https" && port == "443")
|
||||
}
|
||||
|
||||
// Origin returns the scheme and host a URL addresses, which is what a template
|
||||
// built only from {{RootURL}} resolves against. It returns "" when the value
|
||||
// carries no host, so callers can fall back to treating the target as unique.
|
||||
func Origin(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if idx := strings.Index(trimmed, "://"); idx > 0 {
|
||||
trimmed = strings.ToLower(trimmed[:idx]) + trimmed[idx:]
|
||||
}
|
||||
parsed, err := urlutil.Parse(trimmed)
|
||||
if err != nil || parsed == nil || parsed.Host == "" {
|
||||
return ""
|
||||
}
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
host := strings.ToLower(parsed.Host)
|
||||
if h, port, ok := splitHostPort(host); ok && isDefaultPort(scheme, port) {
|
||||
host = h
|
||||
}
|
||||
if scheme == "" {
|
||||
return host
|
||||
}
|
||||
return scheme + "://" + host
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestURLCollapsesEquivalentForms(t *testing.T) {
|
||||
same := "http://acme.test/page"
|
||||
for _, raw := range []string{
|
||||
"http://acme.test/page",
|
||||
"http://ACME.test/page",
|
||||
"HTTP://acme.test/page",
|
||||
"http://acme.test:80/page",
|
||||
"http://acme.test/page/",
|
||||
"http://acme.test/page#section",
|
||||
"http://acme.test/page?utm_source=news&utm_campaign=q4",
|
||||
} {
|
||||
require.Equal(t, same, URL(raw), "%q addresses the same resource", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLKeepsDifferencesThatMatter(t *testing.T) {
|
||||
// path case is significant on most servers, so it is left alone
|
||||
require.NotEqual(t, URL("http://acme.test/page"), URL("http://acme.test/PAGE"))
|
||||
// a non-default port is part of the target
|
||||
require.NotEqual(t, URL("http://acme.test/page"), URL("http://acme.test:8080/page"))
|
||||
// so is the scheme
|
||||
require.NotEqual(t, URL("http://acme.test/page"), URL("https://acme.test/page"))
|
||||
// and a functional query parameter
|
||||
require.NotEqual(t, URL("http://acme.test/page"), URL("http://acme.test/page?id=1"))
|
||||
}
|
||||
|
||||
func TestURLKeepsFunctionalParamsWhenStrippingTracking(t *testing.T) {
|
||||
got := URL("http://acme.test/search?q=admin&utm_source=news&page=2")
|
||||
require.Contains(t, got, "q=admin")
|
||||
require.Contains(t, got, "page=2")
|
||||
require.NotContains(t, got, "utm_source")
|
||||
}
|
||||
|
||||
func TestURLKeepsDefaultPortForOtherSchemes(t *testing.T) {
|
||||
// 443 is only the default for https, so on http it is a real port
|
||||
require.Contains(t, URL("http://acme.test:443/page"), ":443")
|
||||
require.NotContains(t, URL("https://acme.test:443/page"), ":443")
|
||||
}
|
||||
|
||||
func TestURLKeepsRootPath(t *testing.T) {
|
||||
// "/" is the path at the root, not a trailing slash to trim
|
||||
require.Equal(t, URL("http://acme.test/"), URL("http://acme.test/"))
|
||||
require.NotEmpty(t, URL("http://acme.test/"))
|
||||
}
|
||||
|
||||
func TestURLHandlesIPv6(t *testing.T) {
|
||||
require.Equal(t, URL("http://[::1]:8080/page"), URL("http://[::1]:8080/page/"))
|
||||
require.Contains(t, URL("http://[::1]:8080/page"), "[::1]:8080")
|
||||
// the default port still goes
|
||||
require.NotContains(t, URL("http://[::1]:80/page"), ":80")
|
||||
}
|
||||
|
||||
// The provider accepts bare hosts, ip:port and other non-URL values. Guessing
|
||||
// at those would lose targets, so they come back untouched.
|
||||
func TestURLLeavesUnparsableInputAlone(t *testing.T) {
|
||||
for _, raw := range []string{"", " ", "not a url", "acme.test", "10.0.0.1:8080"} {
|
||||
require.Equal(t, raw, URL(raw))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginIgnoresPathQueryAndDefaultPort(t *testing.T) {
|
||||
same := "https://example.com"
|
||||
for _, raw := range []string{
|
||||
"https://example.com/a",
|
||||
"https://EXAMPLE.com/b?q=1#frag",
|
||||
"https://example.com:443/c",
|
||||
} {
|
||||
require.Equal(t, same, Origin(raw), "%q is the same origin", raw)
|
||||
}
|
||||
require.NotEqual(t, Origin("https://example.com/a"), Origin("http://example.com/a"))
|
||||
require.NotEqual(t, Origin("https://example.com/a"), Origin("https://other.test/a"))
|
||||
require.NotEqual(t, Origin("https://example.com/a"), Origin("https://example.com:8443/a"))
|
||||
require.Equal(t, "example.com", Origin("example.com"))
|
||||
require.Empty(t, Origin(""))
|
||||
require.Empty(t, Origin("not a url"))
|
||||
}
|
||||
|
||||
func TestIsTrackingParam(t *testing.T) {
|
||||
require.True(t, IsTrackingParam("utm_source"))
|
||||
require.True(t, IsTrackingParam("UTM_SOURCE"))
|
||||
require.True(t, IsTrackingParam("fbclid"))
|
||||
require.False(t, IsTrackingParam("id"))
|
||||
require.False(t, IsTrackingParam("ref"), "a generic name can be functional")
|
||||
}
|
||||
+136
-45
@@ -20,6 +20,10 @@ import (
|
||||
"github.com/projectdiscovery/hmap/filekv"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/mapcidr/asn"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
providerTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
@@ -39,15 +43,23 @@ const DefaultMaxDedupeItemsCount = 10000
|
||||
// ListInputProvider is a hmap/filekv backed nuclei ListInputProvider provider
|
||||
// it supports list type of input ex: urls,file,stdin,uncover,etc. (i.e just url not complete request/response)
|
||||
type ListInputProvider struct {
|
||||
ipOptions *ipOptions
|
||||
inputCount int64
|
||||
excludedCount int64
|
||||
dupeCount int64
|
||||
skippedCount int64
|
||||
hostMap *hybrid.HybridMap
|
||||
excludedHosts map[string]struct{}
|
||||
hostMapStream *filekv.FileDB
|
||||
hostMapStreamOnce sync.Once
|
||||
ipOptions *ipOptions
|
||||
inputCount int64
|
||||
excludedCount int64
|
||||
dupeCount int64
|
||||
skippedCount int64
|
||||
patternSkippedCount int64
|
||||
hostMap *hybrid.HybridMap
|
||||
excludedHosts map[string]struct{}
|
||||
hostMapStream *filekv.FileDB
|
||||
hostMapStreamOnce sync.Once
|
||||
profiles *targetprofile.Registry
|
||||
// normalizeURLs collapses targets that address the same resource onto one
|
||||
// form before they are deduplicated.
|
||||
normalizeURLs bool
|
||||
// patterns caps how many targets of one structural shape are kept. Nil or
|
||||
// disabled keeps every target.
|
||||
patterns *normalize.Fingerprinter
|
||||
sync.Once
|
||||
}
|
||||
|
||||
@@ -71,13 +83,16 @@ func New(opts *Options) (*ListInputProvider, error) {
|
||||
}
|
||||
|
||||
input := &ListInputProvider{
|
||||
hostMap: hm,
|
||||
hostMap: hm,
|
||||
normalizeURLs: !options.DisableURLNormalization,
|
||||
patterns: normalize.NewFingerprinter(options.MaxURLsPerPattern),
|
||||
ipOptions: &ipOptions{
|
||||
ScanAllIPs: options.ScanAllIPs,
|
||||
IPV4: sliceutil.Contains(options.IPVersion, "4"),
|
||||
IPV6: sliceutil.Contains(options.IPVersion, "6"),
|
||||
},
|
||||
excludedHosts: make(map[string]struct{}),
|
||||
profiles: targetprofile.NewRegistry(configuration.TemplatesDirectory(options, config.DefaultConfig)),
|
||||
}
|
||||
if options.Stream {
|
||||
fkvOptions := filekv.DefaultOptions
|
||||
@@ -105,6 +120,9 @@ func New(opts *Options) (*ListInputProvider, error) {
|
||||
if input.skippedCount > 0 {
|
||||
gologger.Info().Msgf("Number of hosts skipped from input due to exclusion: %d", input.skippedCount)
|
||||
}
|
||||
if input.patternSkippedCount > 0 {
|
||||
gologger.Info().Msgf("Supplied input was limited by max urls per pattern (%d removed).", input.patternSkippedCount)
|
||||
}
|
||||
return input, nil
|
||||
}
|
||||
|
||||
@@ -122,9 +140,14 @@ func (i *ListInputProvider) Iterate(callback func(value *contextargs.MetaInput)
|
||||
}
|
||||
})
|
||||
}
|
||||
callbackFunc := func(k, _ []byte) error {
|
||||
callbackFunc := func(k, v []byte) error {
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
if err := metaInput.Unmarshal(string(k)); err != nil {
|
||||
// v holds the target as supplied when the key is a collapsed form
|
||||
raw := string(k)
|
||||
if len(v) > 0 {
|
||||
raw = string(v)
|
||||
}
|
||||
if err := metaInput.Unmarshal(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if !callback(metaInput) {
|
||||
@@ -139,8 +162,18 @@ func (i *ListInputProvider) Iterate(callback func(value *contextargs.MetaInput)
|
||||
}
|
||||
}
|
||||
|
||||
// TargetProfiles returns the per-target template selections read from input.
|
||||
func (i *ListInputProvider) TargetProfiles() *targetprofile.Registry {
|
||||
return i.profiles
|
||||
}
|
||||
|
||||
// Set normalizes and stores passed input values
|
||||
func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
i.set(executionId, value, nil)
|
||||
}
|
||||
|
||||
// set stores value bound to selection; a nil selection runs every template.
|
||||
func (i *ListInputProvider) set(executionId string, value string, selection *targetprofile.Selection) {
|
||||
URL := strings.TrimSpace(value)
|
||||
if URL == "" {
|
||||
return
|
||||
@@ -156,7 +189,7 @@ func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
})
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
i.setItem(metaInput)
|
||||
i.setItem(metaInput, selection)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,7 +197,7 @@ func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
if iputil.IsIP(urlx.Hostname()) {
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
i.setItem(metaInput)
|
||||
i.setItem(metaInput, selection)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -192,7 +225,7 @@ func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
metaInput := contextargs.NewMetaInput()
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.setItem(metaInput)
|
||||
i.setItem(metaInput, selection)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
@@ -230,10 +263,10 @@ func (i *ListInputProvider) Set(executionId string, value string) {
|
||||
if ip != "" {
|
||||
metaInput.Input = URL
|
||||
metaInput.CustomIP = ip
|
||||
i.setItem(metaInput)
|
||||
i.setItem(metaInput, selection)
|
||||
} else {
|
||||
metaInput.Input = URL
|
||||
i.setItem(metaInput)
|
||||
i.setItem(metaInput, selection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,23 +314,18 @@ func (i *ListInputProvider) initializeInputSources(opts *Options) error {
|
||||
|
||||
// Handle targets flags
|
||||
for _, target := range options.Targets {
|
||||
switch {
|
||||
case iputil.IsCIDR(target):
|
||||
ips := expand.CIDR(target)
|
||||
i.addTargets(options.ExecutionId, ips)
|
||||
case asn.IsASN(target):
|
||||
ips := expand.ASN(target)
|
||||
i.addTargets(options.ExecutionId, ips)
|
||||
default:
|
||||
i.Set(options.ExecutionId, target)
|
||||
if err := i.addInput(options.ExecutionId, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stdin
|
||||
if options.Stdin {
|
||||
i.scanInputFromReader(
|
||||
if err := i.scanInputFromReader(
|
||||
options.ExecutionId,
|
||||
readerutil.TimeoutReader{Reader: os.Stdin, Timeout: time.Duration(options.InputReadTimeout)})
|
||||
readerutil.TimeoutReader{Reader: os.Stdin, Timeout: time.Duration(options.InputReadTimeout)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle target file
|
||||
@@ -310,8 +338,11 @@ func (i *ListInputProvider) initializeInputSources(opts *Options) error {
|
||||
}
|
||||
}
|
||||
if input != nil {
|
||||
i.scanInputFromReader(options.ExecutionId, input)
|
||||
err := i.scanInputFromReader(options.ExecutionId, input)
|
||||
_ = input.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.Uncover && options.UncoverQuery != nil {
|
||||
@@ -356,21 +387,38 @@ func (i *ListInputProvider) initializeInputSources(opts *Options) error {
|
||||
}
|
||||
|
||||
// scanInputFromReader scans a line of input from reader and passes it for storage
|
||||
func (i *ListInputProvider) scanInputFromReader(executionId string, reader io.Reader) {
|
||||
func (i *ListInputProvider) scanInputFromReader(executionId string, reader io.Reader) error {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
item := scanner.Text()
|
||||
switch {
|
||||
case iputil.IsCIDR(item):
|
||||
ips := expand.CIDR(item)
|
||||
i.addTargets(executionId, ips)
|
||||
case asn.IsASN(item):
|
||||
ips := expand.ASN(item)
|
||||
i.addTargets(executionId, ips)
|
||||
default:
|
||||
i.Set(executionId, item)
|
||||
if err := i.addInput(executionId, scanner.Text()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addInput stores a plain target or a target line, expanding CIDR and ASN
|
||||
// targets. An invalid target line fails the run: skipping it would drop the
|
||||
// target, and ignoring its selection would scan it with every template.
|
||||
func (i *ListInputProvider) addInput(executionId string, item string) error {
|
||||
var selection *targetprofile.Selection
|
||||
if targetprofile.IsLine(item) {
|
||||
target, parsed, err := i.profiles.ParseLine(item)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse target line")
|
||||
}
|
||||
item, selection = target, parsed
|
||||
}
|
||||
|
||||
switch {
|
||||
case iputil.IsCIDR(item):
|
||||
i.addTargets(executionId, expand.CIDR(item), selection)
|
||||
case asn.IsASN(item):
|
||||
i.addTargets(executionId, expand.ASN(item), selection)
|
||||
default:
|
||||
i.set(executionId, item, selection)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isExcluded checks if a URL is in the exclusion list
|
||||
@@ -486,24 +534,67 @@ func (i *ListInputProvider) Del(executionId string, value string) {
|
||||
}
|
||||
|
||||
// setItem in the kv store
|
||||
func (i *ListInputProvider) setItem(metaInput *contextargs.MetaInput) {
|
||||
func (i *ListInputProvider) setItem(metaInput *contextargs.MetaInput, selection *targetprofile.Selection) {
|
||||
key, err := metaInput.MarshalString()
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if _, ok := i.hostMap.Get(key); ok {
|
||||
// A crawled list routinely holds the same resource several times over,
|
||||
// differing only by host case, a fragment, a default port, a trailing slash
|
||||
// or a tracking parameter. Those collapse onto one entry, but the target
|
||||
// that is scanned stays exactly as supplied: stripping a parameter from the
|
||||
// request would shrink the attack surface, not just the duplicate count.
|
||||
// Duplicates are removed before the pattern cap, so a repeated URL does not
|
||||
// use up a slot that a later distinct URL still needs.
|
||||
dedupKey := key
|
||||
if i.normalizeURLs {
|
||||
if normalized := i.normalizedKey(metaInput); normalized != "" {
|
||||
dedupKey = normalized
|
||||
}
|
||||
}
|
||||
if _, ok := i.hostMap.Get(dedupKey); ok {
|
||||
i.dupeCount++
|
||||
if i.profiles != nil {
|
||||
i.profiles.Merge(metaInput.Input, 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) {
|
||||
i.patternSkippedCount++
|
||||
return
|
||||
}
|
||||
|
||||
if i.profiles != nil {
|
||||
i.profiles.Bind(metaInput.Input, selection)
|
||||
}
|
||||
i.inputCount++ // tracks target count
|
||||
_ = i.hostMap.Set(key, nil)
|
||||
// the entry is keyed by the collapsed form and carries the original, which
|
||||
// is what Iterate hands back to the engine
|
||||
_ = i.hostMap.Set(dedupKey, []byte(key))
|
||||
if i.hostMapStream != nil {
|
||||
i.setHostMapStream(key)
|
||||
}
|
||||
}
|
||||
|
||||
// normalizedKey returns the deduplication key for a target, or "" when the
|
||||
// value is not a URL and has no collapsed form.
|
||||
func (i *ListInputProvider) normalizedKey(metaInput *contextargs.MetaInput) string {
|
||||
collapsed := normalize.URL(metaInput.Input)
|
||||
if collapsed == metaInput.Input {
|
||||
return ""
|
||||
}
|
||||
clone := *metaInput
|
||||
clone.Input = collapsed
|
||||
key, err := clone.MarshalString()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
const removeTargetsChunkSize = 5000
|
||||
|
||||
// delItem removes all hostMap entries matching any of the given targets in a single scan.
|
||||
@@ -565,9 +656,9 @@ func (i *ListInputProvider) setHostMapStream(data string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ListInputProvider) addTargets(executionId string, targets []string) {
|
||||
func (i *ListInputProvider) addTargets(executionId string, targets []string, selection *targetprofile.Selection) {
|
||||
for _, target := range targets {
|
||||
i.Set(executionId, target)
|
||||
i.set(executionId, target, selection)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
@@ -17,6 +18,76 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetItemDedupDoesNotConsumePatternCap(t *testing.T) {
|
||||
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
|
||||
require.NoError(t, err)
|
||||
input := &ListInputProvider{
|
||||
hostMap: hm,
|
||||
normalizeURLs: true,
|
||||
patterns: normalize.NewFingerprinter(1),
|
||||
}
|
||||
t.Cleanup(input.Close)
|
||||
add := func(raw string) {
|
||||
meta := contextargs.NewMetaInput()
|
||||
meta.Input = raw
|
||||
input.setItem(meta, nil)
|
||||
}
|
||||
add("http://acme.test/user/1")
|
||||
add("http://ACME.test/user/1")
|
||||
add("http://acme.test/user/2")
|
||||
add("http://acme.test/admin")
|
||||
|
||||
require.Equal(t, int64(1), input.dupeCount)
|
||||
require.Equal(t, int64(1), input.patternSkippedCount)
|
||||
require.Equal(t, int64(0), input.skippedCount)
|
||||
require.Equal(t, int64(2), input.inputCount)
|
||||
}
|
||||
|
||||
func TestSetItemKeepsTheSuppliedTarget(t *testing.T) {
|
||||
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
|
||||
require.NoError(t, err)
|
||||
input := &ListInputProvider{
|
||||
hostMap: hm,
|
||||
normalizeURLs: true,
|
||||
patterns: normalize.NewFingerprinter(0),
|
||||
}
|
||||
t.Cleanup(input.Close)
|
||||
add := func(raw string) {
|
||||
meta := contextargs.NewMetaInput()
|
||||
meta.Input = raw
|
||||
input.setItem(meta, nil)
|
||||
}
|
||||
add("http://ACME.test/page/")
|
||||
add("http://acme.test/page?utm_source=news")
|
||||
|
||||
require.Equal(t, int64(1), input.dupeCount)
|
||||
require.Equal(t, int64(1), input.inputCount)
|
||||
var got []string
|
||||
input.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
got = append(got, value.Input)
|
||||
return true
|
||||
})
|
||||
require.Equal(t, []string{"http://ACME.test/page/"}, got)
|
||||
}
|
||||
|
||||
func TestSetItemWithoutNormalizationKeepsCaseVariants(t *testing.T) {
|
||||
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
|
||||
require.NoError(t, err)
|
||||
input := &ListInputProvider{
|
||||
hostMap: hm,
|
||||
normalizeURLs: false,
|
||||
patterns: normalize.NewFingerprinter(0),
|
||||
}
|
||||
t.Cleanup(input.Close)
|
||||
for _, raw := range []string{"http://ACME.test/page", "http://acme.test/page"} {
|
||||
meta := contextargs.NewMetaInput()
|
||||
meta.Input = raw
|
||||
input.setItem(meta, nil)
|
||||
}
|
||||
require.Equal(t, int64(0), input.dupeCount)
|
||||
require.Equal(t, int64(2), input.inputCount)
|
||||
}
|
||||
|
||||
func Test_expandCIDR(t *testing.T) {
|
||||
tests := []struct {
|
||||
cidr string
|
||||
@@ -36,7 +107,7 @@ func Test_expandCIDR(t *testing.T) {
|
||||
input := &ListInputProvider{hostMap: hm}
|
||||
|
||||
ips := expand.CIDR(tt.cidr)
|
||||
input.addTargets("", ips)
|
||||
input.addTargets("", ips, nil)
|
||||
// scan
|
||||
got := []string{}
|
||||
input.hostMap.Scan(func(k, _ []byte) error {
|
||||
@@ -200,7 +271,7 @@ func Test_expandASNInputValue(t *testing.T) {
|
||||
input := &ListInputProvider{hostMap: hm}
|
||||
// get the IP addresses for ASN number
|
||||
ips := expand.ASN(tt.asn)
|
||||
input.addTargets("", ips)
|
||||
input.addTargets("", ips, nil)
|
||||
// scan the hmap
|
||||
got := []string{}
|
||||
input.hostMap.Scan(func(k, v []byte) error {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTargetLines(t *testing.T) {
|
||||
hm, err := hybrid.New(hybrid.DefaultDiskOptions)
|
||||
require.NoError(t, err)
|
||||
input := &ListInputProvider{hostMap: hm, ipOptions: &ipOptions{IPV4: true}, profiles: targetprofile.NewRegistry(t.TempDir())}
|
||||
defer input.Close()
|
||||
|
||||
lines := strings.Join([]string{
|
||||
`{"target": "192.0.2.0/30", "tags": ["tomcat"]}`,
|
||||
`{"target": "192.0.2.10", "tags": ["thinkphp"]}`,
|
||||
`{"target": "192.0.2.10", "tags": ["tomcat"]}`,
|
||||
`192.0.2.20`,
|
||||
}, "\n")
|
||||
require.NoError(t, input.scanInputFromReader("", strings.NewReader(lines)))
|
||||
|
||||
require.EqualValues(t, 6, input.Count(), "4 CIDR hosts, one merged duplicate, one plain target")
|
||||
for _, ip := range []string{"192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.10"} {
|
||||
require.NotNil(t, input.profiles.For(&contextargs.MetaInput{Input: ip}), ip)
|
||||
}
|
||||
require.Nil(t, input.profiles.For(&contextargs.MetaInput{Input: "192.0.2.20"}))
|
||||
require.Nil(t, input.profiles.LoadFilter(), "a plain target needs every template")
|
||||
|
||||
err = input.scanInputFromReader("", strings.NewReader(`{"target": "192.0.2.30", "tagz": ["x"]}`))
|
||||
require.ErrorContains(t, err, "unknown keys tagz")
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package targetprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
)
|
||||
|
||||
// Registry maps targets to their template selection.
|
||||
//
|
||||
// Targets are bound while inputs are read, then Prepare is called with the
|
||||
// loaded templates before the scan starts. Selections are interned, so targets
|
||||
// sharing a selector share one memoized decision per template.
|
||||
type Registry struct {
|
||||
templatesDir string
|
||||
|
||||
mu sync.RWMutex
|
||||
targets map[string]*Selection
|
||||
selections map[string]*Selection
|
||||
rules map[string]*rule
|
||||
profiles map[string]selector
|
||||
unrestricted bool
|
||||
|
||||
metadata map[string]*index.Metadata
|
||||
}
|
||||
|
||||
var _ protocols.TargetScope = (*Registry)(nil)
|
||||
|
||||
// NewRegistry creates a registry resolving profiles and relative template
|
||||
// paths against templatesDir.
|
||||
func NewRegistry(templatesDir string) *Registry {
|
||||
return &Registry{
|
||||
templatesDir: templatesDir,
|
||||
targets: make(map[string]*Selection),
|
||||
selections: make(map[string]*Selection),
|
||||
rules: make(map[string]*rule),
|
||||
profiles: make(map[string]selector),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseLine parses a target line into its target and selection.
|
||||
func (r *Registry) ParseLine(line string) (string, *Selection, error) {
|
||||
target, profile, sel, err := parseLine(line)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if profile != "" {
|
||||
base, err := r.profile(profile)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("target %q: %w", target, err)
|
||||
}
|
||||
sel = base.merge(sel)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
compiled, err := r.internRule(sel)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("target %q: %w", target, err)
|
||||
}
|
||||
return target, r.internSelection([]*rule{compiled}), nil
|
||||
}
|
||||
|
||||
// Bind records the selection of a newly stored input; nil means unrestricted.
|
||||
func (r *Registry) Bind(input string, selection *Selection) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if selection == nil {
|
||||
r.unrestricted = true
|
||||
if _, ok := r.targets[input]; ok {
|
||||
r.targets[input] = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
current, ok := r.targets[input]
|
||||
switch {
|
||||
case !ok:
|
||||
r.targets[input] = selection
|
||||
case current != nil:
|
||||
r.targets[input] = r.union(current, selection)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge records the selection of an input that was already stored, so a
|
||||
// target listed more than once runs the union of its selections and is
|
||||
// scanned once.
|
||||
func (r *Registry) Merge(input string, selection *Selection) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
current, ok := r.targets[input]
|
||||
switch {
|
||||
case !ok || current == nil:
|
||||
// already unrestricted
|
||||
case selection == nil:
|
||||
r.unrestricted = true
|
||||
r.targets[input] = nil
|
||||
default:
|
||||
r.targets[input] = r.union(current, selection)
|
||||
}
|
||||
}
|
||||
|
||||
// Scoped reports whether any target has a selection.
|
||||
func (r *Registry) Scoped() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return len(r.selections) > 0
|
||||
}
|
||||
|
||||
// LoadFilter returns the templates worth loading: those selected for at least
|
||||
// one target. It returns nil when some target is unrestricted, since that
|
||||
// target needs every template.
|
||||
func (r *Registry) LoadFilter() index.FilterFunc {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.unrestricted || len(r.rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
rules := make([]*rule, 0, len(r.rules))
|
||||
for _, compiled := range r.rules {
|
||||
rules = append(rules, compiled)
|
||||
}
|
||||
return func(m *index.Metadata) bool { return anyMatches(rules, m) }
|
||||
}
|
||||
|
||||
// Prepare indexes the metadata of the templates about to run.
|
||||
func (r *Registry) Prepare(tpls []*templates.Template) {
|
||||
metadata := make(map[string]*index.Metadata, len(tpls))
|
||||
for _, tpl := range tpls {
|
||||
metadata[tpl.Path] = index.NewMetadataFromTemplate(tpl.Path, tpl)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.metadata = metadata
|
||||
for _, selection := range r.selections {
|
||||
selection.decisions.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// For returns the selection of input, or nil when every template applies.
|
||||
func (r *Registry) For(input *contextargs.MetaInput) protocols.TemplateSelection {
|
||||
r.mu.RLock()
|
||||
selection := r.targets[input.Input]
|
||||
r.mu.RUnlock()
|
||||
|
||||
// a nil *Selection must not become a non-nil interface
|
||||
if selection == nil {
|
||||
return nil
|
||||
}
|
||||
return selection
|
||||
}
|
||||
|
||||
func (r *Registry) profile(name string) (selector, error) {
|
||||
r.mu.RLock()
|
||||
sel, ok := r.profiles[name]
|
||||
r.mu.RUnlock()
|
||||
if ok {
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
sel, ignored, err := loadProfile(name, r.templatesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ignored) > 0 {
|
||||
gologger.Warning().Msgf("Profile %q keys %s apply to the whole scan and are ignored per target", name, strings.Join(ignored, ", "))
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.profiles[name] = sel
|
||||
r.mu.Unlock()
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
func (r *Registry) internRule(sel selector) (*rule, error) {
|
||||
key, err := canonicalKey(sel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if compiled, ok := r.rules[key]; ok {
|
||||
return compiled, nil
|
||||
}
|
||||
compiled, err := compileRule(sel, r.templatesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.rules[key] = compiled
|
||||
return compiled, nil
|
||||
}
|
||||
|
||||
func (r *Registry) internSelection(rules []*rule) *Selection {
|
||||
keys := make([]string, len(rules))
|
||||
for i, compiled := range rules {
|
||||
keys[i] = compiled.key
|
||||
}
|
||||
key := strings.Join(keys, "\n")
|
||||
if selection, ok := r.selections[key]; ok {
|
||||
return selection
|
||||
}
|
||||
selection := &Selection{registry: r, rules: rules}
|
||||
r.selections[key] = selection
|
||||
return selection
|
||||
}
|
||||
|
||||
func (r *Registry) union(a, b *Selection) *Selection {
|
||||
if a == b {
|
||||
return a
|
||||
}
|
||||
rules := slices.Concat(a.rules, b.rules)
|
||||
slices.SortFunc(rules, func(x, y *rule) int { return strings.Compare(x.key, y.key) })
|
||||
rules = slices.CompactFunc(rules, func(x, y *rule) bool { return x.key == y.key })
|
||||
return r.internSelection(rules)
|
||||
}
|
||||
|
||||
// Selection is the template selection of one or more targets: a template is
|
||||
// selected when any of its rules selects it.
|
||||
type Selection struct {
|
||||
registry *Registry
|
||||
rules []*rule
|
||||
decisions sync.Map
|
||||
}
|
||||
|
||||
var _ protocols.TemplateSelection = (*Selection)(nil)
|
||||
|
||||
// Allows reports whether the template loaded from templatePath is selected.
|
||||
// Templates unknown to Prepare are allowed rather than silently dropped.
|
||||
func (s *Selection) Allows(templatePath string) bool {
|
||||
if allowed, ok := s.decisions.Load(templatePath); ok {
|
||||
return allowed.(bool)
|
||||
}
|
||||
|
||||
s.registry.mu.RLock()
|
||||
m := s.registry.metadata[templatePath]
|
||||
s.registry.mu.RUnlock()
|
||||
|
||||
allowed := m == nil || anyMatches(s.rules, m)
|
||||
s.decisions.Store(templatePath, allowed)
|
||||
return allowed
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package targetprofile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const templatesDir = "/templates"
|
||||
|
||||
func newTemplate(path, id string, sev severity.Severity, product string, tags ...string) *templates.Template {
|
||||
info := model.Info{
|
||||
Tags: stringslice.StringSlice{Value: tags},
|
||||
SeverityHolder: severity.Holder{Severity: sev},
|
||||
}
|
||||
if product != "" {
|
||||
info.Metadata = map[string]any{"product": product}
|
||||
}
|
||||
return &templates.Template{ID: id, Path: filepath.Join(templatesDir, path), Info: info}
|
||||
}
|
||||
|
||||
var (
|
||||
thinkphpRCE = newTemplate("http/cves/thinkphp-rce.yaml", "thinkphp-rce", severity.Critical, "thinkphp", "cve", "thinkphp", "rce")
|
||||
tomcatPanel = newTemplate("http/exposed-panels/tomcat-panel.yaml", "tomcat-panel", severity.Info, "tomcat", "panel", "tomcat")
|
||||
ghostcat = newTemplate("network/cves/CVE-2020-1938.yaml", "CVE-2020-1938", severity.Critical, "geode", "cve", "apache", "tomcat")
|
||||
gitConfig = newTemplate("http/exposures/git-config.yaml", "git-config", severity.Medium, "", "exposure", "git")
|
||||
allTemplates = []*templates.Template{thinkphpRCE, tomcatPanel, ghostcat, gitConfig}
|
||||
)
|
||||
|
||||
func selected(t *testing.T, r *Registry, input string) []string {
|
||||
t.Helper()
|
||||
selection := r.For(&contextargs.MetaInput{Input: input})
|
||||
var ids []string
|
||||
for _, tpl := range allTemplates {
|
||||
if selection == nil || selection.Allows(tpl.Path) {
|
||||
ids = append(ids, tpl.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func bindLine(t *testing.T, r *Registry, line string) string {
|
||||
t.Helper()
|
||||
target, selection, err := r.ParseLine(line)
|
||||
require.NoError(t, err)
|
||||
r.Bind(target, selection)
|
||||
return target
|
||||
}
|
||||
|
||||
func TestParseLineRejectsInvalidLines(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
for name, line := range map[string]string{
|
||||
"malformed json": `{"target": "a.com"`,
|
||||
"missing target": `{"tags": ["cve"]}`,
|
||||
"unknown key": `{"target": "a.com", "tag": ["cve"]}`,
|
||||
"invalid severity": `{"target": "a.com", "severity": ["urgent"]}`,
|
||||
"invalid type": `{"target": "a.com", "type": ["smtp"]}`,
|
||||
"non string value": `{"target": "a.com", "tags": [1]}`,
|
||||
"missing profile": `{"target": "a.com", "profile": "does-not-exist"}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, _, err := r.ParseLine(line)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileMustBeAnID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "profiles"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "profiles", "php.yml"), []byte("tags: [thinkphp]\n"), 0o600))
|
||||
outside := filepath.Join(dir, "outside.yml")
|
||||
require.NoError(t, os.WriteFile(outside, []byte("tags: [tomcat]\n"), 0o600))
|
||||
|
||||
r := NewRegistry(dir)
|
||||
for _, profile := range []string{outside, "../outside.yml", "../outside", "profiles/php.yml", "php.yml", `profiles\php`, ".."} {
|
||||
t.Run(profile, func(t *testing.T) {
|
||||
line, err := json.Marshal(map[string]string{"target": "a.com", "profile": profile})
|
||||
require.NoError(t, err)
|
||||
_, _, err = r.ParseLine(string(line))
|
||||
require.ErrorContains(t, err, "profile ID")
|
||||
})
|
||||
}
|
||||
|
||||
_, selection, err := r.ParseLine(`{"target": "a.com", "profile": "php"}`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"thinkphp"}, selection.rules[0].filter.Tags)
|
||||
}
|
||||
|
||||
func TestSelectionFilters(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
tags := bindLine(t, r, `{"target": "https://a.com", "tags": ["thinkphp"]}`)
|
||||
commaTags := bindLine(t, r, `{"target": "https://b.com", "tags": "tomcat, exposure"}`)
|
||||
sev := bindLine(t, r, `{"target": "https://c.com", "severity": ["critical"], "exclude-id": ["CVE-2020-1938"]}`)
|
||||
paths := bindLine(t, r, `{"target": "https://d.com", "templates": ["http/"]}`)
|
||||
r.Prepare(allTemplates)
|
||||
|
||||
require.Equal(t, []string{"thinkphp-rce"}, selected(t, r, tags))
|
||||
require.Equal(t, []string{"tomcat-panel", "CVE-2020-1938", "git-config"}, selected(t, r, commaTags))
|
||||
require.Equal(t, []string{"thinkphp-rce"}, selected(t, r, sev))
|
||||
require.Equal(t, []string{"thinkphp-rce", "tomcat-panel", "git-config"}, selected(t, r, paths))
|
||||
require.Equal(t, []string{"thinkphp-rce", "tomcat-panel", "CVE-2020-1938", "git-config"}, selected(t, r, "https://unlisted.com"))
|
||||
}
|
||||
|
||||
func TestSelectionTech(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
tomcat := bindLine(t, r, `{"target": "https://a.com", "tech": ["Apache Tomcat"]}`)
|
||||
php := bindLine(t, r, `{"target": "https://b.com", "tech": ["ThinkPHP"], "severity": ["critical", "medium"]}`)
|
||||
r.Prepare(allTemplates)
|
||||
|
||||
// product-bound templates follow the tech list, generic ones always run;
|
||||
// Ghostcat declares product "geode" but is kept by its tomcat tag
|
||||
require.Equal(t, []string{"tomcat-panel", "CVE-2020-1938", "git-config"}, selected(t, r, tomcat))
|
||||
require.Equal(t, []string{"thinkphp-rce", "git-config"}, selected(t, r, php))
|
||||
}
|
||||
|
||||
func TestProfileWithInlineOverride(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "profiles"), 0o755))
|
||||
profile := "code: true\ntags:\n - thinkphp\nseverity:\n - info\n"
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "profiles", "php.yml"), []byte(profile), 0o600))
|
||||
|
||||
r := NewRegistry(dir)
|
||||
target, selection, err := r.ParseLine(`{"target": "https://a.com", "profile": "php", "severity": "critical"}`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, selection.rules, 1)
|
||||
require.Equal(t, []string{"thinkphp"}, selection.rules[0].filter.Tags)
|
||||
require.Equal(t, []severity.Severity{severity.Critical}, selection.rules[0].filter.Severities)
|
||||
require.Equal(t, "https://a.com", target)
|
||||
}
|
||||
|
||||
func TestDuplicateTargetsMerge(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
_, php, err := r.ParseLine(`{"target": "https://a.com", "tags": ["thinkphp"]}`)
|
||||
require.NoError(t, err)
|
||||
_, tomcat, err := r.ParseLine(`{"target": "https://a.com", "tags": ["tomcat"]}`)
|
||||
require.NoError(t, err)
|
||||
|
||||
r.Bind("https://a.com", php)
|
||||
r.Merge("https://a.com", tomcat)
|
||||
r.Merge("https://a.com", php)
|
||||
r.Bind("https://b.com", php)
|
||||
r.Merge("https://b.com", nil)
|
||||
r.Bind("https://c.com", nil)
|
||||
r.Merge("https://c.com", php)
|
||||
r.Prepare(allTemplates)
|
||||
|
||||
require.Equal(t, []string{"thinkphp-rce", "tomcat-panel", "CVE-2020-1938"}, selected(t, r, "https://a.com"))
|
||||
require.Nil(t, r.For(&contextargs.MetaInput{Input: "https://b.com"}), "a plain duplicate lifts the restriction")
|
||||
require.Nil(t, r.For(&contextargs.MetaInput{Input: "https://c.com"}), "a plain target stays unrestricted")
|
||||
|
||||
_, again, err := r.ParseLine(`{"target": "https://z.com", "tags": ["thinkphp"]}`)
|
||||
require.NoError(t, err)
|
||||
require.Same(t, php, again, "identical selectors are interned")
|
||||
}
|
||||
|
||||
func TestLoadFilter(t *testing.T) {
|
||||
t.Run("union of selections", func(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
bindLine(t, r, `{"target": "https://a.com", "tags": ["thinkphp"]}`)
|
||||
bindLine(t, r, `{"target": "https://b.com", "tech": ["tomcat"], "severity": ["info"]}`)
|
||||
filter := r.LoadFilter()
|
||||
require.NotNil(t, filter)
|
||||
|
||||
r.Prepare(allTemplates)
|
||||
var loaded []string
|
||||
for _, tpl := range allTemplates {
|
||||
if filter(r.metadata[tpl.Path]) {
|
||||
loaded = append(loaded, tpl.ID)
|
||||
}
|
||||
}
|
||||
require.Equal(t, []string{"thinkphp-rce", "tomcat-panel"}, loaded)
|
||||
})
|
||||
|
||||
t.Run("unrestricted target loads everything", func(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
bindLine(t, r, `{"target": "https://a.com", "tags": ["thinkphp"]}`)
|
||||
r.Bind("https://b.com", nil)
|
||||
require.Nil(t, r.LoadFilter())
|
||||
})
|
||||
|
||||
t.Run("plain duplicate of a target line loads everything", func(t *testing.T) {
|
||||
r := NewRegistry(templatesDir)
|
||||
target := bindLine(t, r, `{"target": "https://a.com", "tags": ["thinkphp"]}`)
|
||||
r.Merge(target, nil)
|
||||
require.Nil(t, r.LoadFilter())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTechTokens(t *testing.T) {
|
||||
for tech, want := range map[string][]string{
|
||||
"nginx": {"nginx"},
|
||||
"ThinkPHP": {"thinkphp"},
|
||||
"apache_tomcat": {"apache-tomcat"},
|
||||
"Apache Tomcat": {"apache-tomcat", "apache", "tomcat"},
|
||||
"apache:tomcat": {"apache-tomcat", "apache", "tomcat"},
|
||||
"Microsoft IIS": {"microsoft-iis", "microsoft", "iis"},
|
||||
" ": nil,
|
||||
} {
|
||||
require.Equal(t, want, techTokens(tech), tech)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package targetprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
// rule is a compiled selector.
|
||||
type rule struct {
|
||||
key string
|
||||
filter *index.Filter
|
||||
paths []string
|
||||
tech map[string]struct{}
|
||||
}
|
||||
|
||||
func compileRule(sel selector, templatesDir string) (*rule, error) {
|
||||
var severities, excludeSeverities severity.Severities
|
||||
if err := setAll(&severities, sel[keySeverity]); err != nil {
|
||||
return nil, fmt.Errorf("key %q: %w", keySeverity, err)
|
||||
}
|
||||
if err := setAll(&excludeSeverities, sel[keyExcludeSeverity]); err != nil {
|
||||
return nil, fmt.Errorf("key %q: %w", keyExcludeSeverity, err)
|
||||
}
|
||||
var protocols, excludeProtocols types.ProtocolTypes
|
||||
if err := setAll(&protocols, sel[keyType]); err != nil {
|
||||
return nil, fmt.Errorf("key %q: %w", keyType, err)
|
||||
}
|
||||
if err := setAll(&excludeProtocols, sel[keyExcludeType]); err != nil {
|
||||
return nil, fmt.Errorf("key %q: %w", keyExcludeType, err)
|
||||
}
|
||||
|
||||
r := &rule{
|
||||
filter: &index.Filter{
|
||||
Authors: lowered(sel[keyAuthor]),
|
||||
Tags: lowered(sel[keyTags]),
|
||||
ExcludeTags: lowered(sel[keyExcludeTags]),
|
||||
IncludeTags: lowered(sel[keyIncludeTags]),
|
||||
IDs: sel[keyTemplateID],
|
||||
ExcludeIDs: sel[keyExcludeID],
|
||||
Severities: severities,
|
||||
ExcludeSeverities: excludeSeverities,
|
||||
ProtocolTypes: protocols,
|
||||
ExcludeProtocolTypes: excludeProtocols,
|
||||
},
|
||||
}
|
||||
for _, path := range sel[keyTemplates] {
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(templatesDir, path)
|
||||
}
|
||||
r.paths = append(r.paths, filepath.Clean(path))
|
||||
}
|
||||
if len(sel[keyTech]) > 0 {
|
||||
r.tech = make(map[string]struct{}, len(sel[keyTech]))
|
||||
for _, tech := range sel[keyTech] {
|
||||
for _, token := range techTokens(tech) {
|
||||
r.tech[token] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, err := canonicalKey(sel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.key = key
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// matches reports whether the template described by m is selected.
|
||||
func (r *rule) matches(m *index.Metadata) bool {
|
||||
if !r.filter.Matches(m) {
|
||||
return false
|
||||
}
|
||||
if len(r.paths) > 0 && !slices.ContainsFunc(r.paths, func(path string) bool { return underPath(m.FilePath, path) }) {
|
||||
return false
|
||||
}
|
||||
return r.matchesTech(m)
|
||||
}
|
||||
|
||||
// matchesTech implements tech-aware selection: templates bound to a product run
|
||||
// only when the target runs that product, every other template always runs.
|
||||
// A template is bound when it declares metadata.product; its tags are matched
|
||||
// too because product metadata is sometimes imprecise (CVE-2020-1938 targets
|
||||
// Tomcat but declares product "geode"). Erring on running a template keeps
|
||||
// coverage when metadata and the target's tech list disagree.
|
||||
func (r *rule) matchesTech(m *index.Metadata) bool {
|
||||
if len(r.tech) == 0 || m.Product == "" {
|
||||
return true
|
||||
}
|
||||
if _, ok := r.tech[normalizeTech(m.Product)]; ok {
|
||||
return true
|
||||
}
|
||||
for _, tag := range m.Tags {
|
||||
if _, ok := r.tech[normalizeTech(tag)]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func anyMatches(rules []*rule, m *index.Metadata) bool {
|
||||
return slices.ContainsFunc(rules, func(r *rule) bool { return r.matches(m) })
|
||||
}
|
||||
|
||||
// techTokens expands a tech name reported by a fingerprinting tool into the
|
||||
// tokens template metadata uses: "Apache Tomcat" and "apache:tomcat" yield
|
||||
// apache-tomcat, apache and tomcat. Extra tokens can only select more
|
||||
// templates, never fewer.
|
||||
func techTokens(tech string) []string {
|
||||
words := strings.FieldsFunc(strings.ToLower(tech), func(r rune) bool { return r == ':' || unicode.IsSpace(r) })
|
||||
if len(words) == 0 {
|
||||
return nil
|
||||
}
|
||||
tokens := []string{normalizeTech(strings.Join(words, " "))}
|
||||
if len(words) > 1 {
|
||||
for _, word := range words {
|
||||
tokens = append(tokens, normalizeTech(word))
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
// normalizeTech maps a product or tag to its token form ("Apache_Tomcat" and
|
||||
// "apache tomcat" both become apache-tomcat).
|
||||
func normalizeTech(tech string) string {
|
||||
tech = strings.ReplaceAll(strings.ToLower(tech), "_", "-")
|
||||
return strings.Join(strings.Fields(tech), "-")
|
||||
}
|
||||
|
||||
func underPath(templatePath, path string) bool {
|
||||
templatePath = filepath.Clean(templatePath)
|
||||
if templatePath == path || strings.HasPrefix(templatePath, path+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
matched, _ := filepath.Match(path, templatePath)
|
||||
return matched
|
||||
}
|
||||
|
||||
// canonicalKey identifies equivalent selectors so they share one compiled rule.
|
||||
func canonicalKey(sel selector) (string, error) {
|
||||
normalized := make(map[string][]string, len(sel))
|
||||
for key, values := range sel {
|
||||
values = slices.Clone(values)
|
||||
slices.Sort(values)
|
||||
normalized[key] = slices.Compact(values)
|
||||
}
|
||||
data, err := json.Marshal(normalized)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func setAll(value interface{ Set(string) error }, values []string) error {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return value.Set(strings.Join(values, ","))
|
||||
}
|
||||
|
||||
func lowered(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = strings.ToLower(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Package targetprofile binds template selections to individual targets, so a
|
||||
// single scan can run a different profile on each target.
|
||||
//
|
||||
// A target line is a JSON object whose keys mirror template profiles and CLI
|
||||
// flags:
|
||||
//
|
||||
// {"target": "https://a.example", "tags": ["thinkphp"], "tech": ["php"]}
|
||||
// {"target": "https://b.example", "profile": "wordpress", "severity": ["high"]}
|
||||
package targetprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
keyTarget = "target"
|
||||
keyProfile = "profile"
|
||||
|
||||
keyTemplates = "templates"
|
||||
keyTags = "tags"
|
||||
keyExcludeTags = "exclude-tags"
|
||||
keyIncludeTags = "include-tags"
|
||||
keyAuthor = "author"
|
||||
keyTemplateID = "template-id"
|
||||
keyExcludeID = "exclude-id"
|
||||
keySeverity = "severity"
|
||||
keyExcludeSeverity = "exclude-severity"
|
||||
keyType = "type"
|
||||
keyExcludeType = "exclude-type"
|
||||
keyTech = "tech"
|
||||
)
|
||||
|
||||
// selectorKeys are the template selection keys a target line or profile may set.
|
||||
var selectorKeys = []string{
|
||||
keyTemplates, keyTags, keyExcludeTags, keyIncludeTags, keyAuthor, keyTemplateID,
|
||||
keyExcludeID, keySeverity, keyExcludeSeverity, keyType, keyExcludeType, keyTech,
|
||||
}
|
||||
|
||||
// selector is a raw template selection keyed by selector key.
|
||||
type selector map[string][]string
|
||||
|
||||
// IsLine reports whether an input line is a target line rather than a plain target.
|
||||
func IsLine(line string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(line), "{")
|
||||
}
|
||||
|
||||
// parseLine decodes a target line. Unknown keys are rejected so that a typo
|
||||
// cannot silently widen the scan to every template.
|
||||
func parseLine(line string) (target, profile string, sel selector, err error) {
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &raw); err != nil {
|
||||
return "", "", nil, fmt.Errorf("invalid target line %q: %w", line, err)
|
||||
}
|
||||
|
||||
target, err = stringValue(raw, keyTarget)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return "", "", nil, fmt.Errorf("target line %q has no %q", line, keyTarget)
|
||||
}
|
||||
|
||||
profile, err = stringValue(raw, keyProfile)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
delete(raw, keyTarget)
|
||||
delete(raw, keyProfile)
|
||||
|
||||
sel, ignored, err := toSelector(raw)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("target %q: %w", target, err)
|
||||
}
|
||||
if len(ignored) > 0 {
|
||||
return "", "", nil, fmt.Errorf("target %q: unknown keys %s (supported: target, profile, %s)", target, strings.Join(ignored, ", "), strings.Join(selectorKeys, ", "))
|
||||
}
|
||||
return target, profile, sel, nil
|
||||
}
|
||||
|
||||
// loadProfile reads the selection keys of a template profile. Keys that
|
||||
// configure the whole scan (code, var, rate limits...) cannot apply to a single
|
||||
// target and are returned as ignored.
|
||||
func loadProfile(profile, templatesDir string) (selector, []string, error) {
|
||||
// Target lines often come from other tools, so a profile is only looked up
|
||||
// by ID inside the profiles directory and never read from a given path.
|
||||
if profile == "" || strings.ContainsAny(profile, `/\`) || filepath.Ext(profile) != "" {
|
||||
return nil, nil, fmt.Errorf("profile %q must be a profile ID from %s", profile, filepath.Join(templatesDir, "profiles"))
|
||||
}
|
||||
path, err := configuration.ResolveProfilePath(profile, templatesDir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read profile %q: %w", path, err)
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return nil, nil, fmt.Errorf("parse profile %q: %w", path, err)
|
||||
}
|
||||
sel, ignored, err := toSelector(raw)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("profile %q: %w", path, err)
|
||||
}
|
||||
return sel, ignored, nil
|
||||
}
|
||||
|
||||
// toSelector extracts selector keys from raw, returning the other keys as ignored.
|
||||
func toSelector(raw map[string]any) (selector, []string, error) {
|
||||
sel := selector{}
|
||||
var ignored []string
|
||||
for key, value := range raw {
|
||||
if !slices.Contains(selectorKeys, key) {
|
||||
ignored = append(ignored, key)
|
||||
continue
|
||||
}
|
||||
values, err := stringList(value)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("key %q: %w", key, err)
|
||||
}
|
||||
if len(values) > 0 {
|
||||
sel[key] = values
|
||||
}
|
||||
}
|
||||
slices.Sort(ignored)
|
||||
return sel, ignored, nil
|
||||
}
|
||||
|
||||
// merge returns base overridden by the keys set in override, the same way
|
||||
// CLI flags override a profile.
|
||||
func (base selector) merge(override selector) selector {
|
||||
merged := make(selector, len(base)+len(override))
|
||||
for key, values := range base {
|
||||
merged[key] = values
|
||||
}
|
||||
for key, values := range override {
|
||||
merged[key] = values
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func stringValue(raw map[string]any, key string) (string, error) {
|
||||
value, ok := raw[key]
|
||||
if !ok || value == nil {
|
||||
return "", nil
|
||||
}
|
||||
str, ok := value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("key %q must be a string", key)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// stringList accepts a comma-separated string or a list of strings, matching
|
||||
// how the equivalent CLI flags and profile keys are written.
|
||||
func stringList(value any) ([]string, error) {
|
||||
var items []string
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
case string:
|
||||
items = strings.Split(v, ",")
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
str, ok := item.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected strings, got %T", item)
|
||||
}
|
||||
items = append(items, str)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("expected a string or a list of strings, got %T", value)
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
values = append(values, item)
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
updateutils "github.com/projectdiscovery/utils/update"
|
||||
)
|
||||
|
||||
// templateRelease is a published release of the official nuclei-templates.
|
||||
type templateRelease interface {
|
||||
Version() string
|
||||
Changelog() string
|
||||
DownloadSource(showProgressBar bool, callback updateutils.AssetFileCallback) error
|
||||
}
|
||||
|
||||
type githubTemplateRelease struct {
|
||||
*updateutils.GHReleaseDownloader
|
||||
}
|
||||
|
||||
func (r githubTemplateRelease) Version() string { return r.Latest.GetTagName() }
|
||||
func (r githubTemplateRelease) Changelog() string { return r.Latest.GetBody() }
|
||||
|
||||
func (r githubTemplateRelease) DownloadSource(showProgressBar bool, callback updateutils.AssetFileCallback) error {
|
||||
return r.DownloadSourceWithCallback(showProgressBar, callback)
|
||||
}
|
||||
|
||||
func latestGitHubTemplateRelease() (templateRelease, error) {
|
||||
downloader, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return githubTemplateRelease{downloader}, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
updateutils "github.com/projectdiscovery/utils/update"
|
||||
)
|
||||
|
||||
// fakeTemplateRelease is an in-memory nuclei-templates release, mapping paths
|
||||
// to contents.
|
||||
type fakeTemplateRelease struct {
|
||||
version string
|
||||
files map[string]string
|
||||
}
|
||||
|
||||
func (r fakeTemplateRelease) fetch() (templateRelease, error) { return r, nil }
|
||||
|
||||
func (r fakeTemplateRelease) Version() string { return r.version }
|
||||
func (r fakeTemplateRelease) Changelog() string { return "" }
|
||||
|
||||
func (r fakeTemplateRelease) DownloadSource(_ bool, callback updateutils.AssetFileCallback) error {
|
||||
for _, name := range slices.Sorted(maps.Keys(r.files)) {
|
||||
contents := r.files[name]
|
||||
// a github zipball nests every entry under one root directory
|
||||
uri := "projectdiscovery-nuclei-templates-test/" + name
|
||||
if err := callback(uri, fakeFileInfo{name: name, size: int64(len(contents))}, strings.NewReader(contents)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
}
|
||||
|
||||
func (f fakeFileInfo) Name() string { return filepath.Base(f.name) }
|
||||
func (f fakeFileInfo) Size() int64 { return f.size }
|
||||
func (f fakeFileInfo) Mode() fs.FileMode { return 0o644 }
|
||||
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (f fakeFileInfo) IsDir() bool { return false }
|
||||
func (f fakeFileInfo) Sys() any { return nil }
|
||||
+105
-37
@@ -16,7 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/google/go-github/v30/github"
|
||||
"github.com/google/go-github/v92/github"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
@@ -76,6 +76,17 @@ type TemplateManager struct {
|
||||
CustomTemplates *customtemplates.CustomTemplatesManager // optional if given tries to download custom templates
|
||||
DisablePublicTemplates bool // if true,
|
||||
// public templates are not downloaded from the GitHub nuclei-templates repository
|
||||
|
||||
// fetchLatestRelease overrides where the official release comes from; nil
|
||||
// means GitHub
|
||||
fetchLatestRelease func() (templateRelease, error)
|
||||
}
|
||||
|
||||
func (t *TemplateManager) latestRelease() (templateRelease, error) {
|
||||
if t.fetchLatestRelease != nil {
|
||||
return t.fetchLatestRelease()
|
||||
}
|
||||
return latestGitHubTemplateRelease()
|
||||
}
|
||||
|
||||
// FreshInstallIfNotExists installs templates if they are not already installed
|
||||
@@ -99,7 +110,7 @@ func (t *TemplateManager) FreshInstallIfNotExists() error {
|
||||
|
||||
// UpdateIfOutdated updates templates if they are outdated
|
||||
func (t *TemplateManager) UpdateIfOutdated() error {
|
||||
return withTemplatesUpdateLock(t.updateIfOutdatedLocked)
|
||||
return withTemplatesUpdateLock(config.DefaultConfig.TemplatesDirectory, t.updateIfOutdatedLocked)
|
||||
}
|
||||
|
||||
func (t *TemplateManager) updateIfOutdatedLocked() error {
|
||||
@@ -112,15 +123,21 @@ func (t *TemplateManager) updateIfOutdatedLocked() error {
|
||||
return errkit.Wrapf(err, "failed to recover template ownership at %s", config.DefaultConfig.TemplatesDirectory)
|
||||
}
|
||||
|
||||
// the version in memory predates the lock; a process that held it before
|
||||
// this one may have already installed the latest release
|
||||
if err := config.DefaultConfig.ReloadTemplateVersion(); err != nil {
|
||||
gologger.Debug().Msgf("Could not reload templates version, using the one loaded at startup: %s", err)
|
||||
}
|
||||
|
||||
needsUpdate := config.DefaultConfig.NeedsTemplateUpdate()
|
||||
|
||||
// NOTE(dwisiswant0): if PDTM API data is not available
|
||||
// (LatestNucleiTemplatesVersion is empty) but we have a current template
|
||||
// version, so we MUST verify against GitHub directly.
|
||||
if !needsUpdate && config.DefaultConfig.LatestNucleiTemplatesVersion == "" && config.DefaultConfig.TemplateVersion != "" {
|
||||
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
|
||||
release, err := t.latestRelease()
|
||||
if err == nil {
|
||||
latestVersion := ghrd.Latest.GetTagName()
|
||||
latestVersion := release.Version()
|
||||
if config.IsOutdatedVersion(config.DefaultConfig.TemplateVersion, latestVersion) {
|
||||
needsUpdate = true
|
||||
gologger.Debug().Msgf("PDTM API unavailable, verified update needed via GitHub API: %s -> %s", config.DefaultConfig.TemplateVersion, latestVersion)
|
||||
@@ -151,14 +168,14 @@ func (t *TemplateManager) installTemplatesAt(dir string) error {
|
||||
return errkit.Wrapf(err, "failed to recover template ownership at %s", dir)
|
||||
}
|
||||
|
||||
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
|
||||
release, err := t.latestRelease()
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "failed to install templates at %s", dir)
|
||||
}
|
||||
|
||||
// write templates to disk
|
||||
writtenOutputs, writeErr := t.writeTemplatesToDisk(ghrd, dir)
|
||||
if _, finalizeErr := t.finalizeTemplateWrite(config.DefaultConfig, writtenOutputs, ghrd.Latest.GetTagName(), writeErr); finalizeErr != nil {
|
||||
writtenOutputs, writeErr := t.writeTemplatesToDisk(release, dir)
|
||||
if _, finalizeErr := t.finalizeTemplateWrite(config.DefaultConfig, writtenOutputs, release.Version(), writeErr); finalizeErr != nil {
|
||||
return errkit.Wrapf(finalizeErr, "failed to finalize template installation at %s", dir)
|
||||
}
|
||||
|
||||
@@ -268,14 +285,17 @@ func (t *TemplateManager) bootstrapTemplateOwnership(dir, version string, fetchA
|
||||
func fetchTemplateReleaseArchive(version string) (*bytes.Reader, error) {
|
||||
ctx := context.Background()
|
||||
httpClient := &http.Client{Timeout: updateutils.DownloadUpdateTimeout}
|
||||
client := github.NewClient(httpClient)
|
||||
client, err := github.NewClient(github.WithHTTPClient(httpClient))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create github client for prior template release %q: %w", version, err)
|
||||
}
|
||||
archiveURL, _, err := client.Repositories.GetArchiveLink(
|
||||
ctx,
|
||||
updateutils.Organization,
|
||||
config.OfficialNucleiTemplatesRepoName,
|
||||
github.Zipball,
|
||||
&github.RepositoryContentGetOptions{Ref: version},
|
||||
true,
|
||||
1, // Follow one repository rename, as the previous client did.
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve prior template release %q archive: %w", version, err)
|
||||
@@ -287,7 +307,7 @@ func fetchTemplateReleaseArchive(version string) (*bytes.Reader, error) {
|
||||
}
|
||||
|
||||
var contents bytes.Buffer
|
||||
response, err := client.Do(ctx, request, &contents)
|
||||
response, err := client.Do(request, &contents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download prior template release %q: %w", version, err)
|
||||
}
|
||||
@@ -372,12 +392,12 @@ func (t *TemplateManager) updateTemplatesAt(dir string) error {
|
||||
oldchecksums = make(map[string]string)
|
||||
}
|
||||
|
||||
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
|
||||
release, err := t.latestRelease()
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "failed to install templates at %s", dir)
|
||||
}
|
||||
|
||||
latestVersion := ghrd.Latest.GetTagName()
|
||||
latestVersion := release.Version()
|
||||
currentVersion := config.DefaultConfig.TemplateVersion
|
||||
|
||||
if config.IsOutdatedVersion(currentVersion, latestVersion) {
|
||||
@@ -387,7 +407,7 @@ func (t *TemplateManager) updateTemplatesAt(dir string) error {
|
||||
}
|
||||
|
||||
// write templates to disk
|
||||
writtenOutputs, writeErr := t.writeTemplatesToDisk(ghrd, dir)
|
||||
writtenOutputs, writeErr := t.writeTemplatesToDisk(release, dir)
|
||||
newchecksums, finalizeErr := t.finalizeTemplateWrite(config.DefaultConfig, writtenOutputs, latestVersion, writeErr)
|
||||
if finalizeErr != nil {
|
||||
return errkit.Wrapf(finalizeErr, "failed to finalize template update at %s", dir)
|
||||
@@ -398,14 +418,14 @@ func (t *TemplateManager) updateTemplatesAt(dir string) error {
|
||||
|
||||
// print summary
|
||||
if results.totalCount > 0 {
|
||||
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", ghrd.Latest.GetTagName(), dir)
|
||||
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", latestVersion, dir)
|
||||
if !HideUpdateChangesTable {
|
||||
// print summary table
|
||||
gologger.Print().Msgf("\nNuclei Templates %s Changelog\n", ghrd.Latest.GetTagName())
|
||||
gologger.Print().Msgf("\nNuclei Templates %s Changelog\n", latestVersion)
|
||||
gologger.Print().Msg(results.String())
|
||||
}
|
||||
} else {
|
||||
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", ghrd.Latest.GetTagName(), dir)
|
||||
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", latestVersion, dir)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -499,16 +519,24 @@ func isActiveIgnoreFilePath(writePath string) bool {
|
||||
// writeTemplatesToDisk writes release outputs to disk and returns their digests.
|
||||
// The returned map includes every successfully written output; ownership
|
||||
// reconciliation filters it to official template paths.
|
||||
func (t *TemplateManager) writeTemplatesToDisk(ghrd *updateutils.GHReleaseDownloader, dir string) (map[string]string, error) {
|
||||
func (t *TemplateManager) writeTemplatesToDisk(release templateRelease, dir string) (map[string]string, error) {
|
||||
writtenOutputs := make(map[string]string)
|
||||
touchedDirectories := make(map[string]struct{})
|
||||
|
||||
writer, err := newTemplateOutputWriter(dir)
|
||||
if err != nil {
|
||||
return writtenOutputs, err
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
callbackFunc := func(uri string, f fs.FileInfo, r io.Reader) error {
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rootDir, writePath := t.getTemplateOutputLocation(dir, uri, f)
|
||||
// every output is under dir except the ignore file, which the writer
|
||||
// routes to the config directory itself
|
||||
_, writePath := t.getTemplateOutputLocation(dir, uri, f)
|
||||
if writePath == "" {
|
||||
// skip writing file
|
||||
return nil
|
||||
@@ -520,7 +548,7 @@ func (t *TemplateManager) writeTemplatesToDisk(ghrd *updateutils.GHReleaseDownlo
|
||||
return errkit.Wrapf(err, "failed to read file %s", uri)
|
||||
}
|
||||
|
||||
outputResult, outputErr := writeTemplateOutput(rootDir, writePath, bin, f.Mode())
|
||||
outputResult, outputErr := writer.write(writePath, bin, f.Mode())
|
||||
for _, directory := range outputResult.touchedDirectories {
|
||||
touchedDirectories[directory] = struct{}{}
|
||||
}
|
||||
@@ -536,7 +564,7 @@ func (t *TemplateManager) writeTemplatesToDisk(ghrd *updateutils.GHReleaseDownlo
|
||||
|
||||
var writeErr error
|
||||
|
||||
if err := ghrd.DownloadSourceWithCallback(!HideProgressBar, callbackFunc); err != nil {
|
||||
if err := release.DownloadSource(!HideProgressBar, callbackFunc); err != nil {
|
||||
writeErr = errkit.Wrap(err, "failed to download templates")
|
||||
}
|
||||
|
||||
@@ -549,7 +577,7 @@ func (t *TemplateManager) writeTemplatesToDisk(ghrd *updateutils.GHReleaseDownlo
|
||||
}
|
||||
|
||||
if !HideReleaseNotes {
|
||||
output := ghrd.Latest.GetBody()
|
||||
output := release.Changelog()
|
||||
// adjust colors for both dark / light terminal themes
|
||||
r, err := glamour.NewTermRenderer(glamour.WithAutoStyle())
|
||||
if err != nil {
|
||||
@@ -597,15 +625,56 @@ type templateOutputWriteResult struct {
|
||||
touchedDirectories []string
|
||||
}
|
||||
|
||||
func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.FileMode) (templateOutputWriteResult, error) {
|
||||
var result templateOutputWriteResult
|
||||
if isActiveIgnoreFilePath(writePath) {
|
||||
if err := config.DefaultConfig.WriteActiveIgnoreFile(contents); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.touchedDirectories = append(result.touchedDirectories, filepath.Dir(writePath))
|
||||
return result, nil
|
||||
// templateOutputWriter writes the outputs of one release under rootDir. A
|
||||
// release has thousands of files in a few hundred directories, and on windows
|
||||
// every operation inside an os.Root reopens each path component, so the root
|
||||
// is opened once and each directory is created once rather than per file.
|
||||
type templateOutputWriter struct {
|
||||
rootDir string
|
||||
root *os.Root
|
||||
createdDirectories map[string]struct{}
|
||||
}
|
||||
|
||||
func newTemplateOutputWriter(rootDir string) (*templateOutputWriter, error) {
|
||||
root, err := os.OpenRoot(rootDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open output root %q: %w", rootDir, err)
|
||||
}
|
||||
return &templateOutputWriter{rootDir: rootDir, root: root, createdDirectories: make(map[string]struct{})}, nil
|
||||
}
|
||||
|
||||
func (w *templateOutputWriter) Close() error {
|
||||
return w.root.Close()
|
||||
}
|
||||
|
||||
// writeTemplateOutput writes a single output; installs write through one
|
||||
// templateOutputWriter instead.
|
||||
func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.FileMode) (templateOutputWriteResult, error) {
|
||||
if isActiveIgnoreFilePath(writePath) {
|
||||
return writeActiveIgnoreFile(writePath, contents)
|
||||
}
|
||||
writer, err := newTemplateOutputWriter(rootDir)
|
||||
if err != nil {
|
||||
return templateOutputWriteResult{}, err
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
return writer.write(writePath, contents, mode)
|
||||
}
|
||||
|
||||
func writeActiveIgnoreFile(writePath string, contents []byte) (templateOutputWriteResult, error) {
|
||||
if err := config.DefaultConfig.WriteActiveIgnoreFile(contents); err != nil {
|
||||
return templateOutputWriteResult{}, err
|
||||
}
|
||||
return templateOutputWriteResult{touchedDirectories: []string{filepath.Dir(writePath)}}, nil
|
||||
}
|
||||
|
||||
func (w *templateOutputWriter) write(writePath string, contents []byte, mode fs.FileMode) (templateOutputWriteResult, error) {
|
||||
if isActiveIgnoreFilePath(writePath) {
|
||||
return writeActiveIgnoreFile(writePath, contents)
|
||||
}
|
||||
|
||||
var result templateOutputWriteResult
|
||||
rootDir, root := w.rootDir, w.root
|
||||
|
||||
relativePath, err := filepath.Rel(rootDir, writePath)
|
||||
if err != nil {
|
||||
@@ -616,14 +685,8 @@ func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.Fil
|
||||
return result, fmt.Errorf("output path %q escapes root %q", writePath, rootDir)
|
||||
}
|
||||
|
||||
root, err := os.OpenRoot(rootDir)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("open output root %q: %w", rootDir, err)
|
||||
}
|
||||
defer func() { _ = root.Close() }()
|
||||
|
||||
parent := filepath.Dir(relativePath)
|
||||
if parent != "." {
|
||||
if _, created := w.createdDirectories[parent]; !created && parent != "." {
|
||||
parentsToSync, err := createTemplateDirectories(root, parent)
|
||||
for _, parentToSync := range parentsToSync {
|
||||
result.touchedDirectories = append(result.touchedDirectories, filepath.Clean(filepath.Join(rootDir, parentToSync)))
|
||||
@@ -631,6 +694,7 @@ func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.Fil
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("create output parent %q: %w", parent, err)
|
||||
}
|
||||
w.createdDirectories[parent] = struct{}{}
|
||||
}
|
||||
result.touchedDirectories = append(result.touchedDirectories, filepath.Dir(writePath))
|
||||
|
||||
@@ -656,9 +720,12 @@ func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.Fil
|
||||
return result, fmt.Errorf("create temporary output %q: %w", temporaryPath, err)
|
||||
}
|
||||
|
||||
renamed := false
|
||||
defer func() {
|
||||
_ = temporary.Close()
|
||||
_ = root.Remove(temporaryPath)
|
||||
if !renamed {
|
||||
_ = root.Remove(temporaryPath)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := temporary.Write(contents); err != nil {
|
||||
@@ -682,6 +749,7 @@ func writeTemplateOutput(rootDir, writePath string, contents []byte, mode fs.Fil
|
||||
if err := root.Rename(temporaryPath, relativePath); err != nil {
|
||||
return result, fmt.Errorf("replace output %q: %w", relativePath, err)
|
||||
}
|
||||
renamed = true
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -19,15 +19,24 @@ func TestTemplateInstallation(t *testing.T) {
|
||||
// along with necessary changes that are made
|
||||
HideProgressBar = true
|
||||
|
||||
tm := &TemplateManager{}
|
||||
dir, err := os.MkdirTemp("", "nuclei-templates-*")
|
||||
require.Nil(t, err)
|
||||
cfgdir, err := os.MkdirTemp("", "nuclei-config-*")
|
||||
require.Nil(t, err)
|
||||
defer func() {
|
||||
_ = os.RemoveAll(dir)
|
||||
_ = os.RemoveAll(cfgdir)
|
||||
}()
|
||||
templates := []string{
|
||||
"http/cves/2024/CVE-2024-0001.yaml",
|
||||
"http/cves/2024/CVE-2024-0002.yaml",
|
||||
"http/exposures/configs/git-config.yaml",
|
||||
"dns/dns-saas-service-detection.yaml",
|
||||
"network/detection/rdp-detect.yaml",
|
||||
}
|
||||
release := fakeTemplateRelease{version: "v9.9.9", files: map[string]string{
|
||||
config.NucleiIgnoreFileName: "tags:\n - fuzz\n",
|
||||
"README.md": "# nuclei-templates",
|
||||
}}
|
||||
for _, path := range templates {
|
||||
release.files[path] = "id: " + filepath.Base(path)
|
||||
}
|
||||
|
||||
tm := &TemplateManager{fetchLatestRelease: release.fetch}
|
||||
dir := t.TempDir()
|
||||
cfgdir := t.TempDir()
|
||||
|
||||
// set the config directory to a temporary directory
|
||||
config.DefaultConfig.SetConfigDir(cfgdir)
|
||||
@@ -35,36 +44,43 @@ func TestTemplateInstallation(t *testing.T) {
|
||||
templatesTempDir := filepath.Join(dir, "templates")
|
||||
config.DefaultConfig.SetTemplatesDir(templatesTempDir)
|
||||
|
||||
err = tm.FreshInstallIfNotExists()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "rate limit") {
|
||||
t.Skip("Skipping test due to github rate limit")
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, tm.FreshInstallIfNotExists())
|
||||
|
||||
for _, path := range templates {
|
||||
require.FileExists(t, filepath.Join(templatesTempDir, filepath.FromSlash(path)))
|
||||
}
|
||||
|
||||
// we should switch to more fine granular tests for template
|
||||
// integrity, but for now, we just check that the templates are installed
|
||||
counter := 0
|
||||
err = filepath.Walk(templatesTempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
counter++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
// we should have at least 1000 templates
|
||||
require.Greater(t, counter, 1000)
|
||||
require.NoFileExists(t, filepath.Join(templatesTempDir, "README.md"), "meta files are not installed as templates")
|
||||
// every time we install templates, it should override the ignore file with latest one
|
||||
require.FileExists(t, config.DefaultConfig.GetActiveIgnoreFilePath())
|
||||
ownership, err := loadTemplateOwnership(templatesTempDir)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, ownership.Files, "fresh installation should record official template ownership")
|
||||
t.Logf("Installed %d templates", counter)
|
||||
require.Len(t, ownership.Files, len(templates), "fresh installation should record ownership of every installed template")
|
||||
}
|
||||
|
||||
func TestUpdateIfOutdatedSkipsReleaseInstalledWhileWaiting(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
stateDir := t.TempDir()
|
||||
|
||||
// another process held the update lock and installed the latest release
|
||||
installer := &config.Config{TemplateVersion: "v2.0.0"}
|
||||
installer.SetStateDir(stateDir)
|
||||
installer.SetTemplatesDir(templatesDir)
|
||||
require.NoError(t, installer.WriteTemplatesConfig())
|
||||
|
||||
// this process loaded its state before that install finished
|
||||
cfg := &config.Config{LatestNucleiTemplatesVersion: "v2.0.0", Logger: gologger.DefaultLogger}
|
||||
cfg.SetStateDir(stateDir)
|
||||
cfg.SetTemplatesDir(templatesDir)
|
||||
previousConfig := config.DefaultConfig
|
||||
config.DefaultConfig = cfg
|
||||
t.Cleanup(func() { config.DefaultConfig = previousConfig })
|
||||
|
||||
tm := &TemplateManager{fetchLatestRelease: func() (templateRelease, error) {
|
||||
t.Fatal("downloaded a release another process already installed")
|
||||
return nil, nil
|
||||
}}
|
||||
require.NoError(t, tm.UpdateIfOutdated())
|
||||
require.Equal(t, "v2.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
|
||||
func TestIsOutdatedVersion(t *testing.T) {
|
||||
@@ -230,12 +246,18 @@ func benchmarkWriteTemplateOutputs(b *testing.B, syncEachOutput bool) {
|
||||
rootDir := b.TempDir()
|
||||
contents := []byte("id: benchmark\ninfo:\n name: Benchmark\n author: test\n severity: info\n")
|
||||
touchedDirectories := make(map[string]struct{})
|
||||
writer, err := newTemplateOutputWriter(rootDir)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for index := 0; index < b.N; index++ {
|
||||
writePath := filepath.Join(rootDir, fmt.Sprintf("group-%d", index%16), fmt.Sprintf("template-%d.yaml", index))
|
||||
outputResult, err := writeTemplateOutput(rootDir, writePath, contents, 0o644)
|
||||
// releases nest templates a few directories deep, e.g. http/cves/2024
|
||||
writePath := filepath.Join(rootDir, fmt.Sprintf("protocol-%d", index%4), fmt.Sprintf("category-%d", index%16), fmt.Sprintf("group-%d", index%64), fmt.Sprintf("template-%d.yaml", index))
|
||||
outputResult, err := writer.write(writePath, contents, 0o644)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
)
|
||||
|
||||
const templatesUpdateLockName = "nuclei-templates-update.lock"
|
||||
|
||||
// withTemplatesUpdateLock serializes template install/update across processes.
|
||||
// Parallel `go test` packages each have their own sync.Once, so without a
|
||||
// cross-process lock they race on the shared templates directory (ENOENT,
|
||||
// partial trees, checksum mismatches).
|
||||
func withTemplatesUpdateLock(fn func() error) error {
|
||||
lockPath := filepath.Join(os.TempDir(), templatesUpdateLockName)
|
||||
// withTemplatesUpdateLock serializes template install/update of templatesDir
|
||||
// across processes. Parallel `go test` packages each have their own sync.Once,
|
||||
// so without a cross-process lock they race on a shared templates directory
|
||||
// (ENOENT, partial trees, checksum mismatches).
|
||||
func withTemplatesUpdateLock(templatesDir string, fn func() error) error {
|
||||
lockPath := templatesUpdateLockPath(templatesDir)
|
||||
deadline := time.Now().Add(10 * time.Minute)
|
||||
|
||||
for {
|
||||
@@ -40,3 +40,12 @@ func withTemplatesUpdateLock(fn func() error) error {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// templatesUpdateLockPath derives one lock per templates directory, so updates
|
||||
// of different directories do not wait on each other. Every alias of a
|
||||
// directory maps to the same lock, including a symlink whose target does not
|
||||
// exist yet.
|
||||
func templatesUpdateLockPath(templatesDir string) string {
|
||||
sum := sha256.Sum256([]byte(config.CanonicalTemplatesPath(templatesDir)))
|
||||
return filepath.Join(os.TempDir(), fmt.Sprintf("nuclei-templates-update-%x.lock", sum[:8]))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTemplatesUpdateLockPath(t *testing.T) {
|
||||
t.Run("same directory, same lock", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.Equal(t, templatesUpdateLockPath(dir), templatesUpdateLockPath(dir+string(filepath.Separator)))
|
||||
})
|
||||
|
||||
t.Run("different directories, different locks", func(t *testing.T) {
|
||||
require.NotEqual(t, templatesUpdateLockPath(t.TempDir()), templatesUpdateLockPath(t.TempDir()))
|
||||
})
|
||||
|
||||
t.Run("stable across a fresh install creating the directory", func(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "missing", "nuclei-templates")
|
||||
before := templatesUpdateLockPath(dir)
|
||||
require.NoError(t, os.MkdirAll(dir, 0o755))
|
||||
require.Equal(t, before, templatesUpdateLockPath(dir))
|
||||
})
|
||||
|
||||
t.Run("symlink to a directory, same lock", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
link := filepath.Join(t.TempDir(), "templates-link")
|
||||
if err := os.Symlink(dir, link); err != nil {
|
||||
t.Skipf("symlinks are unavailable: %v", err)
|
||||
}
|
||||
require.Equal(t, templatesUpdateLockPath(dir), templatesUpdateLockPath(link))
|
||||
})
|
||||
|
||||
t.Run("dangling symlink, same lock before and after creation", func(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
target := filepath.Join(parent, "nuclei-templates")
|
||||
link := filepath.Join(parent, "templates-link")
|
||||
if err := os.Symlink(target, link); err != nil {
|
||||
t.Skipf("symlinks are unavailable: %v", err)
|
||||
}
|
||||
require.Equal(t, templatesUpdateLockPath(target), templatesUpdateLockPath(link))
|
||||
require.Equal(t, templatesUpdateLockPath(filepath.Join(target, "nested")), templatesUpdateLockPath(filepath.Join(link, "nested")))
|
||||
|
||||
require.NoError(t, os.MkdirAll(target, 0o755))
|
||||
require.Equal(t, templatesUpdateLockPath(target), templatesUpdateLockPath(link))
|
||||
})
|
||||
|
||||
t.Run("relative dangling symlink, same lock", func(t *testing.T) {
|
||||
parent := t.TempDir()
|
||||
target := filepath.Join(parent, "nuclei-templates")
|
||||
link := filepath.Join(parent, "templates-link")
|
||||
if err := os.Symlink("nuclei-templates", link); err != nil {
|
||||
t.Skipf("symlinks are unavailable: %v", err)
|
||||
}
|
||||
require.Equal(t, templatesUpdateLockPath(target), templatesUpdateLockPath(link))
|
||||
})
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libtelnet"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libtsch"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libvnc"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libwebsocket"
|
||||
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libwmi"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/global"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
|
||||
@@ -153,6 +153,9 @@ func (s *session) prepareCommon() {
|
||||
}
|
||||
s.config.runtime.SetContextValue("executionId", s.config.opts.ExecutionId)
|
||||
s.config.runtime.SetContextValue("ctx", executionCtx)
|
||||
// Go calls derive their context from this, so the execution timeout also
|
||||
// cancels a library call in flight instead of leaving it running
|
||||
s.config.runtime.SetContext(executionCtx)
|
||||
enableRequire(s.config.runtime)
|
||||
}
|
||||
|
||||
@@ -221,6 +224,7 @@ func (s *session) cleanupCommon() {
|
||||
}
|
||||
s.config.runtime.RemoveContextValue("executionId")
|
||||
s.config.runtime.RemoveContextValue("ctx")
|
||||
s.config.runtime.SetContext(context.TODO())
|
||||
s.config.runtime.RemoveContextValue("timeoutVariants")
|
||||
s.config.runtime.RemoveContextValue("proxyURL")
|
||||
s.config.runtime.RemoveContextValue("customHeaders")
|
||||
|
||||
@@ -5,7 +5,7 @@ package {{.PackageName}}
|
||||
import (
|
||||
{{$pkgName}} "{{.PackagePath}}"
|
||||
|
||||
"github.com/Mzack9999/goja"
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
lib_websocket "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/websocket"
|
||||
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
|
||||
)
|
||||
|
||||
var (
|
||||
module = gojs.NewGojaModule("nuclei/websocket")
|
||||
)
|
||||
|
||||
func init() {
|
||||
module.Set(
|
||||
gojs.Objects{
|
||||
// Functions
|
||||
"NewClient": lib_websocket.NewClient,
|
||||
|
||||
// Var and consts
|
||||
|
||||
// Objects / Classes
|
||||
"Client": lib_websocket.NewClient,
|
||||
"Options": gojs.GetClassConstructor[lib_websocket.Options](&lib_websocket.Options{}),
|
||||
},
|
||||
).Register()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) {
|
||||
module.Enable(runtime)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function NewExecDialer(execID: string): Dialer | null {
|
||||
|
||||
|
||||
/**
|
||||
* Client is a stateful DCE/RPC + SMB client backed by Mzack9999/goimpacket.
|
||||
* Client is a stateful DCE/RPC + SMB client backed by projectdiscovery/goimpacket.
|
||||
* One Client wraps one authenticated SMB session against the target host;
|
||||
* individual RPC interfaces (SAMR, LSARPC, EPMAPPER, ...) are bound on
|
||||
* demand by the corresponding helper methods.
|
||||
|
||||
@@ -28,4 +28,5 @@ export * as structs from './structs';
|
||||
export * as telnet from './telnet';
|
||||
export * as tsch from './tsch';
|
||||
export * as vnc from './vnc';
|
||||
export * as websocket from './websocket';
|
||||
export * as wmi from './wmi';
|
||||
@@ -3,8 +3,10 @@
|
||||
/**
|
||||
* CreateGoldenTicket forges a TGT for the supplied user against the given
|
||||
* realm using the krbtgt NT hash (or AES key). It returns the ASN.1-encoded
|
||||
* ticket and the session key. If req.OutputFile is empty no file is written;
|
||||
* pass an absolute path to also persist a ccache.
|
||||
* ticket and the session key. goimpacket writes <username>.ccache in the current
|
||||
* directory, replacing slashes in the username with dots. The destination must
|
||||
* be inside the template directory unless -allow-local-file-access is enabled.
|
||||
* req.OutputFile must be empty; custom paths and in-memory generation are unsupported.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const krb = require('nuclei/krbforge');
|
||||
@@ -27,6 +29,8 @@ export function CreateGoldenTicket(req: TicketRequest): Ticket | null {
|
||||
* CreateSilverTicket forges a service ticket (TGS) for the supplied SPN. The
|
||||
* hash supplied must belong to the service account that owns the SPN (e.g.
|
||||
* the machine account NT hash for cifs/host SPNs).
|
||||
* File output follows CreateGoldenTicket. req.OutputFile and outputFile must
|
||||
* be empty; custom paths and in-memory generation are unsupported.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const krb = require('nuclei/krbforge');
|
||||
@@ -36,11 +40,11 @@ export function CreateGoldenTicket(req: TicketRequest): Ticket | null {
|
||||
* domain_sid: 'S-1-5-21-1004336348-1177238915-682003330',
|
||||
* nthash: '31d6cfe0d16ae931b73c59d7e0c089c0',
|
||||
* spn: 'cifs/server01.acme.local',
|
||||
* }, '/tmp/silver.ccache');
|
||||
* });
|
||||
* log(t.output_file);
|
||||
* ```
|
||||
*/
|
||||
export function CreateSilverTicket(req: TicketRequest, outputFile: string): Ticket | null {
|
||||
export function CreateSilverTicket(req: TicketRequest, outputFile?: string): Ticket | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,6 +93,7 @@ export interface TicketRequest {
|
||||
|
||||
KVNO?: number,
|
||||
|
||||
/** Retained for compatibility; must be empty. */
|
||||
OutputFile?: string,
|
||||
}
|
||||
|
||||
@@ -51,6 +51,25 @@ export class SMBClient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Authenticate performs SMB session setup with the supplied credentials without
|
||||
* accessing or enumerating shares. user may be "DOMAIN\\user" or "user@domain";
|
||||
* an empty user and password request a null session. Guest and null status come
|
||||
* from the server's completed session setup, not from the supplied username.
|
||||
* Each call opens and closes a new session. Rejected authentication, connection
|
||||
* failures, and policy denials return an error and no result (throw in JavaScript).
|
||||
* @example
|
||||
* ```javascript
|
||||
* const smb = require('nuclei/smb');
|
||||
* const client = new smb.SMBClient();
|
||||
* const result = client.Authenticate('acme.com', 445, 'username', 'password');
|
||||
* const validCredentials = result.Success && !result.IsGuest && !result.IsNullSession;
|
||||
* ```
|
||||
*/
|
||||
public Authenticate(host: string, port: number, user: string, password: string): AuthenticationResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListShares tries to connect to provided host and port
|
||||
* and list shares by using given credentials.
|
||||
@@ -147,6 +166,17 @@ export class SMBClient {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthenticationResult describes a completed SMB session setup.
|
||||
* Success also includes guest and null sessions. To validate credentials,
|
||||
* require Success && !IsGuest && !IsNullSession.
|
||||
*/
|
||||
export interface AuthenticationResult {
|
||||
Success?: boolean,
|
||||
IsGuest?: boolean,
|
||||
IsNullSession?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* ShareEntry is a single file or directory on an SMB share.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
|
||||
|
||||
/**
|
||||
* Client is a WebSocket client for nuclei JS templates.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* client.Send('hello');
|
||||
* const reply = client.Receive();
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
export class Client {
|
||||
|
||||
|
||||
public URL?: string;
|
||||
|
||||
|
||||
public Protocol?: string;
|
||||
|
||||
|
||||
/**
|
||||
* ResponseHeaders are the handshake response headers, set after connecting.
|
||||
* Upgrade, Connection and Sec-WebSocket-* headers are validated by the
|
||||
* handshake and not included; Protocol holds the negotiated subprotocol.
|
||||
*/
|
||||
public ResponseHeaders?: Record<string, string>;
|
||||
|
||||
|
||||
// Constructor of Client
|
||||
constructor(url: string, options?: Options) {}
|
||||
/**
|
||||
* Connect performs the opening handshake. It is optional; Send and Receive
|
||||
* connect on demand.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* client.Connect();
|
||||
* log(client.Protocol);
|
||||
* ```
|
||||
*/
|
||||
public Connect(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send sends a text message.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* client.Send('hello');
|
||||
* ```
|
||||
*/
|
||||
public Send(data: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SendHex sends a binary message given as hex.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* client.SendHex('68656c6c6f');
|
||||
* ```
|
||||
*/
|
||||
public SendHex(data: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Receive returns the next text or binary message as a string. Pings are
|
||||
* answered automatically.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* const message = client.Receive();
|
||||
* ```
|
||||
*/
|
||||
public Receive(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ReceiveHex returns the next text or binary message as hex.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* const message = client.ReceiveHex();
|
||||
* ```
|
||||
*/
|
||||
public ReceiveHex(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close sends a close frame and closes the connection.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const client = new websocket.Client('ws://acme.com/socket');
|
||||
* client.Close();
|
||||
* ```
|
||||
*/
|
||||
public Close(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Options configures a WebSocket Client. All fields are optional.
|
||||
* @example
|
||||
* ```javascript
|
||||
* const websocket = require('nuclei/websocket');
|
||||
* const opts = new websocket.Options();
|
||||
* opts.Headers = {'Origin': 'https://acme.com'};
|
||||
* opts.Protocols = ['chat'];
|
||||
* const client = new websocket.Client('wss://acme.com/socket', opts);
|
||||
* ```
|
||||
*/
|
||||
export interface Options {
|
||||
|
||||
Headers?: Record<string, string>,
|
||||
|
||||
Protocols?: string[],
|
||||
|
||||
TimeoutSeconds?: number,
|
||||
|
||||
ServerName?: string,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package dcerpc exposes a small subset of the Mzack9999/goimpacket DCE/RPC
|
||||
// Package dcerpc exposes a small subset of the projectdiscovery/goimpacket DCE/RPC
|
||||
// stack to nuclei javascript templates. It is the entry point for AD attack
|
||||
// templates that need to talk EPMAPPER / SAMR / LSARPC / SVCCTL / TSCH / WINREG
|
||||
// / SRVSVC to a domain controller or member server.
|
||||
@@ -33,16 +33,16 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
gpatexec "github.com/Mzack9999/goimpacket/pkg/atexec"
|
||||
gprpc "github.com/Mzack9999/goimpacket/pkg/dcerpc"
|
||||
gpepm "github.com/Mzack9999/goimpacket/pkg/dcerpc/epmapper"
|
||||
gplsa "github.com/Mzack9999/goimpacket/pkg/dcerpc/lsarpc"
|
||||
gpsamr "github.com/Mzack9999/goimpacket/pkg/dcerpc/samr"
|
||||
gpsvcctl "github.com/Mzack9999/goimpacket/pkg/dcerpc/svcctl"
|
||||
gptsch "github.com/Mzack9999/goimpacket/pkg/dcerpc/tsch"
|
||||
gpsession "github.com/Mzack9999/goimpacket/pkg/session"
|
||||
gpsmb "github.com/Mzack9999/goimpacket/pkg/smb"
|
||||
gpsmbexec "github.com/Mzack9999/goimpacket/pkg/smbexec"
|
||||
gpatexec "github.com/projectdiscovery/goimpacket/pkg/atexec"
|
||||
gprpc "github.com/projectdiscovery/goimpacket/pkg/dcerpc"
|
||||
gpepm "github.com/projectdiscovery/goimpacket/pkg/dcerpc/epmapper"
|
||||
gplsa "github.com/projectdiscovery/goimpacket/pkg/dcerpc/lsarpc"
|
||||
gpsamr "github.com/projectdiscovery/goimpacket/pkg/dcerpc/samr"
|
||||
gpsvcctl "github.com/projectdiscovery/goimpacket/pkg/dcerpc/svcctl"
|
||||
gptsch "github.com/projectdiscovery/goimpacket/pkg/dcerpc/tsch"
|
||||
gpsession "github.com/projectdiscovery/goimpacket/pkg/session"
|
||||
gpsmb "github.com/projectdiscovery/goimpacket/pkg/smb"
|
||||
gpsmbexec "github.com/projectdiscovery/goimpacket/pkg/smbexec"
|
||||
"github.com/projectdiscovery/goja"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/libs/smbsession"
|
||||
@@ -60,7 +60,7 @@ type DomainUser = gpsamr.DomainUser
|
||||
type LookupResult = gplsa.LookupResult
|
||||
|
||||
type (
|
||||
// Client is a stateful DCE/RPC + SMB client backed by Mzack9999/goimpacket.
|
||||
// Client is a stateful DCE/RPC + SMB client backed by projectdiscovery/goimpacket.
|
||||
// One Client wraps one authenticated SMB session against the target host;
|
||||
// individual RPC interfaces (SAMR, LSARPC, EPMAPPER, ...) are bound on
|
||||
// demand by the corresponding helper methods.
|
||||
|
||||
@@ -3,10 +3,10 @@ package dcerpc
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gpsrvsvc "github.com/Mzack9999/goimpacket/pkg/dcerpc/srvsvc"
|
||||
gpsvcctl "github.com/Mzack9999/goimpacket/pkg/dcerpc/svcctl"
|
||||
winstation "github.com/Mzack9999/goimpacket/pkg/dcerpc/tsts"
|
||||
gpwkssvc "github.com/Mzack9999/goimpacket/pkg/dcerpc/wkssvc"
|
||||
gpsrvsvc "github.com/projectdiscovery/goimpacket/pkg/dcerpc/srvsvc"
|
||||
gpsvcctl "github.com/projectdiscovery/goimpacket/pkg/dcerpc/svcctl"
|
||||
winstation "github.com/projectdiscovery/goimpacket/pkg/dcerpc/tsts"
|
||||
gpwkssvc "github.com/projectdiscovery/goimpacket/pkg/dcerpc/wkssvc"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package dcerpc
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gpsrvsvc "github.com/Mzack9999/goimpacket/pkg/dcerpc/srvsvc"
|
||||
gpsvcctl "github.com/Mzack9999/goimpacket/pkg/dcerpc/svcctl"
|
||||
winstation "github.com/Mzack9999/goimpacket/pkg/dcerpc/tsts"
|
||||
gpwkssvc "github.com/Mzack9999/goimpacket/pkg/dcerpc/wkssvc"
|
||||
gpsrvsvc "github.com/projectdiscovery/goimpacket/pkg/dcerpc/srvsvc"
|
||||
gpsvcctl "github.com/projectdiscovery/goimpacket/pkg/dcerpc/svcctl"
|
||||
winstation "github.com/projectdiscovery/goimpacket/pkg/dcerpc/tsts"
|
||||
gpwkssvc "github.com/projectdiscovery/goimpacket/pkg/dcerpc/wkssvc"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dcerpc
|
||||
|
||||
import (
|
||||
gptr "github.com/Mzack9999/goimpacket/pkg/transport"
|
||||
gptr "github.com/projectdiscovery/goimpacket/pkg/transport"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/libs/gptransport"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
gpwmiexec "github.com/Mzack9999/goimpacket/pkg/wmiexec"
|
||||
gpwmiexec "github.com/projectdiscovery/goimpacket/pkg/wmiexec"
|
||||
"github.com/oiweiwei/go-msrpc/dcerpc"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/libs/gptransport"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package gptransport binds Mzack9999/goimpacket TCP dials to nuclei's
|
||||
// Package gptransport binds projectdiscovery/goimpacket TCP dials to nuclei's
|
||||
// per-execution fastdialer and network policy.
|
||||
//
|
||||
// Import this package (directly or via smbsession / dcerpc) so init() installs
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
gptr "github.com/Mzack9999/goimpacket/pkg/transport"
|
||||
gptr "github.com/projectdiscovery/goimpacket/pkg/transport"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
)
|
||||
|
||||
@@ -9,16 +9,13 @@ package krbforge
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
gpkrb "github.com/Mzack9999/goimpacket/pkg/kerberos"
|
||||
gpkrb "github.com/projectdiscovery/goimpacket/pkg/kerberos"
|
||||
"github.com/projectdiscovery/goja"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
filepathutil "github.com/projectdiscovery/nuclei/v3/pkg/utils/filepath"
|
||||
)
|
||||
|
||||
// TicketRequest mirrors gopacket's TicketConfig with json-friendly tags.
|
||||
@@ -35,7 +32,10 @@ type TicketRequest struct {
|
||||
ExtraSIDs []string `json:"extra_sids,omitempty"`
|
||||
DurationHours int `json:"duration_hours,omitempty"`
|
||||
KVNO int `json:"kvno,omitempty"`
|
||||
OutputFile string `json:"output_file,omitempty"`
|
||||
|
||||
// OutputFile is retained for compatibility and must be empty.
|
||||
// goimpacket always writes its default ccache file.
|
||||
OutputFile string `json:"output_file,omitempty"`
|
||||
}
|
||||
|
||||
// Ticket is the forged ticket plus metadata.
|
||||
@@ -48,9 +48,10 @@ type Ticket struct {
|
||||
|
||||
// CreateGoldenTicket forges a TGT for the supplied user against the given
|
||||
// realm using the krbtgt NT hash (or AES key). It returns the ASN.1-encoded
|
||||
// ticket and the session key. If req.OutputFile is empty no file is written;
|
||||
// pass an output path allowed by the nuclei file sandbox to also persist a
|
||||
// ccache. Use -allow-local-file-access to allow writing outside the sandbox.
|
||||
// ticket and the session key. goimpacket writes <username>.ccache in the current
|
||||
// directory, replacing slashes in the username with dots. The destination must
|
||||
// be inside the template directory unless -allow-local-file-access is enabled.
|
||||
// req.OutputFile must be empty; custom paths and in-memory generation are unsupported.
|
||||
//
|
||||
// @example
|
||||
// ```javascript
|
||||
@@ -90,16 +91,14 @@ func createGoldenTicket(executionID string, req TicketRequest) (*Ticket, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.OutputFile == "" {
|
||||
cfg.OutputFile = "-"
|
||||
}
|
||||
|
||||
return createTicket(cfg)
|
||||
}
|
||||
|
||||
// CreateSilverTicket forges a service ticket (TGS) for the supplied SPN. The
|
||||
// hash supplied must belong to the service account that owns the SPN (e.g.
|
||||
// the machine account NT hash for cifs/host SPNs).
|
||||
// File output follows CreateGoldenTicket. req.OutputFile and outputFile must
|
||||
// be empty; custom paths and in-memory generation are unsupported.
|
||||
//
|
||||
// @example
|
||||
// ```javascript
|
||||
@@ -111,7 +110,7 @@ func createGoldenTicket(executionID string, req TicketRequest) (*Ticket, error)
|
||||
// domain_sid: 'S-1-5-21-1004336348-1177238915-682003330',
|
||||
// nthash: '31d6cfe0d16ae931b73c59d7e0c089c0',
|
||||
// spn: 'cifs/server01.acme.local',
|
||||
// }, '/tmp/silver.ccache');
|
||||
// });
|
||||
//
|
||||
// log(t.output_file);
|
||||
// ```
|
||||
@@ -150,10 +149,6 @@ func createSilverTicket(executionID string, req TicketRequest, outputFile string
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.OutputFile == "" {
|
||||
cfg.OutputFile = "-"
|
||||
}
|
||||
|
||||
return createTicket(cfg)
|
||||
}
|
||||
|
||||
@@ -167,18 +162,23 @@ func createTicket(cfg *gpkrb.TicketConfig) (*Ticket, error) {
|
||||
HexTicket: hex.EncodeToString(res.Ticket),
|
||||
HexKey: hex.EncodeToString(res.SessionKey),
|
||||
EncType: res.EncType,
|
||||
OutputFile: cfg.OutputFile,
|
||||
OutputFile: res.Filename,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildConfig(executionID string, req TicketRequest, outputFile string) (*gpkrb.TicketConfig, error) {
|
||||
if outputFile == "" {
|
||||
outputFile = req.OutputFile
|
||||
if outputFile != "" || req.OutputFile != "" {
|
||||
return nil, fmt.Errorf("goimpacket writes <username>.ccache in the current directory: custom output paths and in-memory ticket generation are not supported; leave outputFile empty")
|
||||
}
|
||||
|
||||
normalizedOutputFile, err := normalizeOutputFile(executionID, outputFile)
|
||||
// Match goimpacket's destination before it performs the file write.
|
||||
filename := strings.ReplaceAll(req.Username, "/", ".") + ".ccache"
|
||||
destination, err := filepath.Abs(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("resolve ccache destination %q: %w", filename, err)
|
||||
}
|
||||
if _, err := protocolstate.NormalizePathWithExecutionId(executionID, destination); err != nil {
|
||||
return nil, fmt.Errorf("validate ccache destination: %w", err)
|
||||
}
|
||||
|
||||
return &gpkrb.TicketConfig{
|
||||
@@ -194,48 +194,9 @@ func buildConfig(executionID string, req TicketRequest, outputFile string) (*gpk
|
||||
ExtraSIDs: req.ExtraSIDs,
|
||||
Duration: req.DurationHours,
|
||||
KVNO: req.KVNO,
|
||||
OutputFile: normalizedOutputFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeOutputFile(executionID string, outputFile string) (string, error) {
|
||||
if outputFile == "" || outputFile == "-" {
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
if protocolstate.IsLfaAllowed(&types.Options{ExecutionId: executionID}) {
|
||||
// Preserve the existing relative-path behavior when
|
||||
// -allow-local-file-access is enabled: avoid implicit CWD writes by
|
||||
// placing relative ccache paths in temp.
|
||||
if !filepath.IsAbs(outputFile) {
|
||||
outputFile = filepath.Join(os.TempDir(), outputFile)
|
||||
}
|
||||
|
||||
normalized, err := filepath.Abs(outputFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("normalize output file %q: %w", outputFile, err)
|
||||
}
|
||||
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
normalized := outputFile
|
||||
if !filepath.IsAbs(normalized) {
|
||||
normalized = filepath.Join(config.DefaultConfig.GetTemplateDir(), normalized)
|
||||
}
|
||||
|
||||
normalized, err := filepath.Abs(normalized)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("normalize output file %q: %w", outputFile, err)
|
||||
}
|
||||
|
||||
if filepathutil.IsPathWithinDirectory(normalized, config.DefaultConfig.GetTemplateDir()) {
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("path %v is outside nuclei-template directory and -allow-local-file-access is not enabled", outputFile)
|
||||
}
|
||||
|
||||
func exportTicketRequest(vm *goja.Runtime, value goja.Value) (TicketRequest, error) {
|
||||
var req TicketRequest
|
||||
if err := vm.ExportTo(value, &req); err != nil {
|
||||
|
||||
Loaded 100 of 251 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user