mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-09 17:43:08 -10:00
Compare commits
447
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ff99c503 | ||
|
|
e8f48fbe47 | ||
|
|
ffd70c3a90 | ||
|
|
d04a1fe7ff | ||
|
|
490e9c3f34 | ||
|
|
263bc13412 | ||
|
|
660b7438d8 | ||
|
|
92ef4fde14 | ||
|
|
595980256b | ||
|
|
aa761b9c33 | ||
|
|
731d438319 | ||
|
|
a85a545323 | ||
|
|
5ea11e5f43 | ||
|
|
230a17d4fa | ||
|
|
d2499b74da | ||
|
|
93483eaff9 | ||
|
|
a81624e595 | ||
|
|
a9df3eff9e | ||
|
|
08820abff6 | ||
|
|
fa3fa92092 | ||
|
|
be7b2648ef | ||
|
|
47398198a0 | ||
|
|
f846a5553d | ||
|
|
39c5d8764e | ||
|
|
93b64043b1 | ||
|
|
7d6b2fef6f | ||
|
|
445e6ab972 | ||
|
|
7f8abe2c0f | ||
|
|
bfd8d4585c | ||
|
|
1c3cc97748 | ||
|
|
947a2a9a2a | ||
|
|
8e1e613a15 | ||
|
|
5a339a8da1 | ||
|
|
779868985d | ||
|
|
c4d933d165 | ||
|
|
f58b21e6c3 | ||
|
|
71805f5093 | ||
|
|
1086d5d391 | ||
|
|
75434a01e9 | ||
|
|
e9f062f600 | ||
|
|
5bd7c79be4 | ||
|
|
8f31e9d24d | ||
|
|
e17273911b | ||
|
|
e2840f3644 | ||
|
|
be6e32d5ae | ||
|
|
86d9a9bd54 | ||
|
|
7c2b74424e | ||
|
|
f1e79cd82b | ||
|
|
4ae1faa64f | ||
|
|
834cff9fa2 | ||
|
|
23f766eb61 | ||
|
|
43f99eadde | ||
|
|
baaeb15022 | ||
|
|
d4b8a07e80 | ||
|
|
319baa3152 | ||
|
|
e24855067e | ||
|
|
a4c597fd31 | ||
|
|
87afa2fd4e | ||
|
|
b4c9cc10c9 | ||
|
|
7f6cfd42f3 | ||
|
|
a03f13e929 | ||
|
|
02f5d9a6c3 | ||
|
|
f46ee55118 | ||
|
|
a9d358a56a | ||
|
|
9545a9246c | ||
|
|
48e04c7317 | ||
|
|
2954b81c85 | ||
|
|
052855717d | ||
|
|
e1f656033d | ||
|
|
9301048bc4 | ||
|
|
e21e636530 | ||
|
|
6d433e6a6f | ||
|
|
e8f1a788ac | ||
|
|
f1702dc401 | ||
|
|
3ab0a66962 | ||
|
|
bf53db167d | ||
|
|
d13b58d987 | ||
|
|
206446f567 | ||
|
|
b901fb8aea | ||
|
|
7e1a8f7da9 | ||
|
|
943ec350bb | ||
|
|
a54a6252a6 | ||
|
|
131b7b5e2c | ||
|
|
773ddf1411 | ||
|
|
665914747c | ||
|
|
bb06c6a470 | ||
|
|
bdbdc1e8de | ||
|
|
68cc4b17cd | ||
|
|
308044e234 | ||
|
|
64ad459151 | ||
|
|
4e6f69cb97 | ||
|
|
cdf3798bb7 | ||
|
|
d2699653fd | ||
|
|
663c3528c7 | ||
|
|
9dbbe4c225 | ||
|
|
40e165a065 | ||
|
|
25c4b0d623 | ||
|
|
cb8434c81c | ||
|
|
e7a383124b | ||
|
|
9cd0fbb983 | ||
|
|
1e8b85a133 | ||
|
|
c1b2553022 | ||
|
|
99c22b36f0 | ||
|
|
213b74b683 | ||
|
|
ee9a2ed36d | ||
|
|
9fd430d803 | ||
|
|
20c1510069 | ||
|
|
6ab068a356 | ||
|
|
a4f855aeee | ||
|
|
30c6e1f0ca | ||
|
|
7448d2ab23 | ||
|
|
833bd0102c | ||
|
|
cefccf97f3 | ||
|
|
b229dadf45 | ||
|
|
332cfd5372 | ||
|
|
bad0e62459 | ||
|
|
cebb1a45b0 | ||
|
|
39ab5141a8 | ||
|
|
347b996d2c | ||
|
|
5dde4c2b30 | ||
|
|
3a686c1ba5 | ||
|
|
8c214de616 | ||
|
|
8081bcbee8 | ||
|
|
913d4161d1 | ||
|
|
5c3c00f2fa | ||
|
|
ab5c51640e | ||
|
|
beccd36038 | ||
|
|
2b39243c73 | ||
|
|
bc53323223 | ||
|
|
0cfd312330 | ||
|
|
8be6ff6389 | ||
|
|
9ece5bc5f6 | ||
|
|
d117fcada4 | ||
|
|
316de3dc43 | ||
|
|
86b0f3f14f | ||
|
|
e284b6bc24 | ||
|
|
1248a14a62 | ||
|
|
4a2eb3b29d | ||
|
|
413a418714 | ||
|
|
4cf0bbc08a | ||
|
|
1628b4c437 | ||
|
|
e554a7e514 | ||
|
|
1fb6a9a287 | ||
|
|
214d5e1310 | ||
|
|
54134f0f5b | ||
|
|
3e68f2b99f | ||
|
|
f8451703a2 | ||
|
|
5e0c3e3d4f | ||
|
|
9f4e53b933 | ||
|
|
aba8fda759 | ||
|
|
c2767d6ef6 | ||
|
|
c99bc1b959 | ||
|
|
05d328d936 | ||
|
|
c1fcd4d8d2 | ||
|
|
d1bc79be95 | ||
|
|
c505b5f3c4 | ||
|
|
6a4e6fab6f | ||
|
|
9bf950c861 | ||
|
|
4edd5c2c68 | ||
|
|
5cf7f3ba47 | ||
|
|
c918d010c5 | ||
|
|
ed46eb76e3 | ||
|
|
b0eeadc554 | ||
|
|
5670af4c66 | ||
|
|
6c4fa11c96 | ||
|
|
88f169d56d | ||
|
|
d7730705af | ||
|
|
f3b817b818 | ||
|
|
be87581d1e | ||
|
|
b4761d4fc4 | ||
|
|
e78e9ddea8 | ||
|
|
e4d1535691 | ||
|
|
339578d415 | ||
|
|
a3109229fd | ||
|
|
69f4fe915b | ||
|
|
c8a0d45d38 | ||
|
|
10f1967cdc | ||
|
|
767655e306 | ||
|
|
0bd50f957a | ||
|
|
e264bd82f9 | ||
|
|
1c0f77d28e | ||
|
|
6a42a7bf52 | ||
|
|
3827928531 | ||
|
|
b296cc0d46 | ||
|
|
9a20d1cc69 | ||
|
|
3bb429522f | ||
|
|
535e04d555 | ||
|
|
4ce6dea3e9 | ||
|
|
b31f787d79 | ||
|
|
17101d0501 | ||
|
|
a4cc18f30e | ||
|
|
866da46866 | ||
|
|
788973d974 | ||
|
|
7989011cdf | ||
|
|
a151a1d54f | ||
|
|
1db63fdb42 | ||
|
|
8b440a5a77 | ||
|
|
af61439dc5 | ||
|
|
4716246849 | ||
|
|
9a13e64144 | ||
|
|
d43c1f547f | ||
|
|
506e4fb0fc | ||
|
|
a5715288aa | ||
|
|
cc5645e674 | ||
|
|
40075b0b20 | ||
|
|
90a0cce807 | ||
|
|
1caa955c77 | ||
|
|
52960db710 | ||
|
|
ca3e27c5f5 | ||
|
|
a94aa7f00d | ||
|
|
c14335d351 | ||
|
|
c5a528cc60 | ||
|
|
d9dabbb4a1 | ||
|
|
d21749be3a | ||
|
|
830e012a10 | ||
|
|
9b368f3eaa | ||
|
|
59e6ffa6fb | ||
|
|
b870cdcd67 | ||
|
|
3f776b5ff0 | ||
|
|
ad74f0094c | ||
|
|
4137bedd31 | ||
|
|
1568f1e68e | ||
|
|
a48a58ef59 | ||
|
|
007ee7d4ba | ||
|
|
0de51237b7 | ||
|
|
4857883667 | ||
|
|
bccaa4410d | ||
|
|
d8253b97e9 | ||
|
|
65f60dd0a6 | ||
|
|
1115f18b68 | ||
|
|
458367feb7 | ||
|
|
0af8c98679 | ||
|
|
50a6526721 | ||
|
|
7010ce6a91 | ||
|
|
5d9da1d199 | ||
|
|
0217339d5d | ||
|
|
d23849e996 | ||
|
|
59dad1b619 | ||
|
|
889c8ff9c5 | ||
|
|
c990724404 | ||
|
|
50e0afd188 | ||
|
|
04d0caa646 | ||
|
|
80a001ca0f | ||
|
|
8d33e4a4c7 | ||
|
|
1b080a7d19 | ||
|
|
a847374f2c | ||
|
|
9c9247895d | ||
|
|
37052a7e3f | ||
|
|
c4825e98c9 | ||
|
|
0c0e886c2c | ||
|
|
b3e02efa7f | ||
|
|
aa3c7a361c | ||
|
|
afb2afaf4f | ||
|
|
6fde18d311 | ||
|
|
62721e589a | ||
|
|
66ef296067 | ||
|
|
496589e7d0 | ||
|
|
113ea8afa9 | ||
|
|
a5d0eb9fda | ||
|
|
65732c382f | ||
|
|
ab8c7feeba | ||
|
|
bd510ac6bf | ||
|
|
71457b4f69 | ||
|
|
2815cb2aef | ||
|
|
97c8233b72 | ||
|
|
cb824a4718 | ||
|
|
78a5cadc67 | ||
|
|
6bb5b5dbd1 | ||
|
|
f5aa294aa6 | ||
|
|
77b34db0cc | ||
|
|
8f23a8633f | ||
|
|
87e0a1d2a4 | ||
|
|
28d5b3e8c0 | ||
|
|
c2d38083dc | ||
|
|
1884ed920f | ||
|
|
d28dd2c745 | ||
|
|
3336556187 | ||
|
|
2c8f4a0318 | ||
|
|
8c8a1a9d78 | ||
|
|
81749cf750 | ||
|
|
846f4425b0 | ||
|
|
fa876a4d92 | ||
|
|
352f46cc35 | ||
|
|
0ae69a7d53 | ||
|
|
80b58e6e9b | ||
|
|
4d7e91e4e0 | ||
|
|
6b9208cdbf | ||
|
|
26b28c69a4 | ||
|
|
98df72854b | ||
|
|
a14b3de2a4 | ||
|
|
7fa79ba6a6 | ||
|
|
52245202ff | ||
|
|
3c434fb73a | ||
|
|
f40fa195b6 | ||
|
|
a2b6d2abc7 | ||
|
|
b87b28d7ad | ||
|
|
4d9f2f9867 | ||
|
|
4177ca752b | ||
|
|
8ffccaf639 | ||
|
|
fd1da307b6 | ||
|
|
b104625865 | ||
|
|
5674792a7c | ||
|
|
a80d0b881a | ||
|
|
0e243e3edb | ||
|
|
54f9310945 | ||
|
|
3a827b8c8b | ||
|
|
73d976be6c | ||
|
|
06dd497f85 | ||
|
|
97e13fae6e | ||
|
|
1b8e3515bf | ||
|
|
0de1d12cc9 | ||
|
|
47f74706a5 | ||
|
|
dad93f0ed6 | ||
|
|
3adff6347b | ||
|
|
349299c9ab | ||
|
|
f76bd78d15 | ||
|
|
cbbf15bc1e | ||
|
|
591a019e83 | ||
|
|
5b4bc8efc8 | ||
|
|
8496c48c6f | ||
|
|
c5611b9ac4 | ||
|
|
ccd0aaca40 | ||
|
|
65589e2369 | ||
|
|
77d3b6d314 | ||
|
|
c36316de36 | ||
|
|
da4e351fc6 | ||
|
|
010ee044b2 | ||
|
|
71f9782b2c | ||
|
|
536e427c01 | ||
|
|
ae203e7aad | ||
|
|
2b6b1d3371 | ||
|
|
3fdd8ca091 | ||
|
|
af77e344f0 | ||
|
|
1b244b2330 | ||
|
|
fb1a318c6a | ||
|
|
dbc69f7bf0 | ||
|
|
aed2cb4643 | ||
|
|
3fb407b345 | ||
|
|
fbeb87e55c | ||
|
|
15e2d88ca5 | ||
|
|
c987c2df6a | ||
|
|
d3ce7ce640 | ||
|
|
4d967cba5c | ||
|
|
213d3badfd | ||
|
|
dece1c5ea9 | ||
|
|
5e5758b7d3 | ||
|
|
06fb71ebf2 | ||
|
|
c50f92fdd3 | ||
|
|
5b6510fb79 | ||
|
|
a5b59a9a4e | ||
|
|
268ea525bd | ||
|
|
14283b0e13 | ||
|
|
322a40e1db | ||
|
|
aa457f1c44 | ||
|
|
bd0c398887 | ||
|
|
b4a5cdab02 | ||
|
|
1981edbaac | ||
|
|
0e2fb326d6 | ||
|
|
ef0faa7d6f | ||
|
|
e8b53505e5 | ||
|
|
1d6b532bbc | ||
|
|
b6cea874d3 | ||
|
|
36dafa988b | ||
|
|
d7bdb9c058 | ||
|
|
f3c12c43eb | ||
|
|
4dad173f0a | ||
|
|
1b557852c4 | ||
|
|
e5052eb771 | ||
|
|
56411349d2 | ||
|
|
77dda01e67 | ||
|
|
a7faddf748 | ||
|
|
d47ace7701 | ||
|
|
0270f5f1da | ||
|
|
9cf5459dd9 | ||
|
|
37c06098ad | ||
|
|
ff7b951bf0 | ||
|
|
4a6aed5eab | ||
|
|
228cf4b9a2 | ||
|
|
f5dcb9bf31 | ||
|
|
b9f971c8fa | ||
|
|
65964f2819 | ||
|
|
40715999a5 | ||
|
|
12fd62163b | ||
|
|
748ff1381e | ||
|
|
026a1890b8 | ||
|
|
e14933f765 | ||
|
|
223daf2615 | ||
|
|
63c656f7ee | ||
|
|
cb876981ac | ||
|
|
bd639a5aff | ||
|
|
fcba75d191 | ||
|
|
33067508b1 | ||
|
|
d4a40d2b04 | ||
|
|
c063d77d2d | ||
|
|
ddb2edb2e9 | ||
|
|
fe14cf5d2c | ||
|
|
89db6cf43a | ||
|
|
a78ea81854 | ||
|
|
c00538364f | ||
|
|
cdfda0d4d1 | ||
|
|
e89cb6bf88 | ||
|
|
a50606fa26 | ||
|
|
cbd4fc13da | ||
|
|
927f30464e | ||
|
|
bf11a866e1 | ||
|
|
ca0688f086 | ||
|
|
8f87aa7440 | ||
|
|
b00db1d984 | ||
|
|
db2b48a3a0 | ||
|
|
e505fb840e | ||
|
|
7496794e58 | ||
|
|
b996c5d4ce | ||
|
|
eded59972e | ||
|
|
4fb773d8b2 | ||
|
|
9f47943e11 | ||
|
|
a1c26f9d98 | ||
|
|
6e64f6907a | ||
|
|
034cf04d47 | ||
|
|
841cd851ca | ||
|
|
59f8657566 | ||
|
|
7a1d539d7e | ||
|
|
03adf593d4 | ||
|
|
fd91245001 | ||
|
|
e6f6274e99 | ||
|
|
371f2fb385 | ||
|
|
0316732363 | ||
|
|
960ba0bb7f | ||
|
|
04c6db7de0 | ||
|
|
caa5dfafea | ||
|
|
8a3a0f33de | ||
|
|
0f5934cfaa | ||
|
|
bfd48c7740 | ||
|
|
92f3a55ee7 | ||
|
|
b19e739961 | ||
|
|
b24ade541a | ||
|
|
58d3ad6063 | ||
|
|
1ff643d72a | ||
|
|
7f2254ae57 | ||
|
|
5371ac689a | ||
|
|
675fcc4ea8 | ||
|
|
a20aa26024 | ||
|
|
06771d7197 | ||
|
|
efbb48c167 | ||
|
|
c03f5c8df1 | ||
|
|
865c90e627 | ||
|
|
2766609119 | ||
|
|
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:
|
||||
|
||||
@@ -17,4 +17,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/typos@v1.49.0
|
||||
- uses: crate-ci/typos@v1.51.1
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
@@ -9,8 +9,9 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
@@ -110,7 +111,7 @@ Browse the full Nuclei [**`documentation here`**](https://docs.projectdiscovery.
|
||||
|
||||
### Installation
|
||||
|
||||
`nuclei` requires **go >= 1.24.2** to install successfully. Run the following command to get the repo:
|
||||
`nuclei` requires **go >= 1.27.3** to install successfully. Run the following command to get the repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -145,6 +146,8 @@ TARGET:
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
@@ -173,6 +176,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -239,9 +250,11 @@ CONFIGURATIONS:
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -284,6 +297,8 @@ UNCOVER:
|
||||
RATE-LIMIT:
|
||||
-rl, -rate-limit int maximum number of requests to send per second (default 150)
|
||||
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
|
||||
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
|
||||
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
@@ -327,6 +342,7 @@ DEBUG:
|
||||
-dresp, -debug-resp show all received responses
|
||||
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
|
||||
-pi, -proxy-internal proxy all internal requests
|
||||
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
|
||||
-ldf, -list-dsl-function list all supported DSL function signatures
|
||||
-tlog, -trace-log string file to write sent requests trace log
|
||||
-elog, -error-log string file to write sent requests error log
|
||||
@@ -342,10 +358,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
@@ -667,7 +684,7 @@ Thanks to all the amazing [**`community contributors for sending PRs`**](https:/
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<sub>**`nuclei`** is distributed under [**MIT License**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md)</sub>
|
||||
|
||||
</div>
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ _针对安全团队和企业,我们提供了构建在 Nuclei OSS 之上的云
|
||||
|
||||
### 安装
|
||||
|
||||
`nuclei` 需要 **go >= 1.24.2** 才能成功安装。运行以下命令获取仓库:
|
||||
`nuclei` 需要 **go >= 1.27.3** 才能成功安装。运行以下命令获取仓库:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Consulta la [**`documentación completa de Nuclei aquí`**](https://docs.project
|
||||
|
||||
### Instalación
|
||||
|
||||
`nuclei` requiere **go >= 1.24.2** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
`nuclei` requiere **go >= 1.27.3** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+725
@@ -0,0 +1,725 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ID.md">`Indonesia`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://docs.projectdiscovery.io/tools/nuclei/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Documentation-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1ib29rLW9wZW4iPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjwvc3ZnPg==&logoColor=white" alt="Documentation Badge"></a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei-templates"><img src="https://img.shields.io/badge/Templates Library-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoaWVsZCI+PHBhdGggZD0iTTIwIDEzYzAgNS0zLjUgNy41LTcuNjYgOC45NWExIDEgMCAwIDEtLjY3LS4wMUM3LjUgMjAuNSA0IDE4IDQgMTNWNmExIDEgMCAwIDEgMS0xYzIgMCA0LjUtMS4yIDYuMjQtMi43MmExLjE3IDEuMTcgMCAwIDEgMS41MiAwQzE0LjUxIDMuODEgMTcgNSAxOSA1YTEgMSAwIDAgMSAxIDF6Ii8+PC9zdmc+&logoColor=white"></a>
|
||||
|
||||
<a href="https://discord.gg/projectdiscovery?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Discord Badge"></a>
|
||||
|
||||
|
||||
<hr>
|
||||
</p>
|
||||
|
||||
<div dir="rtl">
|
||||
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
**Nuclei یک اسکنر آسیبپذیری مدرن و با کارایی بالاست که از قالبهای ساده بر پایه YAML استفاده میکند. این ابزار به شما امکان میدهد سناریوهای سفارشی برای شناسایی آسیبپذیریها طراحی کنید که شرایط واقعی را شبیهسازی میکنند و منجر به صفر شدن خطاهای مثبت کاذب (گزارشهای اشتباه) میشوند.**
|
||||
|
||||
- فرمت ساده YAML برای ساخت و سفارشیسازی قالبهای آسیبپذیری.
|
||||
- مشارکت توسط هزاران متخصص امنیت جهت مقابله با آسیبپذیریهای نوظهور و رایج.
|
||||
- کاهش خطاهای مثبت کاذب با شبیهسازی مراحل واقعی جهت تایید یک آسیبپذیری.
|
||||
- پردازش موازی فوقسریع اسکنها و خوشهبندی درخواستها.
|
||||
- قابلیت ادغام در خطوط لوله CI/CD برای شناسایی آسیبپذیریها و تستهای رگرسیون.
|
||||
- پشتیبانی از پروتکلهای متعدد مانند TCP، DNS، HTTP، SSL، WHOIS، جاوااسکریپت، کد و موارد دیگر.
|
||||
- قابلیت یکپارچهسازی با Jira، Splunk، GitHub، Elastic و GitLab.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## فهرست مطالب
|
||||
|
||||
- [**`شروع کار`**](#شروع-کار)
|
||||
- [_`۱. ابزار خط فرمان Nuclei`_](#۱-ابزار-خط-فرمان-nuclei)
|
||||
- [_`۲. نسخههای حرفهای و سازمانی`_](#۲-نسخههای-حرفهای-و-سازمانی)
|
||||
- [**`مستندات`**](#مستندات)
|
||||
- [_`دستورات خط فرمان`_](#دستورات-خط-فرمان)
|
||||
- [_`اسکن یک هدف واحد`_](#اسکن-یک-هدف)
|
||||
- [_`اسکن اهداف متعدد`_](#اسکن-چندین-هدف)
|
||||
- [_`اسکن شبکه`_](#اسکن-شبکه)
|
||||
- [_`اسکن با قالب سفارشی شما`_](#اسکن-با-استفاده-از-قالب-سفارشی-خودتان)
|
||||
- [_`اتصال Nuclei به ProjectDiscovery`_](#اتصال-nuclei-به-projectdiscovery)
|
||||
- [**`قالبهای Nuclei، جامعه کاربری و پاداشها`**](#قالبهای-nuclei-جامعه-و-پاداشها-) 💎
|
||||
- [**`ماموریت ما`**](#ماموریت-ما)
|
||||
- [**`مشارکتکنندگان`**](#مشارکتکنندگان-) ❤
|
||||
- [**`مجوز استفاده`**](#license)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## شروع کار
|
||||
|
||||
### ۱. ابزار خط فرمان Nuclei
|
||||
|
||||
_ابزار Nuclei را روی سیستم خود نصب کنید. کار خود را با دنبال کردن راهنمای نصب در [**`اینجا`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) آغاز کنید. علاوه بر این، ما [**`یک سطح ابری رایگان`**](https://cloud.projectdiscovery.io/sign-up) ارائه میدهیم که همراه با محدودیتهای رایگان ماهانه سخاوتمندانهای عرضه میشود:_
|
||||
|
||||
- ذخیرهسازی و بصریسازی یافتههای آسیبپذیری شما
|
||||
- نوشتن و مدیریت قالبهای Nuclei شما
|
||||
- دسترسی به آخرین قالبهای Nuclei
|
||||
- شناسایی و ذخیرهسازی اهداف شما
|
||||
|
||||
> [!Important]
|
||||
> |**این پروژه در حال توسعه فعال است**. انتظار تغییرات اساسی در نسخههای جدید را داشته باشید. لطفاً قبل از بهروزرسانی، لیست تغییرات نسخه (Changelog) را بررسی کنید.|
|
||||
> |:--------------------------------|
|
||||
> | این پروژه در درجه اول برای استفاده به عنوان یک ابزار خط فرمان (CLI) مستقل ساخته شده است. **اجرای Nuclei به عنوان یک سرویس ممکن است خطرات امنیتی به همراه داشته باشد.** توصیه میشود با احتیاط و با در نظر گرفتن تدابیر امنیتی اضافی از آن استفاده کنید. |
|
||||
|
||||
<br>
|
||||
|
||||
### ۲. نسخههای حرفهای و سازمانی
|
||||
|
||||
_برای تیمهای امنیتی و سازمانها، ما یک سرویس میزبانیشده در ابر بر پایه نسخه متنباز (OSS) ابزار Nuclei ارائه میدهیم؛ این سرویس بهطور دقیق بهینهسازی شده است تا به شما کمک کند اسکنهای آسیبپذیری را بهصورت مداوم، در مقیاس بزرگ و همراه با تیم و گردشهای کاری فعلی خود اجرا کنید:_
|
||||
|
||||
- اسکنهای ۵۰ برابر سریعتر
|
||||
- اسکن در مقیاس بزرگ با دقت بسیار بالا
|
||||
- یکپارچهسازی با سرویسهای ابری (AWS، GCP، Azure، Cloudflare، Fastly، Terraform، Kubernetes)
|
||||
- اتصال به Jira، Slack، Linear، رابطهای برنامهنویسی (APIs) و وبهوکها (Webhooks)
|
||||
- گزارشدهیهای مدیریتی و سازگاری با استانداردهای امنیتی (Compliance)
|
||||
- بهعلاوه: اسکن بیدرنگ (Real-time)، پشتیبانی از SAML SSO، پلتفرم منطبق با استاندارد SOC 2 (همراه با گزینههای میزبانی در اتحادیه اروپا و آمریکا)، فضاهای کاری مشترک تیمی و موارد دیگر
|
||||
- ما بهطور مداوم در حال [**`افزودن قابلیتهای جدید`**](https://feedback.projectdiscovery.io/changelog) هستیم!
|
||||
- **ایدهآل برای:** نفوذگران قانونمند (Pentesters)، تیمهای امنیتی و سازمانها
|
||||
|
||||
در صورتی که سازمان بزرگی دارید و نیازمندیهای شما پیچیده است، میتوانید در [**`نسخه پرو ثبتنام کنید`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) یا [**`با تیم ما گفتگو کنید`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مستندات
|
||||
|
||||
[**`مستندات کامل`**](https://docs.projectdiscovery.io/tools/nuclei/running) ابزار Nuclei را در اینجا مرور کنید. اگر بهتازگی کار با Nuclei را شروع کردهاید، [**`مجموعه ویدیوهای آموزشی پایهای ما در یوتیوب`**](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl) را بررسی کنید.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=b5qMyQvL1ZA&list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-getting-started.png" width="350px" alt="Nuclei Getting Started Video Tutorial"></a> <a href="https://www.youtube.com/watch?v=nFXygQdtjyw&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-write-your-first-template.png" width="350px" alt="Writing Your First Nuclei Template Video Tutorial"></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
### نصب
|
||||
|
||||
برای نصب موفقیتآمیز `nuclei` به نسخه **go >= 1.24.2** نیاز است. دستور زیر را برای دریافت مخزن اجرا کنید:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
```
|
||||
</div>
|
||||
|
||||
برای اطلاعات بیشتر درباره نحوه نصب nuclei، به آدرس `https://docs.projectdiscovery.io/tools/nuclei/install` مراجعه کنید.
|
||||
|
||||
### دستورات خط فرمان
|
||||
|
||||
برای نمایش تمامی پارامترها و سوییچهای این ابزار:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -h
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>گسترش تمام فلگهای راهنما</summary>
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```yaml
|
||||
Nuclei is a fast, template based vulnerability scanner focusing
|
||||
on extensive configurability, massive extensibility and ease of use.
|
||||
|
||||
Usage:
|
||||
./nuclei [flags]
|
||||
|
||||
Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
-ntv, -new-templates-version string[] run new templates added in specific version
|
||||
-as, -automatic-scan automatic web scan using wappalyzer technology detection to tags mapping
|
||||
-t, -templates string[] list of template or template directory to run (comma-separated, file)
|
||||
-turl, -template-url string[] template url or list containing template urls to run (comma-separated, file)
|
||||
-ai, -prompt string generate and run template using ai prompt
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
-tgl list all available tags
|
||||
-sign signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable
|
||||
-code enable loading code protocol-based templates
|
||||
-dut, -disable-unsigned-templates disable running unsigned templates or templates with mismatched signature
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
-tags string[] templates to run based on tags (comma-separated, file)
|
||||
-etags, -exclude-tags string[] templates to exclude based on tags (comma-separated, file)
|
||||
-itags, -include-tags string[] tags to be executed even if they are excluded either by default or configuration
|
||||
-id, -template-id string[] templates to run based on template ids (comma-separated, file, allow-wildcard)
|
||||
-eid, -exclude-id string[] templates to exclude based on template ids (comma-separated, file)
|
||||
-it, -include-templates string[] path to template file or directory to be executed even if they are excluded either by default or configuration
|
||||
-et, -exclude-templates string[] path to template file or directory to exclude (comma-separated, file)
|
||||
-em, -exclude-matchers string[] template matchers to exclude in result
|
||||
-s, -severity value[] templates to run based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-es, -exclude-severity value[] templates to exclude based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-pt, -type value[] templates to run based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-ept, -exclude-type value[] templates to exclude based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-tc, -template-condition string[] templates to run based on expression condition
|
||||
|
||||
OUTPUT:
|
||||
-o, -output string output file to write found issues/vulnerabilities
|
||||
-sresp, -store-resp store all request/response passed through nuclei to output directory
|
||||
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
|
||||
-silent display findings only
|
||||
-nc, -no-color disable output content coloring (ANSI escape codes)
|
||||
-j, -jsonl write output in JSONL(ines) format
|
||||
-irr, -include-rr -omit-raw include request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only) [DEPRECATED use -omit-raw] (default true)
|
||||
-or, -omit-raw omit request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)
|
||||
-ot, -omit-template omit encoded template in the JSON, JSONL output
|
||||
-nm, -no-meta disable printing result metadata in cli output
|
||||
-ts, -timestamp enables printing timestamp in cli output
|
||||
-rdb, -report-db string nuclei reporting database (always use this to persist report data)
|
||||
-ms, -matcher-status display match failure status
|
||||
-me, -markdown-export string directory to export results in markdown format
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
-tp, -profile string template profile config file to run
|
||||
-tpl, -profile-list list community template profiles
|
||||
-fr, -follow-redirects enable following redirects for http templates
|
||||
-fhr, -follow-host-redirects follow redirects on the same host
|
||||
-mr, -max-redirects int max number of redirects to follow for http templates (default 10)
|
||||
-dr, -disable-redirects disable redirects for http templates
|
||||
-rc, -report-config string nuclei reporting module configuration file
|
||||
-H, -header string[] custom header/cookie to include in all http request in header:value format (cli, file)
|
||||
-V, -var value custom vars in key=value format
|
||||
-r, -resolvers string file containing resolver list for nuclei
|
||||
-sr, -system-resolvers use system DNS resolving as error fallback
|
||||
-dc, -disable-clustering disable clustering of requests
|
||||
-passive enable passive HTTP response processing mode
|
||||
-fh2, -force-http2 force http2 connection on requests
|
||||
-ev, -env-vars enable environment variables to be used in template
|
||||
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ck, -client-key string client key file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ca, -client-ca string client certificate authority file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-sml, -show-match-line show match lines for file templates, works with extractors only
|
||||
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
|
||||
-sni string tls sni hostname to use (default: input domain name)
|
||||
-dka, -dialer-keep-alive value keep-alive duration for network requests.
|
||||
-lfa, -allow-local-file-access allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
|
||||
INTERACTSH:
|
||||
-iserver, -interactsh-server string interactsh server url for self-hosted instance (default: oast.pro,oast.live,oast.site,oast.online,oast.fun,oast.me)
|
||||
-itoken, -interactsh-token string authentication token for self-hosted interactsh server
|
||||
-interactions-cache-size int number of requests to keep in the interactions cache (default 5000)
|
||||
-interactions-eviction int number of seconds to wait before evicting requests from cache (default 60)
|
||||
-interactions-poll-duration int number of seconds to wait before each interaction poll request (default 5)
|
||||
-interactions-cooldown-period int extra time for interaction polling before exiting (default 5)
|
||||
-ni, -no-interactsh disable interactsh server for OAST testing, exclude OAST based templates
|
||||
|
||||
FUZZING:
|
||||
-ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix)
|
||||
-fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single)
|
||||
-fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
|
||||
-dast enable / run dast (fuzz) nuclei templates
|
||||
-dts, -dast-server enable dast server mode (live fuzzing)
|
||||
-dtr, -dast-report write dast scan report to file
|
||||
-dtst, -dast-server-token string dast server token (optional)
|
||||
-dtsa, -dast-server-address string dast server address (default "localhost:9055")
|
||||
-dtp, -dast-proxy enable dast proxy mode (fuzz live traffic proxied through nuclei)
|
||||
-dtpa, -dast-proxy-address string dast proxy listen address (default "127.0.0.1:9056")
|
||||
-dtpau, -dast-proxy-auth string dast proxy basic auth in user:pass format (required for non-loopback address)
|
||||
-dfp, -display-fuzz-points display fuzz points in the output for debugging
|
||||
-fuzz-param-frequency int frequency of uninteresting parameters for fuzzing before skipping (default 10)
|
||||
-fa, -fuzz-aggression string fuzzing aggression level controls payload count for fuzz (low, medium, high) (default "low")
|
||||
-cs, -fuzz-scope string[] in scope url regex to be followed by fuzzer
|
||||
-cos, -fuzz-out-scope string[] out of scope url regex to be excluded by fuzzer
|
||||
|
||||
UNCOVER:
|
||||
-uc, -uncover enable uncover engine
|
||||
-uq, -uncover-query string[] uncover search query
|
||||
-ue, -uncover-engine string[] uncover search engine (shodan,censys,fofa,shodan-idb,quake,hunter,zoomeye,netlas,criminalip,publicwww,hunterhow,google) (default shodan)
|
||||
-uf, -uncover-field string uncover fields to return (ip,port,host) (default "ip:port")
|
||||
-ul, -uncover-limit int uncover results to return (default 100)
|
||||
-ur, -uncover-ratelimit int override ratelimit of engines with unknown ratelimit (default 60 req/min) (default 60)
|
||||
|
||||
RATE-LIMIT:
|
||||
-rl, -rate-limit int maximum number of requests to send per second (default 150)
|
||||
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
|
||||
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
|
||||
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
-hbs, -headless-bulk-size int maximum number of headless hosts to be analyzed in parallel per template (default 10)
|
||||
-headc, -headless-concurrency int maximum number of headless templates to be executed in parallel (default 10)
|
||||
-jsc, -js-concurrency int maximum number of javascript runtimes to be executed in parallel (default 120)
|
||||
-pc, -payload-concurrency int max payload concurrency for each template (default 25)
|
||||
-prc, -probe-concurrency int http probe concurrency with httpx (default 50)
|
||||
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int time to wait in seconds before timeout (default 10)
|
||||
-retries int number of times to retry a failed request (default 1)
|
||||
-ldp, -leave-default-ports leave default HTTP/HTTPS ports (eg. host:80,host:443)
|
||||
-mhe, -max-host-error int max errors for a host before skipping from scan (default 30)
|
||||
-te, -track-error string[] adds given error to max-host-error watchlist (standard, file)
|
||||
-nmhe, -no-mhe disable skipping host from scan based on errors
|
||||
-project use a project folder to avoid sending same request multiple times
|
||||
-project-path string set a specific project path (default "/tmp")
|
||||
-spm, -stop-at-first-match stop processing HTTP requests after the first match (may break template/workflow logic)
|
||||
-stream stream mode - start elaborating without sorting the input
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
|
||||
-page-timeout int seconds to wait for each page in headless mode (default 20)
|
||||
-sb, -show-browser show the browser on the screen when running templates with headless mode
|
||||
-ho, -headless-options string[] start headless chrome with additional options
|
||||
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
|
||||
-cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
|
||||
-lha, -list-headless-action list available headless actions
|
||||
|
||||
DEBUG:
|
||||
-debug show all requests and responses
|
||||
-dreq, -debug-req show all sent requests
|
||||
-dresp, -debug-resp show all received responses
|
||||
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
|
||||
-pi, -proxy-internal proxy all internal requests
|
||||
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
|
||||
-ldf, -list-dsl-function list all supported DSL function signatures
|
||||
-tlog, -trace-log string file to write sent requests trace log
|
||||
-elog, -error-log string file to write sent requests error log
|
||||
-version show nuclei version
|
||||
-hm, -hang-monitor enable nuclei hang monitoring
|
||||
-v, -verbose show verbose output
|
||||
-profile-mem string generate memory (heap) profile & trace files
|
||||
-vv display templates loaded for scan
|
||||
-svd, -show-var-dump show variables dump for debugging
|
||||
-vdl, -var-dump-limit int limit the number of characters displayed in var dump (default 255)
|
||||
-ep, -enable-pprof enable pprof debugging server
|
||||
-tv, -templates-version shows the version of the installed nuclei-templates
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
|
||||
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
|
||||
|
||||
STATISTICS:
|
||||
-stats display statistics about the running scan
|
||||
-sj, -stats-json display statistics in JSONL(ines) format
|
||||
-si, -stats-interval int number of seconds to wait between showing a statistics update (default 5)
|
||||
-mp, -metrics-port int port to expose nuclei metrics on (default 9092)
|
||||
-hps, -http-stats enable http status capturing (experimental)
|
||||
|
||||
CLOUD:
|
||||
-auth configure projectdiscovery cloud (pdcp) api key (default true)
|
||||
-tid, -team-id string upload scan results to given team id (optional) (default "none")
|
||||
-cup, -cloud-upload upload scan results to pdcp dashboard [DEPRECATED use -dashboard]
|
||||
-sid, -scan-id string upload scan results to existing scan id (optional)
|
||||
-sname, -scan-name string scan name to set (optional)
|
||||
-pd, -dashboard upload / view nuclei results in projectdiscovery cloud (pdcp) UI dashboard
|
||||
-pdu, -dashboard-upload string upload / view nuclei results file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard
|
||||
|
||||
AUTHENTICATION:
|
||||
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
|
||||
-ps, -prefetch-secrets prefetch secrets from the secrets file
|
||||
# NOTE: Headers in secrets files preserve exact casing (useful for case-sensitive APIs)
|
||||
|
||||
|
||||
EXAMPLES:
|
||||
Run nuclei on single host:
|
||||
$ nuclei -target example.com
|
||||
|
||||
Run nuclei with specific template directories:
|
||||
$ nuclei -target example.com -t http/cves/ -t ssl
|
||||
|
||||
Run nuclei against a list of hosts:
|
||||
$ nuclei -list hosts.txt
|
||||
|
||||
Run nuclei with a JSON output:
|
||||
$ nuclei -target example.com -json-export output.json
|
||||
|
||||
Run nuclei with sorted Markdown outputs (with environment variables):
|
||||
$ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
|
||||
|
||||
Additional documentation is available at: https://docs.projectdiscovery.io/getting-started/running
|
||||
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
مستندات تکمیلی در این آدرس در دسترس است: [**`docs.projectdiscovery.io/getting-started/running`**](https://docs.projectdiscovery.io/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
|
||||
|
||||
</details>
|
||||
|
||||
### اسکن یک هدف
|
||||
|
||||
برای انجام یک اسکن سریع روی یک وباپلیکیشن:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
### اسکن چندین هدف
|
||||
|
||||
Nuclei میتواند با ارائهٔ فهرستی از اهداف، اسکن گروهی را انجام دهد. میتوانید از فایلی استفاده کنید که شامل چندین URL است.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -list urls.txt
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن شبکه
|
||||
|
||||
این کار کل زیرشبکه را برای مشکلات مرتبط با شبکه، مانند پورتهای باز یا سرویسهای پیکربندی نادرست، اسکن میکند.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target 192.168.1.0/24
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن با استفاده از قالب سفارشی خودتان
|
||||
|
||||
برای نوشتن و استفاده از قالب خودتان، یک فایل `.yaml` با قوانین مشخص ایجاد کنید، سپس آن را بهصورت زیر استفاده کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -u https://example.com -t /path/to/your-template.yaml
|
||||
```
|
||||
</div>
|
||||
|
||||
### اتصال Nuclei به ProjectDiscovery
|
||||
|
||||
میتوانید اسکنها را روی دستگاه خود اجرا کرده و نتایج را برای تحلیل و رفع مشکلات بیشتر، در پلتفرم ابری بارگذاری کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com -dashboard
|
||||
```
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> این قابلیت کاملاً رایگان است و به هیچگونه اشتراکی نیاز ندارد. برای راهنمایی دقیقتر، به [**`مستندات`**](https://docs.projectdiscovery.io/cloud/scanning/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) مراجعه کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## قالبهای Nuclei، جامعه و پاداشها 💎
|
||||
|
||||
[**قالبهای Nuclei**](https://github.com/projectdiscovery/nuclei-templates) بر اساس مفهوم فایلهای قالب مبتنی بر YAML طراحی شدهاند که نحوه ارسال و پردازش درخواستها را تعریف میکنند. این موضوع امکان توسعهپذیری آسان Nuclei را فراهم میکند. قالبها با استفاده از YAML نوشته میشوند که قالبی ساده و قابلخواندن برای انسان ارائه میدهد تا بتوان فرآیند اجرا را بهسرعت تعریف کرد.
|
||||
|
||||
**همین حالا بهصورت آنلاین با ویرایشگر رایگان قالبهای Nuclei مجهز به هوش مصنوعی ما امتحان کنید؛** [**`اینجا کلیک کنید`**](https://cloud.projectdiscovery.io/templates).
|
||||
|
||||
قالبهای Nuclei روشی ساده و یکپارچه برای شناسایی و گزارش آسیبپذیریها ارائه میدهند و جزئیات ضروری مانند سطح شدت و روشهای شناسایی را در بر میگیرند. این ابزار متنباز که توسط جامعه توسعه داده میشود، واکنش به تهدیدات را سرعت میبخشد و در دنیای امنیت سایبری بهطور گسترده شناخته شده است. قالبهای Nuclei بهطور فعال توسط هزاران پژوهشگر امنیتی در سراسر جهان توسعه داده میشوند. ما دو برنامه برای مشارکتکنندگان خود داریم: [**`Pioneers`**](https://projectdiscovery.io/pioneers) و [**`💎 پاداشها`**](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
|
||||
|
||||
<p align="right">
|
||||
<a href="/static/nuclei-templates-teamcity.png" target="_blank"><img src="/static/nuclei-templates-teamcity.png" width="1200px" alt="نمونه قالب Nuclei برای شناسایی پیکربندی نادرست TeamCity" /></a>
|
||||
</p>
|
||||
|
||||
### مثالها
|
||||
|
||||
برای مشاهده موارد استفاده و ایدهها، به [**مستندات ما**](https://docs.projectdiscovery.io/templates/introduction) مراجعه کنید.
|
||||
|
||||
| قالب Nuclei | مورد استفاده |
|
||||
| ---: | ---: |
|
||||
| **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** | شناسایی CVEهای شناختهشده |
|
||||
| **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** | شناسایی آسیبپذیریهای Out-of-Band |
|
||||
| **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** | شناسایی SQL Injection |
|
||||
| **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** | Cross-Site Scripting (XSS) |
|
||||
| **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** | رمزهای عبور پیشفرض یا ضعیف |
|
||||
| **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** | فایلهای محرمانه یا افشای دادهها |
|
||||
| **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** | شناسایی Open Redirect |
|
||||
| **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** | شناسایی Subdomain Takeover |
|
||||
| **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** | پیکربندیهای نادرست امنیتی |
|
||||
| **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** | پیکربندیهای ضعیف SSL/TLS |
|
||||
| **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** | سرویسهای ابری با پیکربندی نادرست |
|
||||
| **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** | آسیبپذیریهای اجرای کد از راه دور |
|
||||
| **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** | حملات Directory Traversal |
|
||||
| **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** | آسیبپذیریهای File Inclusion |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## ماموریت ما
|
||||
|
||||
اسکنرهای سنتی آسیبپذیری دههها پیش ساخته شدهاند. آنها متنبسته، فوقالعاده کند و وابسته به فروشندگان هستند. مهاجمان امروزی، برخلاف گذشته که بهرهبرداری از آسیبپذیریها سالها طول میکشید، ظرف چند روز پس از انتشار CVEهای جدید، آنها را در سراسر اینترنت بهصورت گسترده مورد سوءاستفاده قرار میدهند. این تغییر، نیازمند رویکردی کاملاً متفاوت برای مقابله با اکسپلویتهای پرطرفدار در اینترنت است.
|
||||
|
||||
ما Nuclei را برای حل این چالش ساختیم. ما کل چارچوب موتور اسکن را متنباز و قابل سفارشیسازی کردیم تا جامعه امنیتی جهانی بتواند با یکدیگر همکاری کرده و با بردارهای حمله و آسیبپذیریهای پرطرفدار در اینترنت مقابله کند. امروزه Nuclei توسط شرکتهای Fortune 500، سازمانهای دولتی و دانشگاهها استفاده و توسعه داده میشود.
|
||||
|
||||
شما نیز میتوانید با مشارکت در کد ما، [**`کتابخانه قالبها`**](https://github.com/projectdiscovery/nuclei-templates)، یا [**`پیوستن به تیم ما`**](https://projectdiscovery.io/) مشارکت کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مشارکتکنندگان :heart:
|
||||
|
||||
از تمام مشارکتکنندگان فوقالعاده [**`جامعه که با ارسال PRها`**](https://github.com/projectdiscovery/nuclei/graphs/contributors) و بهروز نگهداشتن این پروژه از آن حمایت میکنند، سپاسگزاریم. :heart:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/Ice3man543"><img src="https://avatars.githubusercontent.com/u/22318055?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehsandeep"><img src="https://avatars.githubusercontent.com/u/8293321?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Mzack9999"><img src="https://avatars.githubusercontent.com/u/13421144?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/forgedhallpass"><img src="https://avatars.githubusercontent.com/u/13679401?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tarunKoyalwar"><img src="https://avatars.githubusercontent.com/u/45962551?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/manuelbua"><img src="https://avatars.githubusercontent.com/u/819314?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/actions-user"><img src="https://avatars.githubusercontent.com/u/65916846?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dogancanbakir"><img src="https://avatars.githubusercontent.com/u/65292895?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parrasajad"><img src="https://avatars.githubusercontent.com/u/16835787?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vzamanillo"><img src="https://avatars.githubusercontent.com/u/10209695?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShubhamRasal"><img src="https://avatars.githubusercontent.com/u/45902122?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/RamanaReddy0M"><img src="https://avatars.githubusercontent.com/u/90540245?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LuitelSamikshya"><img src="https://avatars.githubusercontent.com/u/85764322?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kchason"><img src="https://avatars.githubusercontent.com/u/1111099?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/pmareke"><img src="https://avatars.githubusercontent.com/u/3502075?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dwisiswant0"><img src="https://avatars.githubusercontent.com/u/25837540?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xm1k3"><img src="https://avatars.githubusercontent.com/u/73166077?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/5amu"><img src="https://avatars.githubusercontent.com/u/39925709?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehrishirajsharma"><img src="https://avatars.githubusercontent.com/u/35542790?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zerodivisi0n"><img src="https://avatars.githubusercontent.com/u/687694?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/geeknik"><img src="https://avatars.githubusercontent.com/u/466878?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/TerminalFi"><img src="https://avatars.githubusercontent.com/u/32599364?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KaulSe"><img src="https://avatars.githubusercontent.com/u/45340011?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sullo"><img src="https://avatars.githubusercontent.com/u/1474884?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wdahlenburg"><img src="https://avatars.githubusercontent.com/u/4451504?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ghost"><img src="https://avatars.githubusercontent.com/u/10137?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Nishan8583"><img src="https://avatars.githubusercontent.com/u/20457968?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jdk2588"><img src="https://avatars.githubusercontent.com/u/985054?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nothinux"><img src="https://avatars.githubusercontent.com/u/17433202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CodFrm"><img src="https://avatars.githubusercontent.com/u/22783163?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CasperGN"><img src="https://avatars.githubusercontent.com/u/5549643?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ankh2054"><img src="https://avatars.githubusercontent.com/u/6784287?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revblock"><img src="https://avatars.githubusercontent.com/u/72813848?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/cn-kali-team"><img src="https://avatars.githubusercontent.com/u/30642514?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/EndPositive"><img src="https://avatars.githubusercontent.com/u/25148195?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jimen0"><img src="https://avatars.githubusercontent.com/u/6826244?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xstevens"><img src="https://avatars.githubusercontent.com/u/69216?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mjkim610"><img src="https://avatars.githubusercontent.com/u/17107206?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/organiccrap"><img src="https://avatars.githubusercontent.com/u/376317?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lu4nx"><img src="https://avatars.githubusercontent.com/u/3006875?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/souvikhazra1"><img src="https://avatars.githubusercontent.com/u/13842393?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tovask"><img src="https://avatars.githubusercontent.com/u/22732484?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Marmelatze"><img src="https://avatars.githubusercontent.com/u/199681?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/doug-threatmate"><img src="https://avatars.githubusercontent.com/u/127235272?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/yabeow"><img src="https://avatars.githubusercontent.com/u/21117771?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/olearycrew"><img src="https://avatars.githubusercontent.com/u/6044920?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gano3s"><img src="https://avatars.githubusercontent.com/u/2551605?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alizmhdi"><img src="https://avatars.githubusercontent.com/u/79321261?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hackerpain"><img src="https://avatars.githubusercontent.com/u/61242234?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lc"><img src="https://avatars.githubusercontent.com/u/19563282?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/savushkin-yauheni"><img src="https://avatars.githubusercontent.com/u/5173352?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/74616e696d"><img src="https://avatars.githubusercontent.com/u/97121933?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/edoardottt"><img src="https://avatars.githubusercontent.com/u/35783570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zt2"><img src="https://avatars.githubusercontent.com/u/7644862?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/moonD4rk"><img src="https://avatars.githubusercontent.com/u/24284231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wk8"><img src="https://avatars.githubusercontent.com/u/2536231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mikerott"><img src="https://avatars.githubusercontent.com/u/857712?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/toufik-airane"><img src="https://avatars.githubusercontent.com/u/5610269?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hktalent"><img src="https://avatars.githubusercontent.com/u/18223385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jturner"><img src="https://avatars.githubusercontent.com/u/1825202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gaby"><img src="https://avatars.githubusercontent.com/u/835733?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vavkamil"><img src="https://avatars.githubusercontent.com/u/47953210?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leonjza"><img src="https://avatars.githubusercontent.com/u/1148127?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mionskowski-form3"><img src="https://avatars.githubusercontent.com/u/91873652?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chenrui333"><img src="https://avatars.githubusercontent.com/u/1580956?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamargus95"><img src="https://avatars.githubusercontent.com/u/77744293?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shashikarsiddharth"><img src="https://avatars.githubusercontent.com/u/60960197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/trypa11"><img src="https://avatars.githubusercontent.com/u/67585616?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Zeokat"><img src="https://avatars.githubusercontent.com/u/1313154?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alban-stourbe-wmx"><img src="https://avatars.githubusercontent.com/u/159776828?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/anykno"><img src="https://avatars.githubusercontent.com/u/2528207?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ronaudinho"><img src="https://avatars.githubusercontent.com/u/10264710?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/boy-hack"><img src="https://avatars.githubusercontent.com/u/18695984?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iuliu8899"><img src="https://avatars.githubusercontent.com/u/31680027?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/debasishbsws"><img src="https://avatars.githubusercontent.com/u/65381620?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denysvitali-niantic"><img src="https://avatars.githubusercontent.com/u/157139422?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fail-open"><img src="https://avatars.githubusercontent.com/u/72417455?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Xc1Ym"><img src="https://avatars.githubusercontent.com/u/29765332?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/XTeam-Wing"><img src="https://avatars.githubusercontent.com/u/25416365?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Weltolk"><img src="https://avatars.githubusercontent.com/u/40228052?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tonghuaroot"><img src="https://avatars.githubusercontent.com/u/23011166?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/praetorian-thendrickson"><img src="https://avatars.githubusercontent.com/u/69640071?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/S0obi"><img src="https://avatars.githubusercontent.com/u/4180104?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skahn007gl"><img src="https://avatars.githubusercontent.com/u/144735608?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shouichi"><img src="https://avatars.githubusercontent.com/u/99586?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seb-elttam"><img src="https://avatars.githubusercontent.com/u/111818823?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AdallomRoy"><img src="https://avatars.githubusercontent.com/u/4046118?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rotemreiss"><img src="https://avatars.githubusercontent.com/u/9288082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/oscarintherocks"><img src="https://avatars.githubusercontent.com/u/1785821?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xxcdd"><img src="https://avatars.githubusercontent.com/u/42600601?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chen2aaron"><img src="https://avatars.githubusercontent.com/u/9978183?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/voidz0r"><img src="https://avatars.githubusercontent.com/u/1032286?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vince-isec"><img src="https://avatars.githubusercontent.com/u/149686094?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/true13"><img src="https://avatars.githubusercontent.com/u/18207552?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skhalsa-sigsci"><img src="https://avatars.githubusercontent.com/u/68570441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShuBo6"><img src="https://avatars.githubusercontent.com/u/41125338?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seeyarh"><img src="https://avatars.githubusercontent.com/u/16869800?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/securibee"><img src="https://avatars.githubusercontent.com/u/51520913?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sduc"><img src="https://avatars.githubusercontent.com/u/2879617?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/scottdharvey"><img src="https://avatars.githubusercontent.com/u/25498254?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rykkard"><img src="https://avatars.githubusercontent.com/u/51889048?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/monitor403"><img src="https://avatars.githubusercontent.com/u/45124775?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mlec1"><img src="https://avatars.githubusercontent.com/u/42201667?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/meme-lord"><img src="https://avatars.githubusercontent.com/u/17912559?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LazyMaple"><img src="https://avatars.githubusercontent.com/u/12314941?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/llussy"><img src="https://avatars.githubusercontent.com/u/18432966?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/linchizhen"><img src="https://avatars.githubusercontent.com/u/170242051?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kiokuless"><img src="https://avatars.githubusercontent.com/u/110003596?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Jarnpher553"><img src="https://avatars.githubusercontent.com/u/10233873?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/c-f"><img src="https://avatars.githubusercontent.com/u/35263248?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hanghuge"><img src="https://avatars.githubusercontent.com/u/166206050?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/galoget"><img src="https://avatars.githubusercontent.com/u/8353133?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fudancoder"><img src="https://avatars.githubusercontent.com/u/171416994?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revolunet"><img src="https://avatars.githubusercontent.com/u/124937?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jsoref"><img src="https://avatars.githubusercontent.com/u/2119212?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MachadoOtto"><img src="https://avatars.githubusercontent.com/u/93268441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jonathanwalker"><img src="https://avatars.githubusercontent.com/u/14978093?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nHurD"><img src="https://avatars.githubusercontent.com/u/233374?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jessekelly881"><img src="https://avatars.githubusercontent.com/u/22938931?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/JaneX8"><img src="https://avatars.githubusercontent.com/u/5116641?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/atomiczsec"><img src="https://avatars.githubusercontent.com/u/75549184?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/M-Faheem-Khan"><img src="https://avatars.githubusercontent.com/u/17150767?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denandz"><img src="https://avatars.githubusercontent.com/u/5291556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tibbon"><img src="https://avatars.githubusercontent.com/u/82880?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dany74q"><img src="https://avatars.githubusercontent.com/u/2129762?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0x123456789"><img src="https://avatars.githubusercontent.com/u/36066426?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/danigoland"><img src="https://avatars.githubusercontent.com/u/15079567?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ChrisMandich"><img src="https://avatars.githubusercontent.com/u/14286797?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/austintraver"><img src="https://avatars.githubusercontent.com/u/25112463?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/socialsister"><img src="https://avatars.githubusercontent.com/u/155628741?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Anemys"><img src="https://avatars.githubusercontent.com/u/51196227?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/andreangelucci"><img src="https://avatars.githubusercontent.com/u/18552197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AlexS778"><img src="https://avatars.githubusercontent.com/u/98418121?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/akkuman"><img src="https://avatars.githubusercontent.com/u/7813511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zrquan"><img src="https://avatars.githubusercontent.com/u/33086594?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1efty"><img src="https://avatars.githubusercontent.com/u/18194777?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rsrdesarrollo"><img src="https://avatars.githubusercontent.com/u/5142014?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/razin99"><img src="https://avatars.githubusercontent.com/u/44442082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MetzinAround"><img src="https://avatars.githubusercontent.com/u/65838556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vil02"><img src="https://avatars.githubusercontent.com/u/65706193?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mrschyte"><img src="https://avatars.githubusercontent.com/u/8571?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/PeterDaveHello"><img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parthmalhotra"><img src="https://avatars.githubusercontent.com/u/28601533?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/owenrumney"><img src="https://avatars.githubusercontent.com/u/3049157?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Ovi3"><img src="https://avatars.githubusercontent.com/u/29408109?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Bisstocuz"><img src="https://avatars.githubusercontent.com/u/42398278?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/daffainfo"><img src="https://avatars.githubusercontent.com/u/36522826?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mhmdiaa"><img src="https://avatars.githubusercontent.com/u/19687798?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MiryangJung"><img src="https://avatars.githubusercontent.com/u/48237511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0xmin"><img src="https://avatars.githubusercontent.com/u/44919834?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/maikthulhu"><img src="https://avatars.githubusercontent.com/u/680830?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sttlr"><img src="https://avatars.githubusercontent.com/u/40246850?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamRjarpan"><img src="https://avatars.githubusercontent.com/u/45498226?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leoloobeek"><img src="https://avatars.githubusercontent.com/u/8801754?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KristinnVikar"><img src="https://avatars.githubusercontent.com/u/93918469?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kant01ne"><img src="https://avatars.githubusercontent.com/u/5072452?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KeisukeYamashita"><img src="https://avatars.githubusercontent.com/u/23056537?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1hehaq"><img src="https://avatars.githubusercontent.com/u/162917546?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub>**`nuclei`** تحت [**مجوز MIT**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md) منتشر شده است.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Telusuri [**`dokumentasi Nuclei lengkap di sini`**](https://docs.projectdiscover
|
||||
|
||||
### Instalasi
|
||||
|
||||
`nuclei` memerlukan **go >= 1.24.2** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
`nuclei` memerlukan **go >= 1.27.3** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei の完全な[**`ドキュメントはこちら`**](https://docs.projectdi
|
||||
|
||||
### インストール
|
||||
|
||||
`nuclei` のインストールには **go >= 1.24.2** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
`nuclei` のインストールには **go >= 1.27.3** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei 전체 [**`문서를 여기에서`**](https://docs.projectdiscovery.io/to
|
||||
|
||||
### 설치
|
||||
|
||||
`nuclei` 설치에는 **go >= 1.24.2** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
`nuclei` 설치에는 **go >= 1.27.3** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Acesse a [**`documentação completa do Nuclei aqui`**](https://docs.projectdisc
|
||||
|
||||
### Instalação
|
||||
|
||||
O `nuclei` requer **go >= 1.24.2** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
O `nuclei` requer **go >= 1.27.3** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+17
-6
@@ -9,7 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -110,7 +111,7 @@ Nuclei'nin tam [**`dokümantasyonuna buradan`**](https://docs.projectdiscovery.i
|
||||
|
||||
### Kurulum
|
||||
|
||||
`nuclei` yüklemek için **go >= 1.24.2** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
`nuclei` yüklemek için **go >= 1.27.3** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -173,6 +174,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -242,6 +251,7 @@ CONFIGURATIONS:
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -342,10 +352,11 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
@@ -1011,6 +1011,7 @@ Part Definitions:
|
||||
- <code>headers_from_response</code> - HTTP response headers in name:value format
|
||||
- <code>tls_version</code> - TLS version negotiated for the HTTP connection
|
||||
- <code>cipher</code> - TLS cipher suite negotiated for the HTTP connection
|
||||
- <code>key_exchange</code> - TLS key exchange group negotiated for the HTTP connection
|
||||
- <code>sni</code> - SNI value used in the TLS handshake
|
||||
- <code>subject_cn</code> - Leaf certificate subject common name
|
||||
- <code>subject_dn</code> - Leaf certificate subject distinguished name
|
||||
@@ -2475,6 +2476,136 @@ xpath:
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>prompt</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Prompt is the natural-language question for an llm matcher. The model
|
||||
judges the selected response part and returns a verdict; the matcher
|
||||
fires when the verdict equals Expect with at least MinConfidence.
|
||||
|
||||
{{...}} placeholders are interpolated from the template variables,
|
||||
-var and target. Response derived values are not interpolated, since
|
||||
the response is attacker controlled.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
prompt: Is this a working admin login form rather than a marketing page?
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>expect</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Expect is the verdict that counts as a match for an llm matcher.
|
||||
Defaults to "yes".
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>options</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Options restricts the model to a fixed set of verdicts for an llm
|
||||
matcher. Defaults to yes/no.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>min-confidence</code> <i>float64</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MinConfidence is the minimum confidence (0-1) an llm verdict needs to
|
||||
count as a match. Defaults to 0.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>inputs</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Inputs compares several responses instead of a single part, for
|
||||
checks that are about the difference between responses (blind
|
||||
boolean injection, user enumeration, IDOR). Each entry is resolved
|
||||
against the runtime values, so {{body_1}} and {{body_2}} work with
|
||||
req-condition, and each response is framed separately for the model.
|
||||
Part is ignored when Inputs is set.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
- '{{body_1}}'
|
||||
- '{{body_2}}'
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>max-input-tokens</code> <i>int</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MaxInputTokens caps how much of the response part is sent to the model
|
||||
for an llm matcher, as an approximate token count.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>allow-sole</code> <i>bool</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
AllowSole permits an llm matcher to be the only matcher on a high or
|
||||
critical template. Without it such a template is rejected, so a model is
|
||||
never the sole arbiter of a severe finding by accident.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -2600,6 +2731,8 @@ Enum Values:
|
||||
- <code>dsl</code>
|
||||
|
||||
- <code>xpath</code>
|
||||
|
||||
- <code>llm</code>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -4012,6 +4145,7 @@ Part Definitions:
|
||||
- <code>issuer_cn</code> - Issuer CN is the common name of the certificate issuer
|
||||
- <code>issuer_dn</code> - Issuer DN is the distinguished name of the certificate issuer
|
||||
- <code>issuer_org</code> - Issuer organization is the organization of the certificate issuer
|
||||
- <code>key_exchange</code> - Key exchange is the TLS key exchange group negotiated in the handshake
|
||||
- <code>not_after</code> - Timestamp after which the remote cert expires
|
||||
- <code>not_before</code> - Timestamp before which the certificate is not valid
|
||||
- <code>probe_status</code> - Probe status indicates if the probe was successful
|
||||
|
||||
@@ -32,6 +32,8 @@ AllowdTypes = "AllowdTypes"
|
||||
ExludedDastTmplStats = "ExludedDastTmplStats"
|
||||
|
||||
[default.extend-words]
|
||||
# SMTP greeting command
|
||||
HELO = "HELO"
|
||||
# External package API (fileutil.IsWriteable)
|
||||
Writeable = "Writeable" # TODO(dwisiswant0): Fix in upstream.
|
||||
# Variable name used for flow annotations
|
||||
|
||||
@@ -244,6 +244,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// readConfig defines the cli flags and loads them from the command line and
|
||||
// config files.
|
||||
func readConfig() *goflags.FlagSet {
|
||||
var selectedFiles configuration.Selection
|
||||
// when true updates nuclei binary to latest version
|
||||
@@ -273,6 +275,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
|
||||
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
|
||||
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("target-format", "Target-Format",
|
||||
@@ -304,6 +308,14 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableSelfContainedTemplates, "enable-self-contained", "esc", false, "enable loading self-contained templates"),
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
flagSet.IntVar(&options.LLMMaxCalls, "llm-max-calls", 5000, "maximum llm calls per scan (0 for unlimited)"),
|
||||
flagSet.IntVar(&options.LLMConcurrency, "llm-concurrency", 4, "maximum concurrent llm calls"),
|
||||
flagSet.BoolVar(&options.LLMCache, "llm-cache", true, "cache llm responses within a scan"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("filters", "Filtering",
|
||||
@@ -373,9 +385,11 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.RestrictLocalNetworkAccess, "restrict-local-network-access", "lna", false, "blocks connections to the local / private network"),
|
||||
flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"),
|
||||
flagSet.StringVarP(&options.AttackType, "attack-type", "at", "", "type of payload combinations to perform (batteringram,pitchfork,clusterbomb)"),
|
||||
flagSet.BoolVarP(&options.PayloadRandomOrder, "payload-random-order", "pro", false, "iterate payload combinations in a random order instead of sequentially"),
|
||||
flagSet.StringVarP(&options.SourceIP, "source-ip", "sip", "", "source ip address to use for network scan"),
|
||||
flagSet.IntVarP(&options.ResponseReadSize, "response-size-read", "rsr", 0, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.ResponseSaveSize, "response-size-save", "rss", unitutils.Mega, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.RequestSaveSize, "request-size-save", "rqss", 32*unitutils.Kilo, "max request body size to save in bytes (0 to save the whole body)"),
|
||||
flagSet.CallbackVar(func() { resetRequested = true }, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
|
||||
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
|
||||
@@ -422,6 +436,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("rate-limit", "Rate-Limit",
|
||||
flagSet.IntVarP(&options.RateLimit, "rate-limit", "rl", 150, "maximum number of requests to send per second"),
|
||||
flagSet.DurationVarP(&options.RateLimitDuration, "rate-limit-duration", "rld", time.Second, "maximum number of requests to send per second"),
|
||||
flagSet.BoolVar(&options.HostBackoff, "host-backoff", false, "slow down a host that starts blocking (429, repeated 403s, connection failures)"),
|
||||
flagSet.DurationVar(&options.HostBackoffMax, "host-backoff-max", 30*time.Second, "maximum delay applied to a host that is blocking"),
|
||||
flagSet.BoolVar(&options.PerHostRateLimit, "per-host-rate-limit", false, "enable per-host rate limiting (global rate limit becomes unlimited when enabled)"),
|
||||
flagSet.IntVarP(&options.RateLimitMinute, "rate-limit-minute", "rlm", 0, "maximum number of requests to send per minute (DEPRECATED)"),
|
||||
flagSet.IntVarP(&options.BulkSize, "bulk-size", "bs", 25, "maximum number of hosts to be analyzed in parallel per template"),
|
||||
@@ -472,6 +488,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.DebugResponse, "debug-resp", "dresp", false, "show all received responses"),
|
||||
flagSet.StringSliceVarP(&options.Proxy, "proxy", "p", nil, "list of http/socks5 proxy to use (comma separated or file input)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.ProxyInternal, "proxy-internal", "pi", false, "proxy all internal requests"),
|
||||
flagSet.StringVarP(&options.ReplayProxy, "replay-proxy", "rp", "", "http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)"),
|
||||
flagSet.BoolVarP(&options.ListDslSignatures, "list-dsl-function", "ldf", false, "list all supported DSL function signatures"),
|
||||
flagSet.StringVarP(&options.TraceLogFile, "trace-log", "tlog", "", "file to write sent requests trace log"),
|
||||
flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"),
|
||||
@@ -490,6 +507,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("update", "Update",
|
||||
flagSet.BoolVarP(&updateNucleiBinary, "update", "up", false, "update nuclei engine to the latest released version"),
|
||||
flagSet.BoolVarP(&options.UpdateTemplates, "update-templates", "ut", false, "update nuclei-templates to latest released version"),
|
||||
flagSet.StringVarP(&options.UpdateTemplatesChannel, "update-templates-channel", "utc", installer.TemplatesChannelStable, "nuclei-templates update channel (stable, latest)"),
|
||||
flagSet.StringVarP(&options.NewTemplatesDirectory, "update-template-dir", "ud", "", "custom directory to install / update nuclei-templates"),
|
||||
flagSet.BoolVarP(&disableUpdateCheck, "disable-update-check", "duc", false, "disable automatic nuclei/templates update check"),
|
||||
)
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/itchyny/gojq v0.12.17
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/fastdialer v0.5.20
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/interactsh v1.3.1
|
||||
github.com/projectdiscovery/rawhttp v0.1.92
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/weppos/publicsuffix-go v0.50.3 // indirect
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/text v0.41.0
|
||||
)
|
||||
go 1.27.3
|
||||
|
||||
require (
|
||||
carvel.dev/ytt v0.52.0
|
||||
@@ -48,11 +13,13 @@ require (
|
||||
github.com/FalconOpsLLC/goexec v0.3.0
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415
|
||||
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
github.com/alitto/pond v1.9.2
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/antchfx/xpath v1.3.6
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
@@ -71,19 +38,28 @@ require (
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/getkin/kin-openapi v0.144.0
|
||||
github.com/getsops/sops/v3 v3.9.2
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-pdf/fpdf v0.9.0
|
||||
github.com/go-pg/pg/v10 v10.15.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/go-github/v30 v30.1.0
|
||||
github.com/google/go-github/v92 v92.0.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/graphql-go/graphql v0.8.1
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/hdm/jarm-go v0.0.8
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/itchyny/gojq v0.12.17
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4
|
||||
github.com/jhump/protoreflect v1.17.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kitabisa/go-ci v1.0.3
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
|
||||
github.com/lib/pq v1.12.3
|
||||
@@ -92,45 +68,72 @@ require (
|
||||
github.com/maypok86/otter/v2 v2.2.1
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/miekg/dns v1.1.73
|
||||
github.com/oiweiwei/go-msrpc v1.2.12
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.8.24
|
||||
github.com/projectdiscovery/fastdialer v0.6.0
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/goflags v0.2.1
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
|
||||
github.com/projectdiscovery/gologger v1.1.73
|
||||
github.com/projectdiscovery/gostruct v0.0.2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/httpx v1.12.0
|
||||
github.com/projectdiscovery/interactsh v1.4.1
|
||||
github.com/projectdiscovery/mapcidr v1.1.97
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56
|
||||
github.com/projectdiscovery/ratelimit v0.0.94
|
||||
github.com/projectdiscovery/rawhttp v0.2.0
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31
|
||||
github.com/projectdiscovery/sarif v0.1.0
|
||||
github.com/projectdiscovery/tlsx v1.4.0
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sandrolain/httpcache v1.4.0
|
||||
github.com/segmentio/ksuid v1.0.4
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
|
||||
github.com/trivago/tgo v1.0.7
|
||||
github.com/yassinebenaid/godump v0.11.1
|
||||
github.com/zmap/zgrab2 v0.1.8
|
||||
gitlab.com/gitlab-org/api/client-go v1.9.1
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/term v0.45.0
|
||||
google.golang.org/grpc v1.83.1
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
moul.io/http2curl v1.0.0
|
||||
)
|
||||
@@ -149,7 +152,6 @@ require (
|
||||
cloud.google.com/go/storage v1.56.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 // indirect
|
||||
@@ -162,6 +164,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.13.0 // indirect
|
||||
@@ -204,8 +207,8 @@ require (
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.2 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.3 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/censys/censys-sdk-go v0.19.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -216,6 +219,7 @@ require (
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.5 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -227,7 +231,7 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
@@ -238,6 +242,8 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/emersion/go-smtp v0.24.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
@@ -247,7 +253,7 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gaissmai/bart v0.29.0 // indirect
|
||||
github.com/gaissmai/bart v0.30.0 // indirect
|
||||
github.com/geoffgarside/ber v1.2.0 // indirect
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
@@ -257,7 +263,7 @@ require (
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
@@ -267,6 +273,10 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goburrow/cache v0.1.4 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
@@ -275,6 +285,7 @@ require (
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||
@@ -296,7 +307,6 @@ require (
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.15.0 // indirect
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
|
||||
github.com/hdm/jarm-go v0.0.8 // indirect
|
||||
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/indece-official/go-ebcdic v1.2.0 // indirect
|
||||
@@ -308,6 +318,7 @@ require (
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
|
||||
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
|
||||
github.com/kataras/jwt v0.1.10 // indirect
|
||||
@@ -318,6 +329,7 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
@@ -326,7 +338,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.3 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
@@ -337,7 +349,7 @@ require (
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.54.2 // indirect
|
||||
github.com/moby/moby/client v0.4.1 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -363,22 +375,24 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/projectdiscovery/asnmap v1.1.1 // indirect
|
||||
github.com/projectdiscovery/blackrock v0.0.2 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.0 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.2 // indirect
|
||||
github.com/projectdiscovery/freeport v0.0.7 // indirect
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 // indirect
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
|
||||
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sashabaranov/go-openai v1.37.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||
@@ -404,6 +418,7 @@ require (
|
||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -415,68 +430,46 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.17 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zcalusic/sysinfo v1.1.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
google.golang.org/api v0.264.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/goburrow/cache v0.1.4 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/trivago/tgo v1.0.7
|
||||
github.com/ysmood/goob v0.4.0 // indirect
|
||||
github.com/ysmood/gson v0.7.3 // indirect
|
||||
github.com/ysmood/leakless v0.9.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/mod v0.39.0 // indirect
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
// https://go.dev/ref/mod#go-mod-file-retract
|
||||
retract v3.2.0 // retract due to broken js protocol issue
|
||||
@@ -64,6 +64,8 @@ cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34=
|
||||
code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@@ -71,8 +73,6 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
|
||||
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
@@ -124,13 +124,12 @@ github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb888350
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
|
||||
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 h1:lpSZPcbowbxvKFaFvE1reLTCStezWXcRVk0zzBtUatg=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d h1:xX93NZHxxrHcmN4nn1owvJdr1E/l1kp1Rv7rMr9ly+U=
|
||||
github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
|
||||
@@ -169,6 +168,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY=
|
||||
github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0 h1:nZAzCR+Lj+Vxk4ZXzm2NuKq2O33RXj1XxJ2e2uP9jiw=
|
||||
github.com/alicebob/miniredis/v2 v2.38.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
|
||||
github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
@@ -265,17 +266,16 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.3 h1:P3akjLPBtV/i6bHC6LbcLjY3KuoOvfiqF8wFHeP5IhY=
|
||||
github.com/bytedance/sonic v1.15.3/go.mod h1:8e51yTPdY8M6t+vvGL1c2Y1xL9i+frEeIAQAEl75NUc=
|
||||
github.com/bytedance/sonic/loader v0.5.2 h1:0QtP1gevc1OZ6/H8Lb9BRZiCXd1Ftjd3OKuj1T1lBIo=
|
||||
github.com/bytedance/sonic/loader v0.5.2/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
||||
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -310,17 +310,19 @@ github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0V
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
|
||||
github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
@@ -349,8 +351,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
@@ -382,6 +382,10 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
|
||||
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -419,8 +423,8 @@ github.com/fullstorydev/grpcurl v1.9.3 h1:PC1Xi3w+JAvEE2Tg2Gf2RfVgPbf9+tbuQr1Zky
|
||||
github.com/fullstorydev/grpcurl v1.9.3/go.mod h1:/b4Wxe8bG6ndAjlfSUjwseQReUDUvBJiFEB7UllOlUE=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gaissmai/bart v0.29.0 h1:wO6HGE8g9YE0Wm0bCpYxwRzfQ4+fbJKOhL64e5ACGCI=
|
||||
github.com/gaissmai/bart v0.29.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gaissmai/bart v0.30.0 h1:K1oTSQ6Nf76bhrgxeTqzZa4hE0M9x4xDfL6n6UG67Sc=
|
||||
github.com/gaissmai/bart v0.30.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
|
||||
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
|
||||
@@ -466,8 +470,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
@@ -585,9 +589,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
|
||||
github.com/google/go-github/v92 v92.0.0 h1:4vW4RVffwIvoEfIA4RX09mRSB47qQX557/2+HgeWRRg=
|
||||
github.com/google/go-github/v92 v92.0.0/go.mod h1:w3CH62ZcmRfvW1cdXpyTztSOVMtdjxtKpgo0GouLmjY=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
@@ -639,6 +642,8 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
|
||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -773,6 +778,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa h1:KQKuQDgA3DZX6C396lt3WDYB9Um1gLITLbvficVbqXk=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa/go.mod h1:HbwNE4XGwjgtUELkvQaAOjWrpianHYZdQVNqSdYW3UM=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
@@ -808,8 +817,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
|
||||
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
|
||||
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
|
||||
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -839,8 +848,8 @@ github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -922,14 +931,14 @@ github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kI
|
||||
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
|
||||
github.com/projectdiscovery/blackrock v0.0.2 h1:mxXdu0uM8P2L2Qi210COlU8QiICPFW/Rxk5QUhlPO2k=
|
||||
github.com/projectdiscovery/blackrock v0.0.2/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
|
||||
github.com/projectdiscovery/cdncheck v1.3.0 h1:/bWdNXIU1P5ASZuW8QA+tAw1FNHhmWlWXk/qiUHyx2c=
|
||||
github.com/projectdiscovery/cdncheck v1.3.0/go.mod h1:GIAA0SqjDfxzDQjHR/X+bK05BGmUGfsVplP5Zdv/kHA=
|
||||
github.com/projectdiscovery/cdncheck v1.3.2 h1:I453ZWNBeG0WWIy3wi9412l4PTzorLgebEnqmbcpSfo=
|
||||
github.com/projectdiscovery/cdncheck v1.3.2/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
|
||||
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
|
||||
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8 h1:zaAfFHjyLPq9Aoii1mXPzXkITvXyPJTSZo6nNhzC1xI=
|
||||
github.com/projectdiscovery/dsl v0.8.23-0.20260910174914-8e1207b367b8/go.mod h1:TYqDNtD4hU8bGFgVxPqhYQ9K1z5rMLV/YFyvQgcU5TM=
|
||||
github.com/projectdiscovery/fastdialer v0.5.20 h1:MlXoVXQERJY4ILqMNYfuoTKVR4Ix4XonJLkFaoHzgM0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.20/go.mod h1:zdtIbBt5ELNygMEPDSyQ9Nr9KpP9FuAvJwzNIrZZoJA=
|
||||
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
|
||||
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0 h1:4ZeFQ6LyRH4D8e3qO/yzqIP4Cf9Fr0DduaVpJD6ENnE=
|
||||
github.com/projectdiscovery/fastdialer v0.6.0/go.mod h1:FpHwa9S5PUgfPbh+irOm+cdU6AO631XidxkhPwPv7yY=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
|
||||
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
|
||||
@@ -938,8 +947,10 @@ github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c h1:s+lLAlr
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c/go.mod h1:rN35/D3lVx2YDeENFFz06uj8j3XIqK1Ym9XcISF5fzg=
|
||||
github.com/projectdiscovery/goflags v0.2.1 h1:XnSYvsCjUwHbZaaJUZ3YznCJPoXRk/IhTFP9Gecmx6M=
|
||||
github.com/projectdiscovery/goflags v0.2.1/go.mod h1:7nAP1r2Dqgn/rwmOE3EWbZWUCEJKNIhVSBGpuzJAIns=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534 h1:hYd1zQA/dxO2ASyQ6Re73TcJkW1LjLQvt4+86Hxefz8=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb h1:+L1yW2PblRHSSHQRTmvxLkGDPZnM52N62oeBd2xHKF0=
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb/go.mod h1:bfgULaYf+6pjjcLCekpXIYElnLJbmhvTttxMAbQJvhY=
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c h1:4H/CfVNlue9PIwsCM1IGxOynDI7cir+emXuDvGJTLJs=
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d h1:fqqH9LHpN2WDz9QuxFrhKNxXSRtzk+Sa6jAhbB7tXcQ=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d/go.mod h1:Ezmbgdaw4EunGGBU4MQViLoGMJc37LA3ip55YV3KeRI=
|
||||
github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwapyM7ukGU1jFY=
|
||||
@@ -954,8 +965,8 @@ github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP
|
||||
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
|
||||
github.com/projectdiscovery/httpx v1.12.0 h1:NN5iDrw7NjPBDlACdjac4lePTQmZ5aOmgOzptMpP44c=
|
||||
github.com/projectdiscovery/httpx v1.12.0/go.mod h1:OgtyibNGdu9BrJxRdNKm280Davw1Z4s2FAhfPNNtLB4=
|
||||
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
|
||||
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
|
||||
github.com/projectdiscovery/interactsh v1.4.1 h1:EGHPJ1rnuxvILWfcoaRgb/Ykz67WiDXABRCSJN666GA=
|
||||
github.com/projectdiscovery/interactsh v1.4.1/go.mod h1:5g207F6MvIydlKWo4ZzplXkhaCwSq2veuUTNvEFjqps=
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 h1:GUHQYLllWhoca+C4UAT0+TTB1PrUT05klj39V6/KpK8=
|
||||
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6/go.mod h1:f8P5X5+jEkUqK9z3oNWtSH1g23jMfw3nuEqmtA1jXA4=
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
|
||||
@@ -966,32 +977,32 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
|
||||
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50 h1:z/ZMuNAtPH6Q7BlMZ4DLA1ytm+7TYPAdBCNLjNqaV0U=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.50/go.mod h1:QAAMLSm1YsbGeZTFNGBMtyob28B1VUVblNT56wPhDJc=
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294 h1:d5fkf/st46mg+QpOkzOuFBBCYfV8ljO3pmi8CChwIyE=
|
||||
github.com/projectdiscovery/ratelimit v0.0.90-0.20260912164306-aeeca5645294/go.mod h1:pQg2urKomS0WkMmLCF5HhWyuresBXosh1kIfn1Tlwug=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92 h1:38GRsVLNllES/dZcEcA6s6P+rYBp34H9mDphMJMDvvg=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92/go.mod h1:spfXTPjxCj+bfWLynMVB7uOb+LS3PDNAI9HexMEV1Zg=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56 h1:M/L421HFuN707oX0tLTzKZY46uRyaKwxAeyi0q8PlxI=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56/go.mod h1:N5bXm8L3UsDw51CeA1nhTsGEbQMbgL5lXTiIU013xd0=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94 h1:2qciSuUxarn73YGICZeprZtjrwyAH+0iVpdKzKCXoXE=
|
||||
github.com/projectdiscovery/ratelimit v0.0.94/go.mod h1:GAerX6s+eWRuACedHRv2/xmMtlW+VahZmbRstQh0sS0=
|
||||
github.com/projectdiscovery/rawhttp v0.2.0 h1:TGiHC5hydL97/zoi0WjEuNrfO6Gjtg9LeXndUf1Cq3M=
|
||||
github.com/projectdiscovery/rawhttp v0.2.0/go.mod h1:Waz+K5y6r44ElpqAoOfYtY03zrGGZ12nx0yDKq7Ot1E=
|
||||
github.com/projectdiscovery/rdap v0.9.0 h1:wPhHx5pQ2QI+WGhyNb2PjhTl0NtB39Nk7YFZ9cp8ZGA=
|
||||
github.com/projectdiscovery/rdap v0.9.0/go.mod h1:zk4yrJFQ2Hy36Aqk+DvotYQxYAeALaCJ5ORySkff36Q=
|
||||
github.com/projectdiscovery/retryabledns v1.0.116 h1:tK28PJ4tLO7bgM89RpIFjkfTPMSbRegwvg9mN/q6z78=
|
||||
github.com/projectdiscovery/retryabledns v1.0.116/go.mod h1:r3vH0AKYtJ9fculcqRFrdVYe2ABcHwZeR4C6j3rqCbY=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26 h1:BkVqMUIVPhx+3vOoo5CxYGNnMqQQYXOedu89XQB1KTg=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.26/go.mod h1:poffUOX+jvS0XbaPCYess221jXvSO7R/QlE7zhs3+9Q=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31 h1:0hSEZ5+jj6Oo1IL8qrytD36TK/qCxfe543jEaRqSTjs=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31/go.mod h1:/WEa7YejxTCXEMLCGfGYaC3JsgXgzuqw8rv8cb4buEc=
|
||||
github.com/projectdiscovery/sarif v0.1.0 h1:O541T+a448nSJsmIMnXXSOeDQEzpnCAYvRfe0eG5h74=
|
||||
github.com/projectdiscovery/sarif v0.1.0/go.mod h1:LBC+reM3bkI3qIIhE0rZaINaYX6VG+En6u2hHa5mA7E=
|
||||
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
|
||||
github.com/projectdiscovery/stringsutil v0.0.2/go.mod h1:EJ3w6bC5fBYjVou6ryzodQq37D5c6qbAYQpGmAy+DC0=
|
||||
github.com/projectdiscovery/tlsx v1.4.0 h1:OU2mNK6TOB/YiFX3jhmgwOepFWxPoj5rn8npWiCCz74=
|
||||
github.com/projectdiscovery/tlsx v1.4.0/go.mod h1:WCrUXAHjdKbDNjsu5EbVBS7qvzap3fxi/E85v/Flyx8=
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75 h1:3qJgfWzzwtFXc/vdUIurSzPzyjBH9NQe0wATkZMoQGI=
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75/go.mod h1:8jqB9aHT6U8NkUsENvaSjD/iBEGB4Ta+CbJBMcvzP9c=
|
||||
github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLvh5+UjzATE=
|
||||
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
|
||||
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
|
||||
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668 h1:umwLd2H4q5hfHPdAHSQWdrmBvECZs2qsF9hskvo1jw8=
|
||||
github.com/projectdiscovery/utils v0.11.4-0.20260910160917-83d6b6e62668/go.mod h1:DzaUDLQ+Tqsn2nOL47M1mRBZSVyCmwsAlR2E9dxHB4Y=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0 h1:2AsEkjnKgFZK4tjaiNhwC0zfj04rpckSvIOZINeTGyQ=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.0/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec h1:yoAPKM61ofYFGaUUSqTCW4J0B4mHJoTg5FDv0ZrCDb4=
|
||||
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7/go.mod h1:nqD1iPMcofpRA1DbXavPByJhWURrshT+l8WN9XjyQag=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -1018,8 +1029,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
||||
@@ -1046,8 +1057,6 @@ github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQ
|
||||
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
|
||||
github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
@@ -1066,9 +1075,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
@@ -1174,9 +1182,8 @@ github.com/vulncheck-oss/go-exploit v1.51.0/go.mod h1:J28w0dLnA6DnCrnBm9Sbt6smX8
|
||||
github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
|
||||
github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko=
|
||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb h1:QIW0GHVTSubm3VxkKpn1FnFAgins6N8FiGTvUQwpXzw=
|
||||
github.com/weppos/publicsuffix-go v0.50.4-0.20260918082853-3c3d7dc4a0cb/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
@@ -1231,6 +1238,8 @@ github.com/yuin/goldmark v1.7.17 h1:p36OVWwRb246iHxA/U4p8OPEpOTESm4n+g+8t0EE5uA=
|
||||
github.com/yuin/goldmark v1.7.17/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
|
||||
@@ -1241,16 +1250,17 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30=
|
||||
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
|
||||
github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk=
|
||||
github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
|
||||
github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
|
||||
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w=
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 h1:DCz0McWRVJNICkHdu2XpETqeLvPtZXs315OZyUs1BDk=
|
||||
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77/go.mod h1:aSvf+uTU222mUYq/KQj3oiEU7ajhCZe8RRSLHIoM4EM=
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 h1:B9sBGvtHjiGOOemAh0cCGTzub76DXlxmOh3Qtx3TkRs=
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87/go.mod h1:9O2aSz1l9fUHTFxJMYCoFsZBy6yGlz3nrOp4dHue5qw=
|
||||
github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo=
|
||||
github.com/zmap/zgrab2 v0.1.8 h1:PFnXrIBcGjYFec1JNbxMKQuSXXzS+SbqE89luuF4ORY=
|
||||
github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFnegE=
|
||||
@@ -1274,26 +1284,28 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
@@ -1326,10 +1338,9 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1367,8 +1378,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1416,10 +1427,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1428,7 +1438,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1447,8 +1456,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1495,7 +1504,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1513,10 +1521,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1528,10 +1535,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1547,10 +1553,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1682,8 +1687,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1699,7 +1704,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module nuclei-tools
|
||||
|
||||
go 1.26.1
|
||||
go 1.27.1
|
||||
|
||||
tool (
|
||||
github.com/dvyukov/go-fuzz/go-fuzz
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/httpx/common/httpx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
@@ -68,12 +69,15 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
|
||||
}
|
||||
|
||||
swg.Add()
|
||||
go func(input *contextargs.MetaInput) {
|
||||
go func(target *contextargs.MetaInput) {
|
||||
defer swg.Done()
|
||||
|
||||
if result := utils.ProbeURL(input.Input, httpxClient); result != "" {
|
||||
// a pinned target is probed through its ip, and each ip keeps its
|
||||
// own result, as servers of one host may serve different schemes
|
||||
ctx := contextargs.WithCustomIP(context.Background(), target)
|
||||
if result := utils.ProbeURLWithContext(ctx, target.Input, httpxClient); result != "" {
|
||||
count.Add(1)
|
||||
_ = hm.Set(input.Input, []byte(result))
|
||||
_ = hm.Set(input.ProbeKey(target.Input, target.CustomIP), []byte(result))
|
||||
}
|
||||
}(value)
|
||||
return true
|
||||
|
||||
+24
-19
@@ -39,25 +39,30 @@ func GetAuthTmplStore(opts *types.Options, catalog catalog.Catalog, execOpts *pr
|
||||
}
|
||||
tmpls = append(tmpls, data...)
|
||||
}
|
||||
opts.Templates = tmpls
|
||||
opts.Workflows = nil
|
||||
opts.RemoteTemplateDomainList = nil
|
||||
opts.TemplateURLs = nil
|
||||
opts.WorkflowURLs = nil
|
||||
opts.ExcludedTemplates = nil
|
||||
opts.Tags = nil
|
||||
opts.ExcludeTags = nil
|
||||
opts.IncludeTemplates = nil
|
||||
opts.Authors = nil
|
||||
opts.Severities = nil
|
||||
opts.ExcludeSeverities = nil
|
||||
opts.IncludeTags = nil
|
||||
opts.IncludeIds = nil
|
||||
opts.ExcludeIds = nil
|
||||
opts.Protocols = nil
|
||||
opts.ExcludeProtocols = nil
|
||||
opts.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(opts, catalog, execOpts)
|
||||
// The auth store only wants the secret-file templates. Copy first so
|
||||
// this scoping cannot clear Templates, ExcludeTags, or the other filters
|
||||
// on the caller's options. The CLI already copies; the SDK passes the
|
||||
// engine options directly.
|
||||
scoped := opts.Copy()
|
||||
scoped.Templates = tmpls
|
||||
scoped.Workflows = nil
|
||||
scoped.RemoteTemplateDomainList = nil
|
||||
scoped.TemplateURLs = nil
|
||||
scoped.WorkflowURLs = nil
|
||||
scoped.ExcludedTemplates = nil
|
||||
scoped.Tags = nil
|
||||
scoped.ExcludeTags = nil
|
||||
scoped.IncludeTemplates = nil
|
||||
scoped.Authors = nil
|
||||
scoped.Severities = nil
|
||||
scoped.ExcludeSeverities = nil
|
||||
scoped.IncludeTags = nil
|
||||
scoped.IncludeIds = nil
|
||||
scoped.ExcludeIds = nil
|
||||
scoped.Protocols = nil
|
||||
scoped.ExcludeProtocols = nil
|
||||
scoped.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(scoped, catalog, execOpts)
|
||||
cfg.StoreId = loader.AuthStoreId
|
||||
store, err := loader.New(cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
utilsllm "github.com/projectdiscovery/utils/llm"
|
||||
)
|
||||
|
||||
// llmMatcherClient adapts the shared provider layer to the minimal interface
|
||||
// the matchers package needs, keeping matchers free of any provider SDK import.
|
||||
type llmMatcherClient struct {
|
||||
client *utilsllm.Client
|
||||
model string
|
||||
}
|
||||
|
||||
// Model names the model behind a verdict so it can be recorded in the finding.
|
||||
func (a *llmMatcherClient) Model() string {
|
||||
return a.model
|
||||
}
|
||||
|
||||
func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON bool) (string, error) {
|
||||
return a.client.Complete(ctx, utilsllm.Request{
|
||||
Prompt: prompt,
|
||||
Format: utilsllm.Format{JSON: asJSON},
|
||||
})
|
||||
}
|
||||
|
||||
// configureLLM builds the scan's llm client from options. It returns nil unless
|
||||
// -llm is set, so nothing reaches a model until the user opts in. The API key is
|
||||
// read from the environment by the provider layer, never from options.
|
||||
//
|
||||
// The client is returned rather than installed globally so two scans in one
|
||||
// process cannot overwrite each other's provider, cache, budget, or timeout.
|
||||
func configureLLM(options *types.Options) (llmclient.Client, error) {
|
||||
if !options.EnableLLM {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
client, err := utilsllm.New(utilsllm.Config{
|
||||
Provider: options.LLMProvider,
|
||||
BaseURL: options.LLMBaseURL,
|
||||
Model: options.LLMModel,
|
||||
Timeout: time.Duration(options.LLMTimeout) * time.Second,
|
||||
Cache: options.LLMCache,
|
||||
MaxCalls: options.LLMMaxCalls,
|
||||
MaxConcurrency: options.LLMConcurrency,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
@@ -56,10 +57,18 @@ func ConfigureOptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readsTargetsFromStdin reports whether targets are read from a stdin pipe.
|
||||
// Targets given with -u, -l, or uncover take its place: under cron, CI or an
|
||||
// agent stdin is often a pipe nothing writes to or closes, and reading it
|
||||
// would hold the scan until it does.
|
||||
func readsTargetsFromStdin(options *types.Options, stdinIsPipe bool) bool {
|
||||
hasOtherTargets := len(options.Targets) > 0 || options.TargetsFilePath != "" || options.Uncover || len(options.UncoverQuery) > 0
|
||||
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets
|
||||
}
|
||||
|
||||
// ParseOptions parses the command line flags provided by a user
|
||||
func ParseOptions(options *types.Options) {
|
||||
// Check if stdin pipe was given
|
||||
options.Stdin = !options.DisableStdin && fileutil.HasStdin()
|
||||
options.Stdin = readsTargetsFromStdin(options, fileutil.HasStdin())
|
||||
|
||||
// Read the inputs from env variables that not passed by flag.
|
||||
readEnvInputVars(options)
|
||||
@@ -141,7 +150,7 @@ func ParseOptions(options *types.Options) {
|
||||
}
|
||||
}
|
||||
|
||||
// validateOptions validates the configuration options passed
|
||||
// ValidateOptions validates the configuration options passed
|
||||
func ValidateOptions(options *types.Options) error {
|
||||
if err := validateOptions.Struct(options); err != nil {
|
||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||
@@ -167,10 +176,18 @@ func ValidateOptions(options *types.Options) error {
|
||||
if options.ShouldFollowHTTPRedirects() && options.DisableRedirects {
|
||||
return errors.New("both follow redirects and disable redirects specified")
|
||||
}
|
||||
switch options.UpdateTemplatesChannel {
|
||||
case "", installer.TemplatesChannelStable, installer.TemplatesChannelLatest:
|
||||
default:
|
||||
return fmt.Errorf("invalid templates update channel %q, must be %q or %q", options.UpdateTemplatesChannel, installer.TemplatesChannelStable, installer.TemplatesChannelLatest)
|
||||
}
|
||||
// loading the proxy server list from file or cli and test the connectivity
|
||||
if err := loadProxyServers(options); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReplayProxy(options.ReplayProxy); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Validate {
|
||||
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -59,3 +60,33 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOptionsUpdateTemplatesChannel checks that only known templates
|
||||
// update channels are accepted.
|
||||
func TestValidateOptionsUpdateTemplatesChannel(t *testing.T) {
|
||||
for _, channel := range []string{"", installer.TemplatesChannelStable, installer.TemplatesChannelLatest} {
|
||||
require.NoError(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: channel}), "channel %q", channel)
|
||||
}
|
||||
require.ErrorContains(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: "nightly"}), `invalid templates update channel "nightly"`)
|
||||
}
|
||||
|
||||
func TestReadsTargetsFromStdin(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
options *types.Options
|
||||
stdinIsPipe bool
|
||||
want bool
|
||||
}{
|
||||
{"piped stdin alone", &types.Options{}, true, true},
|
||||
{"no pipe", &types.Options{}, false, false},
|
||||
{"disabled", &types.Options{DisableStdin: true}, true, false},
|
||||
{"targets given with -u", &types.Options{Targets: goflags.StringSlice{"example.com"}}, true, false},
|
||||
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
|
||||
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
|
||||
{"uncover enabled", &types.Options{Uncover: true}, true, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
HTTPS_PROXY_ENV = "HTTPS_PROXY"
|
||||
)
|
||||
|
||||
var proxyEnvVars = []string{
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
}
|
||||
|
||||
func setProxyEnv(proxyURL string) {
|
||||
for _, env := range proxyEnvVars {
|
||||
_ = os.Setenv(env, proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
// loadProxyServers load list of proxy servers from file or comma separated
|
||||
func loadProxyServers(options *types.Options) error {
|
||||
if len(options.Proxy) == 0 {
|
||||
@@ -53,15 +69,37 @@ func loadProxyServers(options *types.Options) error {
|
||||
return errkit.Wrapf(err, "failed to parse proxy got %v", err)
|
||||
}
|
||||
if options.ProxyInternal {
|
||||
_ = os.Setenv(HTTP_PROXY_ENV, proxyURL.String())
|
||||
setProxyEnv(proxyURL.String())
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS:
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
}
|
||||
options.AliveHttpProxy = proxyURL.String()
|
||||
case proxyutils.SOCKS5:
|
||||
options.AliveSocksProxy = proxyURL.String()
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateReplayProxy rejects a -replay-proxy value the replay client cannot dial
|
||||
func validateReplayProxy(raw string) error {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
proxyURL, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "invalid replay proxy %q", raw)
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS, proxyutils.SOCKS5:
|
||||
if proxyURL.Host != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid replay proxy %q: want an http, https or socks5 URL", raw)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateReplayProxy(t *testing.T) {
|
||||
for _, valid := range []string{"", "http://127.0.0.1:8080", "https://proxy.example:8443", "socks5://127.0.0.1:1080"} {
|
||||
require.NoError(t, validateReplayProxy(valid), valid)
|
||||
}
|
||||
for _, invalid := range []string{"127.0.0.1:8080", "ftp://127.0.0.1:21", "http://", "://bad"} {
|
||||
require.Error(t, validateReplayProxy(invalid), invalid)
|
||||
}
|
||||
}
|
||||
|
||||
type envState struct {
|
||||
val string
|
||||
set bool
|
||||
}
|
||||
|
||||
func saveAndRestoreProxyEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
original := make(map[string]envState, len(proxyEnvVars))
|
||||
for _, env := range proxyEnvVars {
|
||||
val, set := os.LookupEnv(env)
|
||||
original[env] = envState{val: val, set: set}
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for env, state := range original {
|
||||
if state.set {
|
||||
_ = os.Setenv(env, state.val)
|
||||
} else {
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetProxyEnv(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
testProxy := "http://127.0.0.1:8080"
|
||||
setProxyEnv(testProxy)
|
||||
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, testProxy, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProxyServers_ProxyInternal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("proxy internal enabled sets all env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: true,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, server.URL, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("proxy internal disabled does not set env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: false,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Empty(t, os.Getenv(env), "expected %s not to be set", env)
|
||||
}
|
||||
})
|
||||
}
|
||||
+75
-11
@@ -3,9 +3,12 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -46,6 +49,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 +58,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
|
||||
@@ -83,7 +88,7 @@ var (
|
||||
// Runner is a client for running the enumeration process.
|
||||
type Runner struct {
|
||||
output output.Writer
|
||||
interactsh *interactsh.Client
|
||||
interactsh interactsh.IClient
|
||||
options *types.Options
|
||||
projectFile *projectfile.ProjectFile
|
||||
catalog catalog.Catalog
|
||||
@@ -109,6 +114,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 +128,12 @@ func New(options *types.Options) (*Runner, error) {
|
||||
return nil, fmt.Errorf("initialize nuclei configuration: %w", err)
|
||||
}
|
||||
|
||||
llmClient, err := configureLLM(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure llm: %w", err)
|
||||
}
|
||||
runner.llmClient = llmClient
|
||||
|
||||
if options.HealthCheck {
|
||||
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
|
||||
os.Exit(0)
|
||||
@@ -151,7 +163,9 @@ func New(options *types.Options) (*Runner, error) {
|
||||
tm := &installer.TemplateManager{
|
||||
CustomTemplates: ctm,
|
||||
DisablePublicTemplates: options.PublicTemplateDisableDownload,
|
||||
Channel: options.UpdateTemplatesChannel,
|
||||
}
|
||||
templatesVersion := config.DefaultConfig.TemplateVersion
|
||||
if err := tm.FreshInstallIfNotExists(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
|
||||
}
|
||||
@@ -168,7 +182,12 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if options.UpdateTemplates {
|
||||
// we automatically check for updates unless explicitly disabled
|
||||
// this print statement is only to inform the user that there are no updates
|
||||
if !config.DefaultConfig.NeedsTemplateUpdate() {
|
||||
noUpdates := !config.DefaultConfig.NeedsTemplateUpdate()
|
||||
if tm.Channel == installer.TemplatesChannelLatest {
|
||||
// the latest channel updates without a new release
|
||||
noUpdates = config.DefaultConfig.TemplateVersion == templatesVersion
|
||||
}
|
||||
if noUpdates {
|
||||
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
|
||||
}
|
||||
|
||||
@@ -213,7 +232,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||
runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
|
||||
var httpclient *retryablehttp.Client
|
||||
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
|
||||
if options.ProxyInternal && (options.AliveHttpProxy != "" || options.AliveSocksProxy != "") {
|
||||
var err error
|
||||
|
||||
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
|
||||
@@ -412,13 +431,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||
opts.HTTPClient = httpclient
|
||||
}
|
||||
if opts.HTTPClient == nil {
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
opts.HTTPClient = newInteractshHTTPClient(options)
|
||||
}
|
||||
|
||||
interactshClient, err := interactsh.New(opts)
|
||||
@@ -657,6 +670,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 +731,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 +746,18 @@ 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 r.hasPinnedTargets() {
|
||||
if err := pinnedTargetConflict(r.options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
r.options.DAST = true
|
||||
@@ -772,6 +802,15 @@ func (r *Runner) RunEnumeration() error {
|
||||
if err := store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Each template is parsed once per scan, so the compiled cache only holds
|
||||
// pre-compilation snapshots that nothing reads after loading.
|
||||
if parser, ok := r.parser.(*templates.Parser); ok {
|
||||
parser.PurgeCompiled()
|
||||
}
|
||||
if profiles != nil {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
}
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
@@ -966,7 +1005,7 @@ func (r *Runner) executeSmartWorkflowInput(executorOpts *protocols.ExecutorOptio
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create automatic scan service")
|
||||
}
|
||||
if err := service.Execute(); err != nil {
|
||||
if err := service.Execute(context.Background()); err != nil {
|
||||
return nil, errors.Wrap(err, "could not execute automatic scan")
|
||||
}
|
||||
result := &atomic.Bool{}
|
||||
@@ -1209,3 +1248,28 @@ func init() {
|
||||
HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false)
|
||||
EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false)
|
||||
}
|
||||
|
||||
func newInteractshHTTPClient(options *types.Options) *retryablehttp.Client {
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
if dialers := protocolstate.GetDialersWithId(options.ExecutionId); dialers != nil && dialers.Fastdialer != nil {
|
||||
// The pooled default keeps ProxyFromEnvironment and the TLS config that
|
||||
// INTERACTSH_TLS_VERIFY edits. Nuclei only exports HTTP_PROXY when -pi
|
||||
// is set, and that path uses the pooled client, so -proxy without -pi
|
||||
// still dials the interactsh server directly.
|
||||
transport := retryablehttp.DefaultReusePooledTransport()
|
||||
transport.DialContext = dialers.Fastdialer.Dial
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialers.Fastdialer.DialTLS(ctx, network, addr)
|
||||
}
|
||||
httpOpts.HttpClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: httpOpts.Timeout,
|
||||
}
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
return retryablehttp.NewClient(httpOpts)
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -143,3 +148,52 @@ func TestWalkReflectStructHandlesNestedStructs(t *testing.T) {
|
||||
require.Equal(t, "2", testStruct.Struct.B)
|
||||
require.Equal(t, "true", testStruct.Struct.C)
|
||||
}
|
||||
|
||||
func TestNewInteractshHTTPClient(t *testing.T) {
|
||||
// ProxyFromEnvironment reads the environment once per process. Set it
|
||||
// before the first request. NO_PROXY keeps the local dial direct.
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
options := &types.Options{
|
||||
ExecutionId: "test-interactsh-client-dialer",
|
||||
Timeout: 35,
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(options))
|
||||
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
|
||||
|
||||
client := newInteractshHTTPClient(options)
|
||||
require.NotNil(t, client)
|
||||
require.Equal(t, 35*time.Second, client.HTTPClient.Timeout)
|
||||
|
||||
transport, ok := client.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.Proxy)
|
||||
require.NotNil(t, transport.DialContext)
|
||||
require.NotNil(t, transport.DialTLSContext)
|
||||
require.NotNil(t, transport.TLSClientConfig, "INTERACTSH_TLS_VERIFY edits this config")
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
resp, err := client.HTTPClient.Get(srv.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
host := srv.Listener.Addr().(*net.TCPAddr).IP.String()
|
||||
dialers := protocolstate.GetDialersWithId(options.ExecutionId)
|
||||
require.Equal(t, host, dialers.Fastdialer.GetDialedIP(host))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
proxyURL, err := transport.Proxy(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proxyURL)
|
||||
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// targetProfiles returns the per-target profiles read from input, or nil when
|
||||
// no target has one.
|
||||
func (r *Runner) targetProfiles() *targetprofile.Registry {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
TargetProfiles() *targetprofile.Registry
|
||||
})
|
||||
if !ok || !provider.TargetProfiles().Scoped() {
|
||||
return nil
|
||||
}
|
||||
return provider.TargetProfiles()
|
||||
}
|
||||
|
||||
// hasPinnedTargets reports whether a target line pinned a target to an ip.
|
||||
func (r *Runner) hasPinnedTargets() bool {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
PinnedTargets() bool
|
||||
})
|
||||
return ok && provider.PinnedTargets()
|
||||
}
|
||||
|
||||
// pinnedTargetConflict returns an error for an option that cannot run with
|
||||
// targets pinned to an ip.
|
||||
func pinnedTargetConflict(options *types.Options) error {
|
||||
switch {
|
||||
case options.AutomaticScan:
|
||||
return errors.New("targets pinned to an ip cannot be combined with automatic scan")
|
||||
case options.AliveHttpProxy != "" || options.AliveSocksProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with a proxy, as the proxy resolves the host itself")
|
||||
case options.ReplayProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with -replay-proxy, as the replay proxy resolves the host itself")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPinnedTargetConflict(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *types.Options
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "no conflicting option", options: &types.Options{}},
|
||||
{name: "automatic scan", options: &types.Options{AutomaticScan: true}, wantErr: true},
|
||||
{name: "http proxy", options: &types.Options{AliveHttpProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
{name: "socks proxy", options: &types.Options{AliveSocksProxy: "socks5://127.0.0.1:1080"}, wantErr: true},
|
||||
{name: "replay proxy", options: &types.Options{ReplayProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantErr, pinnedTargetConflict(tt.options) != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
@@ -51,7 +52,7 @@ type NucleiExecutorOptions struct {
|
||||
Catalog catalog.Catalog
|
||||
IssuesClient reporting.Client
|
||||
RateLimiter *ratelimit.Limiter
|
||||
Interactsh *interactsh.Client
|
||||
Interactsh interactsh.IClient
|
||||
ProjectFile *projectfile.ProjectFile
|
||||
Browser *browserEngine.Browser
|
||||
FuzzStatsDB *stats.Tracker
|
||||
@@ -106,6 +107,10 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if opts.Options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
|
||||
}
|
||||
|
||||
executorEngine := core.New(opts.Options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package functional_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -280,6 +281,12 @@ func trimMatchingQuotes(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func TestIsRetryableFunctionalError(t *testing.T) {
|
||||
if isRetryableFunctionalError(nil) || isRetryableFunctionalError(fmt.Errorf("load mismatch")) {
|
||||
t.Fatal("non-exit errors should not retry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitFunctionalArgs(t *testing.T) {
|
||||
t.Run("quoted values", func(t *testing.T) {
|
||||
args := splitFunctionalArgs(`{{binary}} -tags "cve,exposure" -author "geeknik,pdteam" -severity high,critical`)
|
||||
@@ -338,6 +345,29 @@ func runFunctionalCase(args []string, debug bool) error {
|
||||
return fmt.Errorf("functional runners are not initialized")
|
||||
}
|
||||
|
||||
const attempts = 2
|
||||
var last error
|
||||
for attempt := 1; attempt <= attempts; attempt++ {
|
||||
last = compareFunctionalLoad(args, debug)
|
||||
if last == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRetryableFunctionalError(last) {
|
||||
return last
|
||||
}
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
func isRetryableFunctionalError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
return errors.As(err, &exitErr)
|
||||
}
|
||||
|
||||
func compareFunctionalLoad(args []string, debug bool) error {
|
||||
var (
|
||||
releaseOutput string
|
||||
currentOutput string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ package integration_test
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
@@ -83,6 +84,8 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/annotation-timeout.yaml", TestCase: &annotationTimeout{}},
|
||||
{Path: "protocols/http/custom-attack-type.yaml", TestCase: &customAttackType{}},
|
||||
{Path: "protocols/http/get-all-ips.yaml", TestCase: &scanAllIPS{}},
|
||||
{Path: "protocols/http/target-line-ip.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/target-line-ip-pipeline.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/get-without-scheme.yaml", TestCase: &httpGetWithoutScheme{}},
|
||||
{Path: "protocols/http/cl-body-without-header.yaml", TestCase: &httpCLBodyWithoutHeader{}},
|
||||
{Path: "protocols/http/cl-body-with-header.yaml", TestCase: &httpCLBodyWithHeader{}},
|
||||
@@ -1606,6 +1609,70 @@ func (h *scanAllIPS) Execute(filePath string) error {
|
||||
return expectResultsCount(got, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve to the test servers
|
||||
type httpTargetLineIP struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, name string) (*httptest.Server, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "host=%s server=%s", r.Host, name)
|
||||
}))
|
||||
_ = ts.Listener.Close()
|
||||
ts.Listener = listener
|
||||
ts.Start()
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer one.Close()
|
||||
_, port, err := net.SplitHostPort(one.Listener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := []string{fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.1"}`, port)}
|
||||
want := []string{"one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer two.Close()
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.2"}`, port))
|
||||
want = append(want, "two")
|
||||
}
|
||||
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
got, err := testutils.RunNucleiBareArgsAndGetResults(debug, nil, "-t", filePath, "-l", targets.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(got, len(want)); err != nil {
|
||||
return err
|
||||
}
|
||||
joined := strings.Join(got, "\n")
|
||||
for _, name := range want {
|
||||
if !strings.Contains(joined, name) {
|
||||
return fmt.Errorf("no result from server %q: %v", name, got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure that ip|host are handled without http|https scheme
|
||||
type httpGetWithoutScheme struct{}
|
||||
|
||||
|
||||
@@ -73,17 +73,27 @@ func TestMain(m *testing.M) {
|
||||
templatesDir := filepath.Join(homeDir, config.NucleiTemplatesDirName)
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
|
||||
baseEnv := []string{
|
||||
"NUCLEI_TEMPLATES_DIR=" + templatesDir,
|
||||
"XDG_CONFIG_HOME=" + filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME=" + filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME=" + filepath.Join(tempDir, "cache"),
|
||||
}
|
||||
// cases run nuclei with -duc, so the official templates some of them read
|
||||
// are installed once here rather than by whichever cases start first, which
|
||||
// queued them behind each other's downloads inside their own timeouts
|
||||
if err := installTemplates(binaryPath, baseEnv); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
previousRunner := testutils.DefaultRunner()
|
||||
runner := testutils.NewRunner(
|
||||
testutils.WithBinaryPath(binaryPath),
|
||||
testutils.WithWorkingDir(workingFixturesDir),
|
||||
testutils.WithExtraArgs(integrationExtraArgs...),
|
||||
testutils.WithBaseEnv(
|
||||
"NUCLEI_TEMPLATES_DIR="+templatesDir,
|
||||
"XDG_CONFIG_HOME="+filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME="+filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(tempDir, "cache"),
|
||||
),
|
||||
testutils.WithBaseEnv(baseEnv...),
|
||||
)
|
||||
testutils.SetDefaultRunner(runner)
|
||||
|
||||
@@ -124,6 +134,16 @@ func buildNucleiBinary(repoRoot, binaryPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installTemplates(binaryPath string, env []string) error {
|
||||
cmd := exec.Command(binaryPath, "-update-templates", "-no-color")
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install nuclei templates: %w\n%s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDebugMode() bool {
|
||||
for _, envVar := range []string{"DEBUG", "ACTIONS_STEP_DEBUG", "ACTIONS_RUNNER_DEBUG", "RUNNER_DEBUG"} {
|
||||
if envTruthy(envVar) {
|
||||
|
||||
@@ -5,10 +5,12 @@ package integration_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/ory/dockertest/v3"
|
||||
dc "github.com/ory/dockertest/v3/docker"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
osutils "github.com/projectdiscovery/utils/os"
|
||||
"github.com/projectdiscovery/utils/reader"
|
||||
"go.uber.org/multierr"
|
||||
@@ -34,9 +37,12 @@ var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/redis-lua-script.yaml", TestCase: &javascriptRedisLuaScript{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
|
||||
{Path: "protocols/javascript/target-line-ip.yaml", TestCase: &javascriptTargetLineIP{}},
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
|
||||
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
|
||||
{Path: "protocols/javascript/websocket-echo.yaml", TestCase: &javascriptWebSocketEcho{}},
|
||||
{Path: "protocols/javascript/websocket-denied.yaml", TestCase: &javascriptWebSocketDenied{}},
|
||||
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
|
||||
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
|
||||
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
|
||||
@@ -465,6 +471,87 @@ func (j *networkMultiStep) Execute(filePath string) error {
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve, so the javascript
|
||||
// connection reaches a server only through the pinned ip
|
||||
type javascriptTargetLineIP struct{}
|
||||
|
||||
func (j *javascriptTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, banner string) (net.Listener, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write([]byte(banner))
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = one.Close() }()
|
||||
_, port, err := net.SplitHostPort(one.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
want := map[string]string{"127.0.0.1": "one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer func() { _ = two.Close() }()
|
||||
want["127.0.0.2"] = "two"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(want))
|
||||
for ip := range want {
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "pinned.test:%s", "ip": %q}`, port, ip))
|
||||
}
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
if err := ensureSignedIntegrationTemplates(); err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), "-t", filePath, "-l", targets.Name(), "-jsonl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
got := make(map[string]string)
|
||||
for _, result := range results {
|
||||
var event output.ResultEvent
|
||||
if !strings.HasPrefix(result, "{") || json.Unmarshal([]byte(result), &event) != nil {
|
||||
continue
|
||||
}
|
||||
got[event.IP] = strings.Join(event.ExtractedResults, ",")
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
return fmt.Errorf("got results for %d ips, want %d: %v", len(got), len(want), got)
|
||||
}
|
||||
for ip, banner := range want {
|
||||
if got[ip] != banner {
|
||||
return fmt.Errorf("ip %s answered %q, want %q", ip, got[ip], banner)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expectedNumbers ...int) error {
|
||||
if len(expectedNumbers) == 0 {
|
||||
expectedNumbers = []int{1}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
)
|
||||
|
||||
// startWebSocketEchoServer starts a WebSocket server on a random loopback port
|
||||
// that accepts the "chat" subprotocol, greets, then echoes every message.
|
||||
func startWebSocketEchoServer() (string, func(), error) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("websocket listen: %w", err)
|
||||
}
|
||||
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := ws.HTTPUpgrader{Protocol: func(protocol string) bool { return protocol == "chat" }}
|
||||
conn, _, _, err := upgrader.Upgrade(r, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if err := wsutil.WriteServerText(conn, []byte("welcome")); err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
payload, op, err := wsutil.ReadClientData(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerMessage(conn, op, payload); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})}
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return lis.Addr().String(), func() { _ = srv.Close() }, nil
|
||||
}
|
||||
|
||||
// javascriptWebSocketEcho exercises the nuclei/websocket library end to end
|
||||
// against a real WebSocket server.
|
||||
type javascriptWebSocketEcho struct{}
|
||||
|
||||
func (j *javascriptWebSocketEcho) Execute(filePath string) error {
|
||||
address, stop, err := startWebSocketEchoServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stop()
|
||||
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// javascriptWebSocketDenied asserts the nuclei/websocket library refuses a host
|
||||
// on the exclude list (network-policy enforcement) before connecting.
|
||||
type javascriptWebSocketDenied struct{}
|
||||
|
||||
func (j *javascriptWebSocketDenied) Execute(filePath string) error {
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
@@ -6,6 +6,8 @@ package integration_test
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -44,6 +46,38 @@ func TestNetwork(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PinnedTargetLine", func(t *testing.T) {
|
||||
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
|
||||
if err == nil && string(data) == "PING" {
|
||||
_, _ = conn.Write([]byte("PONG"))
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
_, port, err := net.SplitHostPort(server.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
targets := filepath.Join(t.TempDir(), "targets.txt")
|
||||
line := fmt.Sprintf(`{"target": "pinned.test:%s", "ip": "127.0.0.1"}`, port)
|
||||
if err := os.WriteFile(targets, []byte(line), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// pinned.test does not resolve: the network request reaches the server
|
||||
// only through the pinned ip
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, nil, "-t", "protocols/network/basic.yaml", "-l", targets)
|
||||
if err != nil {
|
||||
t.Fatalf("pinned network request failed: %v", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Hex", func(t *testing.T) {
|
||||
errState := &capturedError{}
|
||||
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
|
||||
|
||||
@@ -23,6 +23,11 @@ headless:
|
||||
args:
|
||||
by: x
|
||||
xpath: /html/body/form/input[2]
|
||||
# the upload response echoes the file, rendered as text in a pre element
|
||||
- action: waitvisible
|
||||
args:
|
||||
by: xpath
|
||||
xpath: //pre[contains(., "Basic File Upload")]
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
id: target-line-ip-pipeline
|
||||
|
||||
info:
|
||||
name: Target Line IP Pipeline
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- raw:
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
|
||||
pipeline: true
|
||||
pipeline-concurrent-connections: 1
|
||||
pipeline-requests-per-connection: 1
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "host=pinned.test:"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: 1
|
||||
regex:
|
||||
- "server=([a-z]+)"
|
||||
@@ -0,0 +1,20 @@
|
||||
id: target-line-ip
|
||||
|
||||
info:
|
||||
name: Target Line IP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "host=pinned.test:"
|
||||
extractors:
|
||||
- type: regex
|
||||
group: 1
|
||||
regex:
|
||||
- "server=([a-z]+)"
|
||||
@@ -0,0 +1,25 @@
|
||||
id: javascript-target-line-ip
|
||||
|
||||
info:
|
||||
name: JavaScript Target Line IP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
javascript:
|
||||
- code: |
|
||||
var m = require("nuclei/net");
|
||||
var conn = m.Open("tcp", address);
|
||||
conn.SetTimeout(timeout);
|
||||
conn.RecvString(3)
|
||||
|
||||
args:
|
||||
address: "{{Host}}:{{Port}}"
|
||||
Host: "{{Host}}"
|
||||
Port: 5431
|
||||
timeout: 3
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
part: response
|
||||
regex:
|
||||
- "[a-z]+"
|
||||
+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: "",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package nuclei_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestThreadSafeCachedFlowPreservesPrecondition(t *testing.T) {
|
||||
for _, positive := range []bool{false, true} {
|
||||
name := "negative"
|
||||
if positive {
|
||||
name = "positive"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "flow.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`id: cached-flow
|
||||
info:
|
||||
name: Cached flow
|
||||
author: pdteam
|
||||
severity: info
|
||||
flow: http(1) && http(2)
|
||||
http:
|
||||
- method: GET
|
||||
path: ["{{BaseURL}}/precondition"]
|
||||
matchers:
|
||||
- type: status
|
||||
status: [200]
|
||||
internal: true
|
||||
- method: GET
|
||||
path: ["{{BaseURL}}/check"]
|
||||
matchers:
|
||||
- type: status
|
||||
status: [404]
|
||||
`), 0o600))
|
||||
var checks, findings atomic.Int64
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/check" {
|
||||
checks.Add(1)
|
||||
}
|
||||
if positive && r.URL.Path == "/precondition" {
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
engine, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(), nuclei.DisableUpdateCheck())
|
||||
require.NoError(t, err)
|
||||
defer engine.Close()
|
||||
engine.GlobalResultCallback(func(*output.ResultEvent) { findings.Add(1) })
|
||||
for call := int64(1); call <= 3; call++ {
|
||||
require.NoError(t, engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL}, nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{path}})))
|
||||
expected := int64(0)
|
||||
if positive {
|
||||
expected = call
|
||||
}
|
||||
require.Equal(t, expected, checks.Load(), "check requests after call %d", call)
|
||||
require.Equal(t, expected, findings.Load(), "findings after call %d", call)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -367,7 +367,9 @@ func UseOutputWriter(writer OutputWriter) NucleiSDKOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// StatsWriter
|
||||
// StatsWriter receives scan statistics. A writer that also implements
|
||||
// progress.TaskTracker is told how many template executions the scan runs
|
||||
// and how many are done.
|
||||
type StatsWriter progress.Progress
|
||||
|
||||
// UseStatsWriter allows setting a custom stats writer
|
||||
|
||||
+61
-1
@@ -8,9 +8,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Update checks are disabled explicitly so this pins corrupt-file rejection on
|
||||
// its own. The ignore file is read after init's update block, which may replace
|
||||
// a corrupt file via UpdateIgnoreFile — with update checks left on, this test
|
||||
// would assert rejection or repair depending on whether the process could reach
|
||||
// the network.
|
||||
func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, config.NucleiIgnoreFileName)
|
||||
@@ -21,8 +27,62 @@ func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
engine, err := NewNucleiEngineCtx(context.Background())
|
||||
engine, err := NewNucleiEngineCtx(context.Background(), DisableUpdateCheck())
|
||||
require.Nil(t, engine)
|
||||
require.ErrorContains(t, err, "error parsing")
|
||||
require.ErrorContains(t, err, strconv.Quote(path))
|
||||
}
|
||||
|
||||
// TestIgnoreFileSurvivesAuthTemplateStore pins two init orderings.
|
||||
//
|
||||
// The ignore file is read after the update block, so a file this same call
|
||||
// creates is the one that becomes the deny-list. GetAuthTmplStore also runs
|
||||
// during init whenever SecretsFile is set. It must scope a copy of the
|
||||
// options: the caller's Templates and ExcludeTags stay put, and the ignore
|
||||
// file is still applied on top.
|
||||
//
|
||||
// A static-only secrets file is enough to build that store.
|
||||
func TestIgnoreFileSurvivesAuthTemplateStore(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(root, config.NucleiIgnoreFileName),
|
||||
[]byte("tags:\n - dos\nfiles:\n - dns/soa-detect.yaml\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
secrets := filepath.Join(root, "secrets.yaml")
|
||||
require.NoError(t, os.WriteFile(secrets, []byte(`static:
|
||||
- type: header
|
||||
domains:
|
||||
- example.com
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: not-a-real-secret
|
||||
`), 0o600))
|
||||
|
||||
opts := types.DefaultOptions()
|
||||
opts.SecretsFile = []string{secrets}
|
||||
opts.Templates = []string{"http/kept.yaml"}
|
||||
opts.ExcludeTags = []string{"custom"}
|
||||
|
||||
engine, err := NewNucleiEngineCtx(context.Background(),
|
||||
WithOptions(opts),
|
||||
DisableUpdateCheck(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
require.Equal(t, []string{"http/kept.yaml"}, []string(engine.Options().Templates),
|
||||
"the auth store must not replace the caller's template list")
|
||||
require.Contains(t, []string(engine.Options().ExcludeTags), "custom",
|
||||
"the caller's own excluded tags must survive auth-store setup")
|
||||
require.Contains(t, []string(engine.Options().ExcludeTags), "dos",
|
||||
"the .nuclei-ignore deny-list must be applied after auth-store setup")
|
||||
require.Contains(t, []string(engine.Options().ExcludedTemplates), "dns/soa-detect.yaml",
|
||||
"the files section must be applied too")
|
||||
}
|
||||
+1
-5
@@ -44,11 +44,7 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Parser: base.parser,
|
||||
Browser: base.browserInstance,
|
||||
// Thread-safe executes can run concurrently with different Output writers.
|
||||
// The compiled-template cache shallow-copies requests and mutates shared
|
||||
// ExecutorOptions.Output via UpdateOptions/ApplyNewEngineOptions, which
|
||||
// would otherwise route all findings to whichever call last won the race.
|
||||
DoNotCache: true,
|
||||
DoNotCache: opts.DoNotCacheTemplates,
|
||||
}
|
||||
if base.interactshClient != nil {
|
||||
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
|
||||
|
||||
@@ -2,10 +2,14 @@ package nuclei
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -68,3 +72,87 @@ http:
|
||||
require.NotNil(t, cached)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreadSafeExecuteUsesSharedCompiledCache(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "template.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-shared-cache
|
||||
|
||||
info:
|
||||
name: Thread safe shared cache
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: thread-safe-shared-cache
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "thread-safe-shared-cache"
|
||||
`), 0o600))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("thread-safe-shared-cache"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
engine, err := NewThreadSafeNucleiEngineCtx(context.Background())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
const executions = 4
|
||||
var waitGroup sync.WaitGroup
|
||||
errors := make([]error, executions)
|
||||
for i := range executions {
|
||||
waitGroup.Add(1)
|
||||
go func() {
|
||||
defer waitGroup.Done()
|
||||
errors[i] = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
|
||||
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
|
||||
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-shared-cache"}}),
|
||||
WithResultCallback(func(*output.ResultEvent) {}),
|
||||
)
|
||||
}()
|
||||
}
|
||||
waitGroup.Wait()
|
||||
|
||||
for _, err := range errors {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, 1, engine.eng.GetParser().CompiledCount())
|
||||
}
|
||||
|
||||
func TestThreadSafeExecuteHonorsDisableTemplateCache(t *testing.T) {
|
||||
templatePath := filepath.Join(t.TempDir(), "template.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte(`id: thread-safe-disable-cache
|
||||
|
||||
info:
|
||||
name: Thread safe disable cache
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: thread-safe-disable-cache
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`), 0o600))
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
engine, err := NewThreadSafeNucleiEngineCtx(context.Background(), DisableTemplateCache())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
err = engine.ExecuteNucleiWithOptsCtx(context.Background(), []string{server.URL},
|
||||
WithTemplatesOrWorkflows(TemplateSources{Templates: []string{templatePath}}),
|
||||
WithTemplateFilters(TemplateFilters{Tags: []string{"thread-safe-disable-cache"}}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, engine.eng.GetParser().CompiledCount())
|
||||
}
|
||||
+16
-4
@@ -70,7 +70,7 @@ type NucleiEngine struct {
|
||||
|
||||
// unexported core fields
|
||||
ctx context.Context
|
||||
interactshClient *interactsh.Client
|
||||
interactshClient interactsh.IClient
|
||||
catalog catalog.Catalog
|
||||
rateLimiter *ratelimit.Limiter
|
||||
store *loader.Store
|
||||
@@ -151,6 +151,9 @@ func (e *NucleiEngine) GetTemplates() []*templates.Template {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
}
|
||||
if e.store == nil {
|
||||
return nil
|
||||
}
|
||||
return e.store.Templates()
|
||||
}
|
||||
|
||||
@@ -159,6 +162,9 @@ func (e *NucleiEngine) GetWorkflows() []*templates.Template {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
}
|
||||
if e.store == nil {
|
||||
return nil
|
||||
}
|
||||
return e.store.Workflows()
|
||||
}
|
||||
|
||||
@@ -251,7 +257,7 @@ func (e *NucleiEngine) closeInternal() {
|
||||
if e.hostErrCache != nil {
|
||||
e.hostErrCache.Close()
|
||||
}
|
||||
if e.executerOpts.RateLimiter != nil {
|
||||
if e.executerOpts != nil && e.executerOpts.RateLimiter != nil {
|
||||
e.executerOpts.RateLimiter.Stop()
|
||||
}
|
||||
if e.rateLimiter != nil {
|
||||
@@ -297,9 +303,11 @@ func (e *NucleiEngine) Close() {
|
||||
// enable matcher-status option if you expect this callback to be called for all results regardless if it matched or not
|
||||
func (e *NucleiEngine) ExecuteCallbackWithCtx(ctx context.Context, callback ...func(event *output.ResultEvent)) error {
|
||||
if !e.templatesLoaded {
|
||||
_ = e.LoadAllTemplates()
|
||||
if err := e.LoadAllTemplates(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0 {
|
||||
if e.store == nil || (len(e.store.Templates()) == 0 && len(e.store.Workflows()) == 0) {
|
||||
return ErrNoTemplatesAvailable
|
||||
}
|
||||
if e.inputProvider.Count() == 0 {
|
||||
@@ -378,6 +386,10 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
|
||||
}
|
||||
}
|
||||
if err := e.init(ctx); err != nil {
|
||||
// init may already own interactsh, a browser, a temp dir, and
|
||||
// reporting. Close is safe on a partial engine: executerOpts is
|
||||
// checked, and protocolinit.Close is a no-op before Init.
|
||||
e.Close()
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
|
||||
+34
-8
@@ -44,6 +44,9 @@ import (
|
||||
func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
|
||||
mockoutput := testutils.NewMockOutputWriter(e.opts.OmitTemplate)
|
||||
mockoutput.WriteCallback = func(event *output.ResultEvent) {
|
||||
if (event.Error != "" || !event.MatcherStatus) && !e.opts.MatcherStatus {
|
||||
return
|
||||
}
|
||||
if len(e.resultCallbacks) > 0 {
|
||||
for _, callback := range e.resultCallbacks {
|
||||
if callback != nil {
|
||||
@@ -184,10 +187,6 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.opts.Parser != nil {
|
||||
if op, ok := e.opts.Parser.(*templates.Parser); ok {
|
||||
e.parser = op
|
||||
@@ -211,7 +210,7 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
_ = protocolinit.Init(e.opts)
|
||||
}
|
||||
|
||||
if e.opts.ProxyInternal && e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "" {
|
||||
if e.opts.ProxyInternal && (e.opts.AliveHttpProxy != "" || e.opts.AliveSocksProxy != "") {
|
||||
httpclient, err := httpclientpool.Get(e.opts, &httpclientpool.Configuration{}, "")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -382,6 +381,26 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Read .nuclei-ignore LAST, once init has finished managing that file and
|
||||
// the options it writes into.
|
||||
//
|
||||
// It must come after the update block above: init creates or replaces the
|
||||
// ignore file there (UpdateIgnoreFile), so reading earlier reads a file this
|
||||
// same call is about to write. On a host without a pre-existing ignore file
|
||||
// — a fresh install, or any container whose filesystem was reset — the read
|
||||
// found nothing, warned, and left the engine with an empty deny-list for its
|
||||
// whole lifetime, because ExcludeTags is never re-read after init. Reading
|
||||
// afterwards also makes the corrupt-file check recoverable rather than fatal:
|
||||
// a malformed local file that IgnoreFileNeedsUpdate replaces no longer fails
|
||||
// the scan.
|
||||
//
|
||||
// GetAuthTmplStore scopes a copy of the options, so it no longer clears
|
||||
// the engine filters. This read is still what installs the deny-list, and
|
||||
// it has to follow the update block above.
|
||||
if err := e.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -389,7 +408,10 @@ type syncOnce struct {
|
||||
sync.Once
|
||||
}
|
||||
|
||||
var updateCheckInstance = &syncOnce{}
|
||||
var (
|
||||
updateCheckInstance = &syncOnce{}
|
||||
templateUpdateInstance = &syncOnce{}
|
||||
)
|
||||
|
||||
// processUpdateCheckResults processes update check results
|
||||
func (e *NucleiEngine) processUpdateCheckResults() error {
|
||||
@@ -398,8 +420,12 @@ func (e *NucleiEngine) processUpdateCheckResults() error {
|
||||
if e.onUpdateAvailableCallback != nil {
|
||||
e.onUpdateAvailableCallback(config.DefaultConfig.LatestNucleiTemplatesVersion)
|
||||
}
|
||||
tm := installer.TemplateManager{}
|
||||
err = tm.UpdateIfOutdated()
|
||||
})
|
||||
if !e.disableTemplatesAutoUpgrade {
|
||||
templateUpdateInstance.Do(func() {
|
||||
tm := installer.TemplateManager{}
|
||||
err = tm.UpdateIfOutdated()
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
+172
@@ -2,13 +2,20 @@ package nuclei_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -155,3 +162,168 @@ func TestIgnoreFileFilesSectionIsApplied(t *testing.T) {
|
||||
require.Contains(t, ne.Options().ExcludedTemplates, "http/fuzzing/wordpress-themes-detect.yaml",
|
||||
"files section should reach ExcludedTemplates")
|
||||
}
|
||||
|
||||
func TestSDKCustomPortTemplateDoesNotPoisonBaseHost(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprint(w, "server-ok")
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Allocate and immediately close a TCP listener to obtain a guaranteed closed local port.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
closedPort := ln.Addr().(*net.TCPAddr).Port
|
||||
require.NoError(t, ln.Close())
|
||||
|
||||
tplDir := t.TempDir()
|
||||
closedPortTpl := fmt.Sprintf(`id: closed-port-parallel-probe
|
||||
info:
|
||||
name: Closed Port Parallel Probe
|
||||
author: test
|
||||
severity: info
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "http://{{Hostname}}:%d/probe?p={{val}}"
|
||||
threads: 2
|
||||
payloads:
|
||||
val:
|
||||
- "1"
|
||||
- "2"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "never-matches"
|
||||
`, closedPort)
|
||||
healthyTpl := `id: healthy-base-target-match
|
||||
info:
|
||||
name: Healthy Base Target Match
|
||||
author: test
|
||||
severity: high
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "server-ok"
|
||||
`
|
||||
closedPortPath := filepath.Join(tplDir, "01-closed-port.yaml")
|
||||
healthyPath := filepath.Join(tplDir, "02-healthy.yaml")
|
||||
require.NoError(t, os.WriteFile(closedPortPath, []byte(closedPortTpl), 0o600))
|
||||
require.NoError(t, os.WriteFile(healthyPath, []byte(healthyTpl), 0o600))
|
||||
|
||||
for run := 1; run <= 3; run++ {
|
||||
ne, err := nuclei.NewNucleiEngineCtx(
|
||||
context.Background(),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
|
||||
Templates: []string{closedPortPath, healthyPath},
|
||||
}),
|
||||
nuclei.WithConcurrency(nuclei.Concurrency{
|
||||
TemplateConcurrency: 1,
|
||||
HostConcurrency: 1,
|
||||
HeadlessHostConcurrency: 1,
|
||||
HeadlessTemplateConcurrency: 1,
|
||||
JavascriptTemplateConcurrency: 1,
|
||||
TemplatePayloadConcurrency: 2,
|
||||
ProbeConcurrency: 1,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
ne.Options().RestrictLocalNetworkAccess = false
|
||||
ne.Options().Timeout = 2
|
||||
ne.Options().Retries = 0
|
||||
|
||||
ne.LoadTargets([]string{ts.URL}, false)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
matched []string
|
||||
violations []*output.ResultEvent
|
||||
)
|
||||
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !event.MatcherStatus || event.Error != "" {
|
||||
violations = append(violations, event)
|
||||
}
|
||||
matched = append(matched, event.TemplateID)
|
||||
})
|
||||
ne.Close()
|
||||
|
||||
require.NoError(t, err, "run %d failed", run)
|
||||
require.Empty(t, violations, "ExecuteCallbackWithCtx must not emit non-matching or skip events when MatcherStatus is false")
|
||||
require.Equal(t, []string{"healthy-base-target-match"}, matched, "run %d should consistently match healthy base target after custom-port failure", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKSkipEventsRespectMatcherStatus(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
closedAddr := ln.Addr().String()
|
||||
require.NoError(t, ln.Close())
|
||||
|
||||
tplDir := t.TempDir()
|
||||
for i := 1; i <= 3; i++ {
|
||||
tpl := fmt.Sprintf(`id: unreachable-check-%d
|
||||
info:
|
||||
name: Unreachable Check %d
|
||||
author: test
|
||||
severity: info
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/path-%d"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "ok"
|
||||
`, i, i, i)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(tplDir, fmt.Sprintf("0%d-tpl.yaml", i)), []byte(tpl), 0o600))
|
||||
}
|
||||
|
||||
runScan := func(matcherStatus bool) []*output.ResultEvent {
|
||||
ne, err := nuclei.NewNucleiEngineCtx(
|
||||
context.Background(),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
|
||||
Templates: []string{tplDir},
|
||||
}),
|
||||
nuclei.WithConcurrency(nuclei.Concurrency{
|
||||
TemplateConcurrency: 1,
|
||||
HostConcurrency: 1,
|
||||
HeadlessHostConcurrency: 1,
|
||||
HeadlessTemplateConcurrency: 1,
|
||||
JavascriptTemplateConcurrency: 1,
|
||||
TemplatePayloadConcurrency: 1,
|
||||
ProbeConcurrency: 1,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
ne.Options().RestrictLocalNetworkAccess = false
|
||||
ne.Options().Timeout = 1
|
||||
ne.Options().Retries = 0
|
||||
ne.Options().MatcherStatus = matcherStatus
|
||||
|
||||
ne.LoadTargets([]string{"http://" + closedAddr}, false)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
events []*output.ResultEvent
|
||||
)
|
||||
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
events = append(events, event)
|
||||
})
|
||||
ne.Close()
|
||||
require.NoError(t, err)
|
||||
return events
|
||||
}
|
||||
|
||||
require.Empty(t, runScan(false), "skip/error events must not be emitted when MatcherStatus is false")
|
||||
require.NotEmpty(t, runScan(true), "skip/error events should be emitted when MatcherStatus is true")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
|
||||
@@ -32,6 +33,13 @@ func GetKnownConfigFiles() []string {
|
||||
return knownConfigFiles
|
||||
}
|
||||
|
||||
// IsKnownConfigFile returns true if the base name of fpath is exactly one of
|
||||
// the known config files. Names that only contain one of them, such as
|
||||
// "cves.json.yaml", are not considered config files.
|
||||
func IsKnownConfigFile(fpath string) bool {
|
||||
return slices.Contains(knownConfigFiles, filepath.Base(filepath.FromSlash(fpath)))
|
||||
}
|
||||
|
||||
// GetKnownMiscDirectories returns known misc directories with trailing slashes.
|
||||
//
|
||||
// The trailing slash ensures that directory matching is explicit and avoids
|
||||
@@ -78,7 +86,7 @@ func IsTemplateWithRoot(fpath, rootDir string) bool {
|
||||
fname := filepath.Base(fpath)
|
||||
fext := strings.ToLower(filepath.Ext(fpath))
|
||||
|
||||
if stringsutil.ContainsAny(fname, GetKnownConfigFiles()...) {
|
||||
if IsKnownConfigFile(fname) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// toAbs converts a slash separated absolute path into an OS specific absolute
|
||||
// path, prefixing it with the system volume on Windows.
|
||||
func toAbs(p string) string {
|
||||
if osutils.IsWindows() {
|
||||
// Infer the drive letter from the Windows system path
|
||||
@@ -28,6 +30,8 @@ func toAbs(p string) string {
|
||||
return filepath.FromSlash(p)
|
||||
}
|
||||
|
||||
// TestIsTemplate checks template detection for supported extensions, excluded
|
||||
// directories and known config files.
|
||||
func TestIsTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -89,6 +93,24 @@ func TestIsTemplate(t *testing.T) {
|
||||
rootDir: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "excluded config file in subdirectory",
|
||||
fpath: "nuclei-templates/TEMPLATES-STATS.json",
|
||||
rootDir: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "template name containing config file name",
|
||||
fpath: "http/cves.json.yaml",
|
||||
rootDir: "",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "json template name containing config file name",
|
||||
fpath: "http/contributors.json.json",
|
||||
rootDir: "",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -99,6 +121,29 @@ func TestIsTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsKnownConfigFile ensures only exact config file base names match, and
|
||||
// names that merely contain one (e.g. cves.json.yaml) do not.
|
||||
func TestIsKnownConfigFile(t *testing.T) {
|
||||
for _, fpath := range []string{
|
||||
"cves.json",
|
||||
"contributors.json",
|
||||
"TEMPLATES-STATS.json",
|
||||
toAbs("/home/user/nuclei-templates/cves.json"),
|
||||
"https://example.com/nuclei-templates/cves.json",
|
||||
} {
|
||||
require.True(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
|
||||
}
|
||||
|
||||
for _, fpath := range []string{
|
||||
"cves.json.yaml",
|
||||
"http/cves.json.yaml",
|
||||
"my-contributors.json",
|
||||
toAbs("/home/user/cves.json/test.yaml"),
|
||||
} {
|
||||
require.False(t, IsKnownConfigFile(fpath), "IsKnownConfigFile(%q)", fpath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNucleiTemplatesIndexIncludesCustomDirSiblingPrefix(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfgDir := t.TempDir()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
|
||||
erred := make(map[string]error)
|
||||
|
||||
for _, t := range definitions {
|
||||
if stringsutil.ContainsAny(t, knownConfigFiles...) {
|
||||
// TODO: this is a temporary fix to avoid treating these files as templates
|
||||
// this should be replaced with more appropriate and robust logic
|
||||
if config.IsKnownConfigFile(t) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(t, urlutil.SchemeSeparator) && stringsutil.ContainsAny(t, config.GetSupportTemplateFileExtensions()...) {
|
||||
@@ -52,9 +50,7 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
|
||||
// purge all false positives
|
||||
filteredTemplates := []string{}
|
||||
for _, v := range allTemplates {
|
||||
// TODO: this is a temporary fix to avoid treating these files as templates
|
||||
// this should be replaced with more appropriate and robust logic
|
||||
if !stringsutil.ContainsAny(v, knownConfigFiles...) {
|
||||
if !config.IsKnownConfigFile(v) {
|
||||
filteredTemplates = append(filteredTemplates, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"testing/fstest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -42,6 +43,8 @@ func TestFindGlobPathMatchesUsesCanonicalContainment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindGlobPathMatchesResolvesContainedPath ensures a glob inside the
|
||||
// templates directory resolves against the embedded FS.
|
||||
func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
|
||||
memFS := fstest.MapFS{
|
||||
"http/test.yaml": {Data: []byte("legit")},
|
||||
@@ -56,3 +59,42 @@ func TestFindGlobPathMatchesResolvesContainedPath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"http/test.yaml"}, matches)
|
||||
}
|
||||
|
||||
// TestGetTemplatesPathKnownConfigFiles ensures that only exact known config
|
||||
// file names are skipped, and templates whose names merely contain one (e.g.
|
||||
// cves.json.yaml) are still loaded.
|
||||
func TestGetTemplatesPathKnownConfigFiles(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
httpDir := filepath.Join(templatesDir, "http")
|
||||
require.NoError(t, os.MkdirAll(httpDir, 0755))
|
||||
|
||||
template := filepath.Join(httpDir, "cves.json.yaml")
|
||||
for _, f := range []string{
|
||||
template,
|
||||
filepath.Join(templatesDir, "cves.json"),
|
||||
filepath.Join(templatesDir, "contributors.json"),
|
||||
filepath.Join(templatesDir, "TEMPLATES-STATS.json"),
|
||||
} {
|
||||
require.NoError(t, os.WriteFile(f, []byte("id: test"), 0644))
|
||||
}
|
||||
|
||||
cat := NewCatalog(templatesDir)
|
||||
|
||||
t.Run("direct path", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{template})
|
||||
require.Empty(t, errs)
|
||||
require.Equal(t, []string{template}, paths)
|
||||
})
|
||||
|
||||
t.Run("directory traversal", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{templatesDir})
|
||||
require.Empty(t, errs)
|
||||
require.Equal(t, []string{template}, paths)
|
||||
})
|
||||
|
||||
t.Run("config file is skipped", func(t *testing.T) {
|
||||
paths, errs := cat.GetTemplatesPath([]string{filepath.Join(templatesDir, "cves.json")})
|
||||
require.Empty(t, errs)
|
||||
require.Empty(t, paths)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package disk
|
||||
|
||||
var knownConfigFiles = []string{"cves.json", "contributors.json", "TEMPLATES-STATS.json"}
|
||||
@@ -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
|
||||
|
||||
+68
-16
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -62,20 +63,6 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
return &atomic.Bool{}
|
||||
}
|
||||
|
||||
if e.executerOpts.Progress != nil {
|
||||
// Notes:
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), int64(totalReqAfterClustering))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
||||
// based on no of hosts , templates , stream and other optimization parameters
|
||||
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
|
||||
}
|
||||
|
||||
filtered := []*templates.Template{}
|
||||
selfContained := []*templates.Template{}
|
||||
// Filter Self Contained templates since they are not bound to target
|
||||
@@ -87,6 +74,23 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
}
|
||||
}
|
||||
|
||||
if e.executerOpts.Progress != nil {
|
||||
// Notes:
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
requests, tasks := e.expectedWork(finalTemplates, filtered, target, totalReqAfterClustering)
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), requests)
|
||||
// a self-contained template runs once
|
||||
e.addTaskTotal(tasks + int64(len(selfContained)))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
// TODO: this is only a placeholder, auto scan strategy should choose scan strategy
|
||||
// based on no of hosts , templates , stream and other optimization parameters
|
||||
e.options.ScanStrategy = scanstrategy.TemplateSpray.String()
|
||||
}
|
||||
|
||||
// Execute All SelfContained in parallel
|
||||
e.executeAllSelfContained(ctx, selfContained, results, selfcontainedWg)
|
||||
|
||||
@@ -122,7 +126,7 @@ func (e *Engine) executeTemplateSpray(ctx context.Context, templatesList []*temp
|
||||
}
|
||||
|
||||
// resize check point - nop if there are no changes
|
||||
wp.RefreshWithConfig(e.GetWorkPoolConfig())
|
||||
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
|
||||
|
||||
templateType := template.Type()
|
||||
var wg *syncutil.AdaptiveWaitGroup
|
||||
@@ -169,7 +173,10 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
default:
|
||||
}
|
||||
|
||||
wp.Add()
|
||||
if err := wp.AddWithContext(ctx); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
go func(targetval *contextargs.MetaInput) {
|
||||
defer wp.Done()
|
||||
e.executeTemplatesOnTarget(ctx, templatesList, targetval, results)
|
||||
@@ -179,8 +186,50 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
return results
|
||||
}
|
||||
|
||||
// expectedWork returns the requests and the tasks the target-bound templates
|
||||
// will run. It counts only the template and target pairs that per-target
|
||||
// profiles select, so progress reflects the work that will be done. A task is
|
||||
// one template, cluster or workflow run on one target.
|
||||
func (e *Engine) expectedWork(templatesList, targetBound []*templates.Template, target provider.InputProvider, unscopedRequests int) (requests, tasks int64) {
|
||||
scope := e.executerOpts.TargetScope
|
||||
if scope == nil {
|
||||
return int64(unscopedRequests), target.Count() * int64(len(targetBound))
|
||||
}
|
||||
targets := make(map[protocols.TemplateSelection]int64)
|
||||
target.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
targets[scope.For(value)]++
|
||||
return true
|
||||
})
|
||||
for selection, count := range targets {
|
||||
requests += count * int64(getSelectedRequestCount(templatesList, selection))
|
||||
tasks += count * int64(getSelectedCount(targetBound, selection))
|
||||
}
|
||||
return requests, tasks
|
||||
}
|
||||
|
||||
// getSelectedCount returns the number of templates selection selects; a nil
|
||||
// selection selects every template.
|
||||
func getSelectedCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
|
||||
if selection == nil {
|
||||
return len(templates)
|
||||
}
|
||||
count := 0
|
||||
for _, template := range templates {
|
||||
if template.SelectedBy(selection) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// returns total requests count
|
||||
func getRequestCount(templates []*templates.Template) int {
|
||||
return getSelectedRequestCount(templates, nil)
|
||||
}
|
||||
|
||||
// getSelectedRequestCount returns the requests of the templates selection
|
||||
// selects; a nil selection selects every template.
|
||||
func getSelectedRequestCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
|
||||
count := 0
|
||||
for _, template := range templates {
|
||||
// ignore requests in workflows as total requests in workflow
|
||||
@@ -188,6 +237,9 @@ func getRequestCount(templates []*templates.Template) int {
|
||||
if len(template.Workflows) > 0 {
|
||||
continue
|
||||
}
|
||||
if selection != nil && !template.SelectedBy(selection) {
|
||||
continue
|
||||
}
|
||||
count += template.TotalRequests
|
||||
}
|
||||
return count
|
||||
|
||||
+143
-12
@@ -6,8 +6,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
@@ -35,6 +38,7 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
|
||||
}
|
||||
finished := e.templateExecutionStarted(template, "")
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
defer e.tasksDone(1)
|
||||
var err error
|
||||
var match bool
|
||||
ctx := scan.NewScanContext(ctx, contextargs.New(ctx))
|
||||
@@ -154,17 +158,29 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
skip = false
|
||||
}
|
||||
|
||||
// out of scope pairs keep their index so resume positions stay stable
|
||||
inScope := e.inScope(template, scannedValue)
|
||||
if !inScope {
|
||||
skip = true
|
||||
}
|
||||
|
||||
currentInfo.Lock()
|
||||
currentInfo.InFlight[index] = struct{}{}
|
||||
currentInfo.Unlock()
|
||||
|
||||
// Skip if the host has had errors
|
||||
if e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
hostDown := inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(template.Type().String(), contextargs.NewWithMetaInput(ctx, scannedValue))
|
||||
// in-scope pairs that never reach executeTemplateOnInput are done here:
|
||||
// those done by the resumed scan and those of an unresponsive host
|
||||
if inScope && (skip || hostDown) {
|
||||
e.tasksDone(1)
|
||||
}
|
||||
if hostDown {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: template.ID,
|
||||
TemplatePath: template.Path,
|
||||
Info: template.Info,
|
||||
Type: e.executerOpts.ProtocolType.String(),
|
||||
Type: template.Type().String(),
|
||||
Host: scannedValue.Input,
|
||||
MatcherStatus: false,
|
||||
Error: "host was skipped as it was found unresponsive",
|
||||
@@ -203,22 +219,26 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
wp := e.GetWorkPool()
|
||||
defer wp.Wait()
|
||||
|
||||
for _, tpl := range alltemplates {
|
||||
for i, tpl := range alltemplates {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if !e.inScope(tpl, target) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check whether the target has already been marked as permanently
|
||||
// unresponsive by HostErrorsCache before spawning another goroutine.
|
||||
if e.executerOpts.HostErrorsCache != nil &&
|
||||
e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, target)) {
|
||||
e.executerOpts.HostErrorsCache.Check(tpl.Type().String(), contextargs.NewWithMetaInput(ctx, target)) {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: tpl.ID,
|
||||
TemplatePath: tpl.Path,
|
||||
Info: tpl.Info,
|
||||
Type: e.executerOpts.ProtocolType.String(),
|
||||
Type: tpl.Type().String(),
|
||||
Host: target.Input,
|
||||
MatcherStatus: false,
|
||||
Error: "host was skipped as it was found unresponsive",
|
||||
@@ -229,11 +249,13 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
} else if e.executerOpts.Output != nil {
|
||||
_ = e.executerOpts.Output.Write(skipEvent)
|
||||
}
|
||||
// the remaining templates are skipped for this target as well
|
||||
e.tasksDone(int64(getSelectedCount(alltemplates[i:], e.selectionFor(target))))
|
||||
break
|
||||
}
|
||||
|
||||
// resize check point - nop if there are no changes
|
||||
wp.RefreshWithConfig(e.GetWorkPoolConfig())
|
||||
wp.RefreshWithConfigContext(ctx, e.GetWorkPoolConfig())
|
||||
|
||||
var sg *syncutil.AdaptiveWaitGroup
|
||||
if tpl.Type() == types.HeadlessProtocol {
|
||||
@@ -266,22 +288,74 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
}
|
||||
}
|
||||
|
||||
// inScope reports whether per-target profiles select template for input.
|
||||
func (e *Engine) inScope(template *templates.Template, input *contextargs.MetaInput) bool {
|
||||
selection := e.selectionFor(input)
|
||||
return selection == nil || template.SelectedBy(selection)
|
||||
}
|
||||
|
||||
// selectionFor returns the templates per-target profiles select for input; nil
|
||||
// selects every template.
|
||||
func (e *Engine) selectionFor(input *contextargs.MetaInput) protocols.TemplateSelection {
|
||||
if e.executerOpts.TargetScope == nil {
|
||||
return nil
|
||||
}
|
||||
return e.executerOpts.TargetScope.For(input)
|
||||
}
|
||||
|
||||
// taskTracker returns the progress when it counts tasks, nil otherwise.
|
||||
func (e *Engine) taskTracker() progress.TaskTracker {
|
||||
if e.executerOpts == nil {
|
||||
return nil
|
||||
}
|
||||
tracker, _ := e.executerOpts.Progress.(progress.TaskTracker)
|
||||
return tracker
|
||||
}
|
||||
|
||||
// addTaskTotal adds count tasks to the total when the progress counts tasks.
|
||||
func (e *Engine) addTaskTotal(count int64) {
|
||||
if tracker := e.taskTracker(); tracker != nil {
|
||||
tracker.AddToTaskTotal(count)
|
||||
}
|
||||
}
|
||||
|
||||
// tasksDone marks count tasks as done when the progress counts tasks.
|
||||
func (e *Engine) tasksDone(count int64) {
|
||||
if tracker := e.taskTracker(); tracker != nil {
|
||||
tracker.IncrementTasksBy(count)
|
||||
}
|
||||
}
|
||||
|
||||
// executeTemplateOnInput performs template execution for a single input and returns match status and error
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (bool, error) {
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (matched bool, err error) {
|
||||
// a pair skipped as a repeat of its origin is done as well
|
||||
defer e.tasksDone(1)
|
||||
// A template fixed to the target's origin makes the same requests for every
|
||||
// target that shares one, which a crawled list produces by the hundred.
|
||||
// Running it once per origin sends exactly the same traffic, minus the
|
||||
// repeats. The reservation is kept only when execution succeeds, so a
|
||||
// failed attempt leaves the origin free for a later target.
|
||||
finish, run := e.claimOriginScoped(template, value)
|
||||
if !run {
|
||||
return false, nil
|
||||
}
|
||||
defer func() { finish(err == nil) }()
|
||||
finished := e.templateExecutionStarted(template, value.Input)
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
// every protocol dials the target's host through its pinned ip, if any
|
||||
pinnedCtx := contextargs.WithCustomIP(ctx, value)
|
||||
ctxArgs := contextargs.New(pinnedCtx)
|
||||
ctxArgs.MetaInput = value
|
||||
scanCtx := scan.NewScanContext(ctx, ctxArgs)
|
||||
scanCtx := scan.NewScanContext(pinnedCtx, ctxArgs)
|
||||
|
||||
switch template.Type() {
|
||||
case types.WorkflowProtocol:
|
||||
return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
results, err := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
results, execErr := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if execErr != nil {
|
||||
return false, execErr
|
||||
}
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
@@ -291,3 +365,60 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
return template.Executer.Execute(scanCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// originAttempt is one origin-scoped execution. done is set only after a
|
||||
// successful run. While inflight is set, other callers wait, then retry if
|
||||
// that attempt failed.
|
||||
type originAttempt struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
inflight bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newOriginAttempt() *originAttempt {
|
||||
attempt := &originAttempt{}
|
||||
attempt.cond = sync.NewCond(&attempt.mu)
|
||||
return attempt
|
||||
}
|
||||
|
||||
// claimOriginScoped reports whether this caller should execute an origin-scoped
|
||||
// template. finish(true) records a successful run. finish(false) releases the
|
||||
// claim so a later target, including one that waited, can try again. Templates
|
||||
// whose requests depend on the target path always run. CustomIP is part of the
|
||||
// key so each address selected for one URL is tested on its own.
|
||||
func (e *Engine) claimOriginScoped(template *templates.Template, value *contextargs.MetaInput) (finish func(bool), run bool) {
|
||||
noop := func(bool) {}
|
||||
if template == nil || value == nil || !template.IsOriginScoped() {
|
||||
return noop, true
|
||||
}
|
||||
origin := normalize.Origin(value.Input)
|
||||
if origin == "" {
|
||||
return noop, true
|
||||
}
|
||||
key := template.ID + "\x00" + origin + "\x00" + value.CustomIP
|
||||
actual, _ := e.originScoped.LoadOrStore(key, newOriginAttempt())
|
||||
slot := actual.(*originAttempt)
|
||||
|
||||
slot.mu.Lock()
|
||||
for {
|
||||
if slot.done {
|
||||
slot.mu.Unlock()
|
||||
return noop, false
|
||||
}
|
||||
if !slot.inflight {
|
||||
slot.inflight = true
|
||||
slot.mu.Unlock()
|
||||
return func(success bool) {
|
||||
slot.mu.Lock()
|
||||
slot.inflight = false
|
||||
if success {
|
||||
slot.done = true
|
||||
}
|
||||
slot.cond.Broadcast()
|
||||
slot.mu.Unlock()
|
||||
}, true
|
||||
}
|
||||
slot.cond.Wait()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -9,10 +10,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
inputtypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -140,6 +143,289 @@ func TestExecuteTemplateOnInputReportsCancellationOnFinish(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type scriptedExecuter struct {
|
||||
calls int
|
||||
errs []error
|
||||
miss bool
|
||||
}
|
||||
|
||||
func (s *scriptedExecuter) Compile() error { return nil }
|
||||
func (s *scriptedExecuter) Requests() int { return 1 }
|
||||
func (s *scriptedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
s.calls++
|
||||
if len(s.errs) == 0 {
|
||||
return !s.miss, nil
|
||||
}
|
||||
err := s.errs[0]
|
||||
s.errs = s.errs[1:]
|
||||
return false, err
|
||||
}
|
||||
func (s *scriptedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
_, err := s.Execute(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func originScopedTemplate(path string) *templates.Template {
|
||||
return &templates.Template{
|
||||
ID: "root-health",
|
||||
RequestsHTTP: []*httpprotocol.Request{{
|
||||
Path: []string{path},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedRetriesAfterFailure(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
|
||||
if err == nil {
|
||||
t.Fatal("expected the first attempt to fail")
|
||||
}
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("later target should retry the origin, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedSkipsAfterSuccess(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b?q=1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedKeepsEachCustomIP(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, ip := range []string{"203.0.113.1", "203.0.113.2"} {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
|
||||
Input: "https://example.com/a",
|
||||
CustomIP: ip,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseURLTemplateIsNotOriginScoped(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{BaseURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedWaiterRetriesAfterFailedAttempt(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = &gatedExecuter{started: started, release: release, calls: &calls}
|
||||
|
||||
firstDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"})
|
||||
firstDone <- err
|
||||
}()
|
||||
<-started
|
||||
secondDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"})
|
||||
secondDone <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-secondDone:
|
||||
t.Fatalf("second target returned while the first attempt was still running: %v", err)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
close(release)
|
||||
if err := <-firstDone; err == nil {
|
||||
t.Fatal("expected the first attempt to fail")
|
||||
}
|
||||
if err := <-secondDone; err != nil {
|
||||
t.Fatalf("waiter should run after the failed attempt: %v", err)
|
||||
}
|
||||
if calls.Load() != 2 {
|
||||
t.Fatalf("calls = %d, want 2", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedKeepsDistinctOriginsAndTemplates(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
first := originScopedTemplate("{{RootURL}}/health")
|
||||
first.Executer = exec
|
||||
second := originScopedTemplate("{{RootURL}}/health")
|
||||
second.ID = "other-template"
|
||||
second.Executer = exec
|
||||
|
||||
inputs := []*contextargs.MetaInput{
|
||||
{Input: "https://example.com/a"},
|
||||
{Input: "https://other.test/a"},
|
||||
{Input: "http://example.com/a"},
|
||||
{Input: "https://example.com:8443/a"},
|
||||
}
|
||||
for _, value := range inputs {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), first, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), second, inputs[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 5 {
|
||||
t.Fatalf("calls = %d, want 5", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedFoldsDefaultPort(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com:443/b"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedSameCustomIPSkips(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for range 2 {
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{
|
||||
Input: "https://example.com/a",
|
||||
CustomIP: "203.0.113.1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedMissStillCounts(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{miss: true}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for _, raw := range []string{"https://example.com/a", "https://example.com/b"} {
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw})
|
||||
if err != nil || ok {
|
||||
t.Fatalf("non-match ok=%v err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedCallbackErrorRetries(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
e.Callback = func(*output.ResultEvent) {}
|
||||
exec := &scriptedExecuter{errs: []error{errors.New("timeout")}}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/a"}); err == nil {
|
||||
t.Fatal("expected the callback path to fail")
|
||||
}
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com/b"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exec.calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginScopedLeavesValuesWithoutAHost(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
exec := &scriptedExecuter{}
|
||||
tpl := originScopedTemplate("{{RootURL}}/health")
|
||||
tpl.Executer = exec
|
||||
|
||||
for range 2 {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "not a url"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, raw := range []string{"example.com", "other.test", "example.com"} {
|
||||
if _, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if exec.calls != 4 {
|
||||
t.Fatalf("calls = %d, want 4", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
type gatedExecuter struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
calls *atomic.Int32
|
||||
}
|
||||
|
||||
func (g *gatedExecuter) Compile() error { return nil }
|
||||
func (g *gatedExecuter) Requests() int { return 1 }
|
||||
func (g *gatedExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
if g.calls.Add(1) == 1 {
|
||||
close(g.started)
|
||||
<-g.release
|
||||
return false, errors.New("timeout")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
func (g *gatedExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
ok, err := g.Execute(ctx)
|
||||
if err != nil || !ok {
|
||||
return nil, err
|
||||
}
|
||||
return []*output.ResultEvent{{}}, nil
|
||||
}
|
||||
|
||||
type fakeTargetProvider struct {
|
||||
values []*contextargs.MetaInput
|
||||
}
|
||||
@@ -370,3 +656,113 @@ func Test_executeTemplateWithTargets_RespectsCancellation(t *testing.T) {
|
||||
var matched atomic.Bool
|
||||
e.executeTemplateWithTargets(ctx, tpl, targets, &matched)
|
||||
}
|
||||
|
||||
func newCancelTestEngine() *Engine {
|
||||
opts := &types.Options{
|
||||
TemplateThreads: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
BulkSize: 1,
|
||||
HeadlessBulkSize: 1,
|
||||
Logger: gologger.DefaultLogger,
|
||||
}
|
||||
e := New(opts)
|
||||
e.SetExecuterOptions(&protocols.ExecutorOptions{Logger: e.Logger, ResumeCfg: types.NewResumeCfg(), ProtocolType: tmpltypes.HTTPProtocol})
|
||||
return e
|
||||
}
|
||||
|
||||
type gateExecuter struct {
|
||||
started chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (g *gateExecuter) Compile() error { return nil }
|
||||
func (g *gateExecuter) Requests() int { return 1 }
|
||||
func (g *gateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
g.once.Do(func() { close(g.started) })
|
||||
<-ctx.Context().Done()
|
||||
return false, ctx.Context().Err()
|
||||
}
|
||||
func (g *gateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
_, err := g.Execute(ctx)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func TestExecuteTemplateSprayUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
templatesList := []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeTemplateSpray(ctx, templatesList, targets)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first template did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeTemplateSpray stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteHostSprayUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
templatesList := []*templates.Template{{Executer: gate}}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}, {Input: "b"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeHostSpray(ctx, templatesList, targets)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first host did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeHostSpray stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTemplatesOnTargetUnblocksCancelledAcquire(t *testing.T) {
|
||||
e := newCancelTestEngine()
|
||||
gate := &gateExecuter{started: make(chan struct{})}
|
||||
var matched atomic.Bool
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.executeTemplatesOnTarget(ctx, []*templates.Template{{Executer: gate}, {Executer: &gateExecuter{started: make(chan struct{})}}}, &contextargs.MetaInput{Input: "a"}, &matched)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-gate.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("first template did not start")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("executeTemplatesOnTarget stayed blocked after cancel")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// taskRecorder counts the tasks the engine schedules and completes.
|
||||
type taskRecorder struct {
|
||||
progress.Progress
|
||||
total atomic.Int64
|
||||
done atomic.Int64
|
||||
}
|
||||
|
||||
func (r *taskRecorder) Init(int64, int, int64) {}
|
||||
func (r *taskRecorder) AddToTaskTotal(delta int64) { r.total.Add(delta) }
|
||||
func (r *taskRecorder) IncrementTasksBy(count int64) { r.done.Add(count) }
|
||||
|
||||
// downHosts reports the listed inputs as unresponsive.
|
||||
type downHosts struct {
|
||||
hosterrorscache.CacheInterface
|
||||
inputs map[string]bool
|
||||
}
|
||||
|
||||
func (d *downHosts) Check(_ string, ctx *contextargs.Context) bool {
|
||||
return d.inputs[ctx.MetaInput.Input]
|
||||
}
|
||||
|
||||
// newTaskEngine returns an engine for strategy whose progress records tasks.
|
||||
// down lists the unresponsive inputs, and a nil resume starts a fresh scan.
|
||||
func newTaskEngine(strategy scanstrategy.ScanStrategy, scope protocols.TargetScope, down map[string]bool, resume *types.ResumeCfg) (*Engine, *taskRecorder) {
|
||||
options := &types.Options{
|
||||
BulkSize: 2,
|
||||
TemplateThreads: 2,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
ScanStrategy: strategy.String(),
|
||||
Logger: gologger.DefaultLogger,
|
||||
}
|
||||
if resume == nil {
|
||||
resume = types.NewResumeCfg()
|
||||
}
|
||||
recorder := &taskRecorder{}
|
||||
executerOpts := &protocols.ExecutorOptions{
|
||||
Options: options,
|
||||
ResumeCfg: resume,
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: recorder,
|
||||
TargetScope: scope,
|
||||
}
|
||||
if down != nil {
|
||||
executerOpts.HostErrorsCache = &downHosts{inputs: down}
|
||||
}
|
||||
engine := New(options)
|
||||
executerOpts.Logger = engine.Logger
|
||||
engine.SetExecuterOptions(executerOpts)
|
||||
return engine, recorder
|
||||
}
|
||||
|
||||
// targetsOf returns a target provider of inputs.
|
||||
func targetsOf(inputs ...string) *fakeTargetProvider {
|
||||
targets := &fakeTargetProvider{}
|
||||
for _, input := range inputs {
|
||||
targets.values = append(targets.values, &contextargs.MetaInput{Input: input})
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsTasks(t *testing.T) {
|
||||
scope := scopeByInput{
|
||||
"php": {paths: []string{"thinkphp.yaml"}},
|
||||
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
|
||||
"none": {},
|
||||
}
|
||||
inputs := []string{"php", "tomcat", "none", "plain"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scope protocols.TargetScope
|
||||
down map[string]bool
|
||||
wantRuns int
|
||||
wantTotal int64
|
||||
wantDone int64
|
||||
}{
|
||||
{
|
||||
name: "every pair",
|
||||
// 3 templates on 4 targets, and the self-contained template once
|
||||
wantRuns: 13,
|
||||
wantTotal: 13,
|
||||
wantDone: 13,
|
||||
},
|
||||
{
|
||||
name: "per-target profiles",
|
||||
scope: scope,
|
||||
// php 1, tomcat 2, none 0, plain 3, self-contained 1
|
||||
wantRuns: 7,
|
||||
wantTotal: 7,
|
||||
wantDone: 7,
|
||||
},
|
||||
{
|
||||
name: "unresponsive host",
|
||||
down: map[string]bool{"tomcat": true},
|
||||
// tomcat is skipped for all 3 templates
|
||||
wantRuns: 10,
|
||||
wantTotal: 13,
|
||||
wantDone: 13,
|
||||
},
|
||||
{
|
||||
name: "unresponsive host with per-target profiles",
|
||||
scope: scope,
|
||||
down: map[string]bool{"tomcat": true},
|
||||
// tomcat is skipped for its 2 templates
|
||||
wantRuns: 5,
|
||||
wantTotal: 7,
|
||||
wantDone: 7,
|
||||
},
|
||||
}
|
||||
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
for _, test := range tests {
|
||||
t.Run(strategy.String()+"/"+test.name, func(t *testing.T) {
|
||||
engine, recorder := newTaskEngine(strategy, test.scope, test.down, nil)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: "self.yaml",
|
||||
Path: "self.yaml",
|
||||
TotalRequests: 1,
|
||||
SelfContained: true,
|
||||
Executer: &recordingExecuter{path: "self.yaml", mu: &mu, runs: &runs},
|
||||
})
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
|
||||
|
||||
require.Len(t, runs, test.wantRuns)
|
||||
require.Equal(t, test.wantTotal, recorder.total.Load(), "task total")
|
||||
require.Equal(t, test.wantDone, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsOriginScopedTasks(t *testing.T) {
|
||||
// a {{RootURL}} template runs once per origin, and the skipped repeats
|
||||
// are done tasks too
|
||||
inputs := []string{"http://a.test/x", "http://a.test/y", "http://a.test/z", "http://b.test/x"}
|
||||
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
t.Run(strategy.String(), func(t *testing.T) {
|
||||
engine, recorder := newTaskEngine(strategy, nil, nil, nil)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
templatesList := []*templates.Template{{
|
||||
ID: "root.yaml",
|
||||
Path: "root.yaml",
|
||||
TotalRequests: 1,
|
||||
RequestsHTTP: []*http.Request{{Path: []string{"{{RootURL}}/"}}},
|
||||
Executer: &recordingExecuter{path: "root.yaml", mu: &mu, runs: &runs},
|
||||
}}
|
||||
require.True(t, templatesList[0].IsOriginScoped())
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf(inputs...), true)
|
||||
|
||||
require.Len(t, runs, 2, "one run per origin")
|
||||
require.EqualValues(t, 4, recorder.total.Load(), "task total")
|
||||
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteScanCountsResumedTasks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
down map[string]bool
|
||||
wantRuns []string
|
||||
}{
|
||||
{
|
||||
name: "responsive hosts",
|
||||
wantRuns: []string{"todo.yaml@a", "todo.yaml@b"},
|
||||
},
|
||||
{
|
||||
// a resumed pair on an unresponsive host is counted once
|
||||
name: "unresponsive host",
|
||||
down: map[string]bool{"a": true},
|
||||
wantRuns: []string{"todo.yaml@b"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resumeCfg := types.NewResumeCfg()
|
||||
resumeCfg.ResumeFrom["done.yaml"] = &types.ResumeInfo{Completed: true}
|
||||
engine, recorder := newTaskEngine(scanstrategy.TemplateSpray, nil, test.down, resumeCfg)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"done.yaml", "todo.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targetsOf("a", "b"), true)
|
||||
|
||||
require.ElementsMatch(t, test.wantRuns, runs)
|
||||
require.EqualValues(t, 4, recorder.total.Load(), "task total")
|
||||
require.EqualValues(t, 4, recorder.done.Load(), "done tasks")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
}
|
||||
|
||||
for _, subtemplate := range matcher.Subtemplates {
|
||||
swg.Add()
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func(subtemplate *workflows.WorkflowTemplate) {
|
||||
defer swg.Done()
|
||||
@@ -161,7 +163,9 @@ func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan
|
||||
}
|
||||
if len(template.Subtemplates) > 0 && firstMatched {
|
||||
for _, subtemplate := range template.Subtemplates {
|
||||
swg.Add()
|
||||
if err := swg.AddWithContext(ctx.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(template *workflows.WorkflowTemplate) {
|
||||
// create a new context with the same input but with unset callbacks
|
||||
|
||||
@@ -2,7 +2,9 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
||||
@@ -301,6 +303,61 @@ func TestWorkflowsSameStepExecutersRefreshTemplateContext(t *testing.T) {
|
||||
require.Equal(t, "foo", secondWhoami, "could not refresh workflow context into later executers in the same step")
|
||||
}
|
||||
|
||||
func TestWorkflowSubtemplatesUnblockCancelledAcquire(t *testing.T) {
|
||||
progressBar, _ := progress.NewStatsTicker(0, false, false, false, 0)
|
||||
var started atomic.Int32
|
||||
block := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
|
||||
started.Add(1)
|
||||
<-ctx.Context().Done()
|
||||
}}
|
||||
var waitingStarted atomic.Bool
|
||||
waiting := &mockExecuter{result: true, executeScanHook: func(ctx *scan.ScanContext) {
|
||||
waitingStarted.Store(true)
|
||||
<-ctx.Context().Done()
|
||||
}}
|
||||
|
||||
// Two slots: the two blockers occupy them so the third AddWithContext waits.
|
||||
workflow := &workflows.Workflow{Options: &protocols.ExecutorOptions{Options: &types.Options{TemplateThreads: 2}}, Workflows: []*workflows.WorkflowTemplate{
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{
|
||||
Executer: &mockExecuter{result: true, outputs: []*output.InternalWrappedEvent{
|
||||
{OperatorsResult: &operators.Result{}, Results: []*output.ResultEvent{{}}},
|
||||
}}, Options: &protocols.ExecutorOptions{Progress: progressBar}},
|
||||
}, Subtemplates: []*workflows.WorkflowTemplate{
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: block, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
{Executers: []*workflows.ProtocolExecuterPair{{Executer: waiting, Options: &protocols.ExecutorOptions{Progress: progressBar}}}},
|
||||
}},
|
||||
}}
|
||||
|
||||
engine := &Engine{}
|
||||
parent, cancel := context.WithCancel(context.Background())
|
||||
input := contextargs.NewWithInput(parent, "https://test.com")
|
||||
ctx := scan.NewScanContext(parent, input)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
engine.executeWorkflow(ctx, workflow)
|
||||
}()
|
||||
|
||||
deadline := time.After(2 * time.Second)
|
||||
for started.Load() < 2 {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("blocking subtemplates did not occupy both slots")
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("workflow stayed blocked after cancel")
|
||||
}
|
||||
require.False(t, waitingStarted.Load(), "third subtemplate must not start while slots are held")
|
||||
}
|
||||
|
||||
type mockExecuter struct {
|
||||
result bool
|
||||
executeHook func(input *contextargs.MetaInput)
|
||||
|
||||
@@ -62,6 +62,10 @@ func (w *WorkPool) InputPool(templateType types.ProtocolType) *syncutil.Adaptive
|
||||
}
|
||||
|
||||
func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
|
||||
w.RefreshWithConfigContext(context.Background(), config)
|
||||
}
|
||||
|
||||
func (w *WorkPool) RefreshWithConfigContext(ctx context.Context, config WorkPoolConfig) {
|
||||
if w.config.TypeConcurrency != config.TypeConcurrency {
|
||||
w.config.TypeConcurrency = config.TypeConcurrency
|
||||
}
|
||||
@@ -74,7 +78,7 @@ func (w *WorkPool) RefreshWithConfig(config WorkPoolConfig) {
|
||||
if w.config.HeadlessInputConcurrency != config.HeadlessInputConcurrency {
|
||||
w.config.HeadlessInputConcurrency = config.HeadlessInputConcurrency
|
||||
}
|
||||
w.Refresh(context.Background())
|
||||
w.Refresh(ctx)
|
||||
}
|
||||
|
||||
func (w *WorkPool) Refresh(ctx context.Context) {
|
||||
|
||||
+18
-14
@@ -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
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
@@ -55,18 +56,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)
|
||||
@@ -137,10 +148,7 @@ func (b *Body) Rebuild() (*retryablehttp.Request, error) {
|
||||
return nil, errors.Wrap(err, "could not encode body")
|
||||
}
|
||||
cloned := b.req.Clone(context.Background())
|
||||
err = cloned.SetBodyString(encoded)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not set body")
|
||||
}
|
||||
httpbody.SetString(cloned, encoded)
|
||||
cloned.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/httpbody"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -45,6 +46,7 @@ func TestBodyComponent(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
require.IsType(t, &httpbody.Body{}, rebuilt.Body, "rebuilt body must not be copied per read")
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -244,3 +246,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,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newFuzzTestInteractshClient(t *testing.T) *interactsh.Client {
|
||||
func newFuzzTestInteractshClient(t *testing.T) interactsh.IClient {
|
||||
t.Helper()
|
||||
|
||||
client, err := interactsh.New(&interactsh.Options{
|
||||
|
||||
@@ -28,6 +28,50 @@ 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 |
|
||||
| `ip` | the address to connect to instead of resolving the target's host, see below |
|
||||
| `profile` | a template profile ID from the templates `profiles` directory (paths are rejected); the line's other keys override it |
|
||||
| `templates` | template paths, relative to the templates directory |
|
||||
| `tags`, `exclude-tags`, `include-tags`, `author`, `template-id`, `exclude-id`, `severity`, `exclude-severity`, `type`, `exclude-type` | same as the CLI flags |
|
||||
| `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.
|
||||
|
||||
### Pinning a target to an IP
|
||||
|
||||
`ip` sends the target's traffic to one address, like `curl --resolve`: the hostname is still used for the Host header, SNI and template variables such as `{{Hostname}}`, while connections to that hostname go to the ip. This scans a vhost on a given server, or an origin behind a CDN.
|
||||
|
||||
```jsonl
|
||||
{"target": "https://app.example", "ip": "192.0.2.10"}
|
||||
{"target": "https://app.example", "ip": "192.0.2.11", "tags": ["cve"]}
|
||||
```
|
||||
|
||||
- Each `target` and `ip` pair is a separate target: the same host can be listed with several ips, and each pair has its own selection. `{{ip}}` and the `ip` field of results hold the pinned ip.
|
||||
- The target must have a hostname; IP, CIDR and ASN targets cannot be pinned. `-sa` and `-iv` do not apply to a pinned target.
|
||||
- A pinned ip excluded with `-eh`, or a local one with `-lna`, is skipped.
|
||||
- `-mup` counts the urls of each ip apart, so a url listed with several ips is kept on each of them.
|
||||
- HTTP, network, websocket, javascript and ssl templates connect to the ip. Headless templates and `unsafe: true` raw requests do not.
|
||||
- Pinned targets cannot be combined with `-proxy`, `-replay-proxy` or `-automatic-scan`.
|
||||
- `-u` splits its value on commas unless the value is quoted, so pass target lines in a file (`-l`) or on stdin.
|
||||
|
||||
## Formats
|
||||
|
||||
Fuzzing and DAST take the request shape (method, body, params) from the input, not from CLI flags, so a bare URL is always fuzzed as a `GET`. To fuzz anything else, feed a request-shaped input with `-im`:
|
||||
|
||||
@@ -53,7 +53,25 @@ For unsupported media types, no appropriate content type is found for the body.
|
||||
|
||||
This module converts each operation into one or more example HTTP requests. Each request is dumped into a string format, accompanied by its method, URL, headers, and body. These are send as a callback for further processing.
|
||||
|
||||
_Please note: This document does not cover other features of OpenAPI specification like responses, security schemes, links, callbacks, etc. as these are not currently handled by the module._
|
||||
### Security Schemes
|
||||
|
||||
The OpenAPI input supports HTTP basic and bearer authentication, API keys in headers, query parameters or cookies, and OAuth2/OpenID Connect access tokens. Security requirements can be declared globally or per operation.
|
||||
|
||||
For OAuth2 (`type: oauth2`) and OpenID Connect (`type: openIdConnect`), supply an existing access token as the full `Authorization` header value:
|
||||
|
||||
```sh
|
||||
nuclei -l openapi.yaml -im openapi -var 'Authorization=Bearer YOUR_ACCESS_TOKEN'
|
||||
```
|
||||
|
||||
The same variable is used for HTTP bearer authentication. For basic authentication, provide the full `Basic ...` value; for API keys, use the parameter name specified by the security scheme.
|
||||
|
||||
Nuclei does not perform OAuth authorization flows, discover OpenID Connect providers, acquire tokens, or refresh them. Obtain an access token with the scopes required by the API before running the scan. The provided header value is used unchanged.
|
||||
|
||||
Security requirement objects are alternatives. The generator selects the first supported alternative, in document order, for which all required credentials are supplied, and includes every scheme in that object. An empty object (`{}`) permits anonymous access. If no supported alternative has all its credentials, the first supported alternative is used to report missing values; credentials from different alternatives are not combined.
|
||||
|
||||
Operations declaring `security: []` can be generated without the global token. Absent, nil or empty credential values cause affected requests to be skipped. Normal parameter validation reports missing credentials even when another required parameter is also missing. With `-skip-format-validation`, requests with missing credentials are silently skipped and the remaining operations are generated without inventing placeholder credentials.
|
||||
|
||||
_Please note: Other OpenAPI features such as responses, links and callbacks are not currently handled by the module._
|
||||
|
||||
## Postman Collection file
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,33 +36,7 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
|
||||
if len(schema.Servers) == 0 {
|
||||
return errors.New("no servers found in openapi schema")
|
||||
}
|
||||
|
||||
// new set of globalParams obtained from security schemes
|
||||
globalParams := openapi3.NewParameters()
|
||||
|
||||
if len(schema.Security) > 0 {
|
||||
params, err := GetGlobalParamsForSecurityRequirement(schema, &schema.Security)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalParams = append(globalParams, params...)
|
||||
}
|
||||
|
||||
// validate global param requirements
|
||||
for _, param := range globalParams {
|
||||
if val, ok := opts.Variables[param.Value.Name]; ok {
|
||||
param.Value.Example = val
|
||||
} else {
|
||||
// if missing check for validation
|
||||
if opts.SkipFormatValidation {
|
||||
gologger.Verbose().Msgf("openapi: skipping all requests due to missing global auth parameter: %s\n", param.Value.Name)
|
||||
return nil
|
||||
} else {
|
||||
// fatal error
|
||||
gologger.Fatal().Msgf("openapi: missing global auth parameter: %s\n", param.Value.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
globalParams, globalSecurityErr := selectSecurityParameters(schema, &schema.Security, opts.Variables)
|
||||
|
||||
missingVarMap := make(map[string]struct{})
|
||||
optionalVarMap := make(map[string]struct{})
|
||||
@@ -92,6 +66,10 @@ func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOpti
|
||||
requestPath = serverPath + path
|
||||
}
|
||||
for method, ov := range ops {
|
||||
// An override need not use an unsupported global security scheme.
|
||||
if ov.Security == nil && globalSecurityErr != nil {
|
||||
return globalSecurityErr
|
||||
}
|
||||
if err := generateRequestsFromOp(&generateReqOptions{
|
||||
requiredOnly: opts.RequiredOnly,
|
||||
method: method,
|
||||
@@ -148,7 +126,7 @@ type generateReqOptions struct {
|
||||
// post request generation callback
|
||||
callback formats.ParseReqRespCallback
|
||||
|
||||
// global parameters
|
||||
// Selected global authentication parameters, used only without an override.
|
||||
globalParams openapi3.Parameters
|
||||
// requestparams map
|
||||
reqParams openapi3.Parameters
|
||||
@@ -175,21 +153,42 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
}
|
||||
// add existing req params
|
||||
reqParams = append(reqParams, opts.op.Parameters...)
|
||||
// check for endpoint specific auth
|
||||
// Select one complete requirement for this operation before adding its
|
||||
// authentication parameters. Operation security overrides global security.
|
||||
authParams := opts.globalParams
|
||||
if opts.op.Security != nil {
|
||||
params, err := GetGlobalParamsForSecurityRequirement(opts.schema, opts.op.Security)
|
||||
authParams, err = selectSecurityParameters(opts.schema, opts.op.Security, opts.opts.Variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqParams = append(reqParams, params...)
|
||||
} else {
|
||||
reqParams = append(reqParams, opts.globalParams...)
|
||||
}
|
||||
reqParams = append(reqParams, authParams...)
|
||||
authParameters := make(map[*openapi3.Parameter]struct{}, len(authParams))
|
||||
for _, parameter := range authParams {
|
||||
authParameters[parameter.Value] = struct{}{}
|
||||
}
|
||||
|
||||
// Collect missing credentials before ordinary parameter validation can return
|
||||
// early. Still validate ordinary parameters so their diagnostics are retained.
|
||||
missingAuth := false
|
||||
for _, parameter := range authParams {
|
||||
value := parameter.Value
|
||||
if types.ToString(opts.opts.Variables[value.Name]) != "" {
|
||||
continue
|
||||
}
|
||||
missingAuth = true
|
||||
if opts.missingParamValueCallback != nil {
|
||||
opts.missingParamValueCallback(value, opts)
|
||||
}
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
for _, parameter := range reqParams {
|
||||
value := parameter.Value
|
||||
|
||||
if _, isAuth := authParameters[value]; missingAuth && isAuth {
|
||||
continue
|
||||
}
|
||||
if value.Schema == nil || value.Schema.Value == nil {
|
||||
continue
|
||||
}
|
||||
@@ -261,6 +260,9 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
req.AddCookie(&http.Cookie{Name: value.Name, Value: types.ToString(paramValue)})
|
||||
}
|
||||
}
|
||||
if missingAuth {
|
||||
return nil
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.URL.Path = opts.requestPath
|
||||
|
||||
@@ -271,7 +273,7 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
var val interface{}
|
||||
|
||||
if value.Schema == nil || value.Schema.Value == nil {
|
||||
val = generateEmptySchemaValue(content)
|
||||
val, _ = generateExampleFromSchema(generateEmptySchemaValue(content))
|
||||
} else {
|
||||
var err error
|
||||
|
||||
@@ -291,15 +293,25 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
cloned.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
case "application/xml":
|
||||
values := mxj.Map(val.(map[string]interface{}))
|
||||
var marshalled []byte
|
||||
var err error
|
||||
switch v := val.(type) {
|
||||
case map[string]interface{}:
|
||||
marshalled, err = mxj.Map(v).Xml()
|
||||
case string:
|
||||
marshalled = []byte(v)
|
||||
default:
|
||||
err = errors.Errorf("unsupported xml example type %T", val)
|
||||
}
|
||||
|
||||
if marshalled, err := values.Xml(); err == nil {
|
||||
if err == nil {
|
||||
// body = string(marshalled)
|
||||
cloned.Body = io.NopCloser(bytes.NewReader(marshalled))
|
||||
cloned.ContentLength = int64(len(marshalled))
|
||||
cloned.Header.Set("Content-Type", "application/xml")
|
||||
} else {
|
||||
gologger.Warning().Msgf("openapi: could not encode xml")
|
||||
continue
|
||||
}
|
||||
case "application/x-www-form-urlencoded":
|
||||
if values, ok := val.(map[string]interface{}); ok {
|
||||
@@ -345,7 +357,7 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
// use two strings
|
||||
str = "string1\nstring2"
|
||||
}
|
||||
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "bindary", "byte") {
|
||||
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "binary", "byte") {
|
||||
cloned.Body = io.NopCloser(bytes.NewReader([]byte(str)))
|
||||
cloned.ContentLength = int64(len(str))
|
||||
cloned.Header.Set("Content-Type", "application/octet-stream")
|
||||
@@ -391,45 +403,79 @@ func generateRequestsFromOp(opts *generateReqOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement
|
||||
// GetGlobalParamsForSecurityRequirement returns parameters for the first supported
|
||||
// security alternative, or no parameters when anonymous access is permitted.
|
||||
func GetGlobalParamsForSecurityRequirement(schema *openapi3.T, requirement *openapi3.SecurityRequirements) ([]*openapi3.ParameterRef, error) {
|
||||
globalParams := openapi3.NewParameters()
|
||||
if len(schema.Components.SecuritySchemes) == 0 {
|
||||
return nil, errkit.Newf("security requirements (%+v) without any security schemes found in openapi file", schema.Security)
|
||||
return selectSecurityParameters(schema, requirement, nil)
|
||||
}
|
||||
|
||||
// selectSecurityParameters chooses the first fully supplied alternative. If none
|
||||
// is supplied, return the first supported alternative for missing-value reporting.
|
||||
func selectSecurityParameters(schema *openapi3.T, requirement *openapi3.SecurityRequirements, variables map[string]interface{}) ([]*openapi3.ParameterRef, error) {
|
||||
if requirement == nil || len(*requirement) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
found := false
|
||||
// this api is protected for each security scheme pull its corresponding scheme
|
||||
schemaLabel:
|
||||
var fallback []*openapi3.ParameterRef
|
||||
var firstError error
|
||||
for _, security := range *requirement {
|
||||
for name := range security {
|
||||
if scheme, ok := schema.Components.SecuritySchemes[name]; ok {
|
||||
found = true
|
||||
param, err := GenerateParameterFromSecurityScheme(scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
globalParams = append(globalParams, &openapi3.ParameterRef{Value: param})
|
||||
continue schemaLabel
|
||||
if len(security) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
params := openapi3.NewParameters()
|
||||
complete, supported := true, true
|
||||
// Map order must not change which schemes are emitted or reported missing.
|
||||
for _, name := range mapsutil.GetSortedKeys(security) {
|
||||
var scheme *openapi3.SecuritySchemeRef
|
||||
if schema.Components != nil {
|
||||
scheme = schema.Components.SecuritySchemes[name]
|
||||
}
|
||||
if scheme == nil || scheme.Value == nil {
|
||||
if firstError == nil {
|
||||
firstError = errkit.Newf("security scheme (%s) not found in openapi file", name)
|
||||
}
|
||||
supported = false
|
||||
break
|
||||
}
|
||||
param, err := GenerateParameterFromSecurityScheme(scheme)
|
||||
if err != nil {
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
supported = false
|
||||
break
|
||||
}
|
||||
params = append(params, &openapi3.ParameterRef{Value: param})
|
||||
complete = complete && types.ToString(variables[param.Name]) != ""
|
||||
}
|
||||
if !found && len(security) > 1 {
|
||||
// if this is case then both security schemes are required
|
||||
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", security)
|
||||
if !supported {
|
||||
continue
|
||||
}
|
||||
if complete {
|
||||
return params, nil
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = params
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", requirement)
|
||||
if fallback != nil {
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
return globalParams, nil
|
||||
return nil, firstError
|
||||
}
|
||||
|
||||
// GenerateParameterFromSecurityScheme generates an example from a schema object
|
||||
func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*openapi3.Parameter, error) {
|
||||
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey") {
|
||||
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey", "oauth2", "openIdConnect") {
|
||||
return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)
|
||||
}
|
||||
if generic.EqualsAny(scheme.Value.Type, "oauth2", "openIdConnect") {
|
||||
// Access tokens are supplied by the caller through the Authorization variable.
|
||||
// Request generation does not acquire or refresh tokens from the provider.
|
||||
h := openapi3.NewHeaderParameter(DEFAULT_HTTP_SCHEME_HEADER).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth
|
||||
return h, nil
|
||||
}
|
||||
if scheme.Value.Type == "http" {
|
||||
// check scheme
|
||||
if !generic.EqualsAny(scheme.Value.Scheme, "basic", "bearer") {
|
||||
@@ -443,12 +489,12 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
|
||||
// create parameters using the scheme
|
||||
switch scheme.Value.Scheme {
|
||||
case "basic":
|
||||
h := openapi3.NewHeaderParameter(headerName)
|
||||
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
case "bearer":
|
||||
h := openapi3.NewHeaderParameter(headerName)
|
||||
h := openapi3.NewHeaderParameter(headerName).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
@@ -466,17 +512,17 @@ func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*o
|
||||
// create parameters using the scheme
|
||||
switch scheme.Value.In {
|
||||
case "query":
|
||||
q := openapi3.NewQueryParameter(scheme.Value.Name)
|
||||
q := openapi3.NewQueryParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
q.Required = true
|
||||
q.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return q, nil
|
||||
case "header":
|
||||
h := openapi3.NewHeaderParameter(scheme.Value.Name)
|
||||
h := openapi3.NewHeaderParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
h.Required = true
|
||||
h.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return h, nil
|
||||
case "cookie":
|
||||
c := openapi3.NewCookieParameter(scheme.Value.Name)
|
||||
c := openapi3.NewCookieParameter(scheme.Value.Name).WithSchema(openapi3.NewStringSchema())
|
||||
c.Required = true
|
||||
c.Description = globalAuth // differentiator for normal variables and global auth
|
||||
return c, nil
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpenAPIOctetStreamContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema string
|
||||
contentType string
|
||||
}{
|
||||
{name: "binary format", schema: "{type: string, format: binary}", contentType: "application/octet-stream"},
|
||||
{name: "byte format", schema: "{type: string, format: byte}", contentType: "application/octet-stream"},
|
||||
{name: "plain string", schema: "{type: string}", contentType: "text/plain"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
spec := `openapi: 3.0.0
|
||||
info: {title: t, version: "1"}
|
||||
servers: [{url: "http://example.com"}]
|
||||
paths:
|
||||
/upload:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream: {schema: ` + tt.schema + `}
|
||||
responses:
|
||||
"200": {description: ok}
|
||||
`
|
||||
var contentTypes []string
|
||||
err := New().Parse(strings.NewReader(spec), func(rr *types.RequestResponse) bool {
|
||||
value, _ := rr.Request.Headers.Get("Content-Type")
|
||||
contentTypes = append(contentTypes, value)
|
||||
return false
|
||||
}, "spec.yaml")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{tt.contentType}, contentTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateRequestsWithSecuritySchemes(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
scheme string
|
||||
variable string
|
||||
value string
|
||||
in string
|
||||
}{
|
||||
{
|
||||
name: "oauth2", scheme: `{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{"read":"Read access"}}}}`,
|
||||
variable: "Authorization", value: "Bearer supplied-token", in: "header",
|
||||
},
|
||||
{
|
||||
name: "openIdConnect", scheme: `{"type":"openIdConnect","openIdConnectUrl":"https://example.com/.well-known/openid-configuration"}`,
|
||||
variable: "Authorization", value: "Bearer supplied-token", in: "header",
|
||||
},
|
||||
{
|
||||
name: "bearer", scheme: `{"type":"http","scheme":"bearer"}`,
|
||||
variable: "Authorization", value: "Bearer supplied-token", in: "header",
|
||||
},
|
||||
{
|
||||
name: "basic", scheme: `{"type":"http","scheme":"basic"}`,
|
||||
variable: "Authorization", value: "Basic dXNlcjpwYXNz", in: "header",
|
||||
},
|
||||
{
|
||||
name: "apiKey-header", scheme: `{"type":"apiKey","name":"X-API-Key","in":"header"}`,
|
||||
variable: "X-API-Key", value: "supplied-key", in: "header",
|
||||
},
|
||||
{
|
||||
name: "apiKey-query", scheme: `{"type":"apiKey","name":"api_key","in":"query"}`,
|
||||
variable: "api_key", value: "supplied-key", in: "query",
|
||||
},
|
||||
{
|
||||
name: "apiKey-cookie", scheme: `{"type":"apiKey","name":"session","in":"cookie"}`,
|
||||
variable: "session", value: "supplied-key", in: "cookie",
|
||||
},
|
||||
} {
|
||||
for _, scope := range []string{"global", "operation"} {
|
||||
t.Run(test.name+"/"+scope, func(t *testing.T) {
|
||||
security := `"security":[{"auth":[]}],`
|
||||
if test.name == "oauth2" || test.name == "openIdConnect" {
|
||||
security = `"security":[{"auth":["read"]}],`
|
||||
}
|
||||
globalSecurity, operationSecurity := security, ""
|
||||
if scope == "operation" {
|
||||
globalSecurity, operationSecurity = "", security
|
||||
}
|
||||
document := fmt.Sprintf(`{
|
||||
"openapi":"3.0.3",
|
||||
"info":{"title":"Authentication test","version":"1.0"},
|
||||
"servers":[{"url":"https://example.com"}],
|
||||
"components":{"securitySchemes":{"auth":%s}},
|
||||
%s
|
||||
"paths":{"/protected":{"get":{%s"responses":{"200":{"description":"OK"}}}}}
|
||||
}`, test.scheme, globalSecurity, operationSecurity)
|
||||
parser := New()
|
||||
parser.SetOptions(formats.InputFormatOptions{
|
||||
RequiredOnly: true,
|
||||
Variables: map[string]interface{}{test.variable: test.value},
|
||||
})
|
||||
var requests []*http.Request
|
||||
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
|
||||
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(rr.Request.Raw)))
|
||||
require.NoError(t, err)
|
||||
requests = append(requests, req)
|
||||
return false
|
||||
}, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, requests, 1)
|
||||
req := requests[0]
|
||||
t.Cleanup(func() { require.NoError(t, req.Body.Close()) })
|
||||
require.Equal(t, "/protected", req.URL.Path)
|
||||
switch test.in {
|
||||
case "header":
|
||||
require.Equal(t, test.value, req.Header.Get(test.variable))
|
||||
case "query":
|
||||
require.Equal(t, test.value, req.URL.Query().Get(test.variable))
|
||||
case "cookie":
|
||||
cookie, err := req.Cookie(test.variable)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.value, cookie.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRequestsOAuthOperationOverride(t *testing.T) {
|
||||
parser := New()
|
||||
parser.SetOptions(formats.InputFormatOptions{
|
||||
Variables: map[string]interface{}{
|
||||
"Authorization": "Bearer supplied-token",
|
||||
"X-API-Key": "supplied-key",
|
||||
},
|
||||
})
|
||||
document := `{
|
||||
"openapi":"3.0.3",
|
||||
"info":{"title":"Authentication override test","version":"1.0"},
|
||||
"servers":[{"url":"https://example.com"}],
|
||||
"components":{"securitySchemes":{
|
||||
"oauth":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{}}}},
|
||||
"key":{"type":"apiKey","name":"X-API-Key","in":"header"}
|
||||
}},
|
||||
"security":[{"oauth":[]}],
|
||||
"paths":{
|
||||
"/protected":{"get":{"security":[{"key":[]}],"responses":{"200":{"description":"OK"}}}},
|
||||
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
|
||||
}
|
||||
}`
|
||||
paths := make(map[string]bool)
|
||||
err := parser.Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
|
||||
paths[rr.URL.Path] = true
|
||||
key, _ := rr.Request.Headers.Get("X-Api-Key")
|
||||
if rr.URL.Path == "/protected" {
|
||||
require.Equal(t, "supplied-key", key)
|
||||
} else {
|
||||
require.Empty(t, key)
|
||||
}
|
||||
auth, _ := rr.Request.Headers.Get("Authorization")
|
||||
require.Empty(t, auth)
|
||||
return false
|
||||
}, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]bool{"/protected": true, "/public": true}, paths)
|
||||
}
|
||||
|
||||
func TestGenerateRequestsOAuthRequiresToken(t *testing.T) {
|
||||
for _, skipValidation := range []bool{false, true} {
|
||||
for _, schemeType := range []string{"oauth2", "openIdConnect"} {
|
||||
t.Run(fmt.Sprintf("%s/skip-validation=%t", schemeType, skipValidation), func(t *testing.T) {
|
||||
param, err := GenerateParameterFromSecurityScheme(&openapi3.SecuritySchemeRef{
|
||||
Value: &openapi3.SecurityScheme{Type: schemeType},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var missing []string
|
||||
err = generateRequestsFromOp(&generateReqOptions{
|
||||
method: http.MethodGet,
|
||||
pathURL: "https://example.com",
|
||||
requestPath: "/protected",
|
||||
op: &openapi3.Operation{},
|
||||
opts: formats.InputFormatOptions{SkipFormatValidation: skipValidation},
|
||||
globalParams: openapi3.Parameters{&openapi3.ParameterRef{Value: param}},
|
||||
missingParamValueCallback: func(param *openapi3.Parameter, _ *generateReqOptions) {
|
||||
missing = append(missing, param.Name)
|
||||
},
|
||||
callback: func(_ *httpTypes.RequestResponse) bool {
|
||||
t.Fatal("must not generate an authenticated request without a token")
|
||||
return false
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"Authorization"}, missing)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func oauthSecurityDocument(paths string) string {
|
||||
return fmt.Sprintf(`{
|
||||
"openapi":"3.0.3",
|
||||
"info":{"title":"Authentication scope test","version":"1.0"},
|
||||
"servers":[{"url":"https://example.com"}],
|
||||
"components":{"securitySchemes":{
|
||||
"oauth":{"type":"oauth2","flows":{"clientCredentials":{"tokenUrl":"https://example.com/token","scopes":{}}}},
|
||||
"key":{"type":"apiKey","name":"X-API-Key","in":"header"}
|
||||
}},
|
||||
"security":[{"oauth":[]}],
|
||||
"paths":{%s}
|
||||
}`, paths)
|
||||
}
|
||||
|
||||
func TestGenerateRequestsOAuthWithoutGlobalToken(t *testing.T) {
|
||||
for _, skipValidation := range []bool{true, false} {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
paths string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "public-only",
|
||||
paths: `"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}`,
|
||||
want: []string{"/public"},
|
||||
},
|
||||
{
|
||||
name: "mixed-security",
|
||||
paths: `
|
||||
"/protected":{"get":{"responses":{"200":{"description":"OK"}}}},
|
||||
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}},
|
||||
"/key":{"get":{"security":[{"key":[]}],"responses":{"200":{"description":"OK"}}}}
|
||||
`,
|
||||
want: []string{"/public", "/key"},
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s/skip-validation=%t", test.name, skipValidation), func(t *testing.T) {
|
||||
parser := New()
|
||||
parser.SetOptions(formats.InputFormatOptions{
|
||||
SkipFormatValidation: skipValidation,
|
||||
Variables: map[string]interface{}{"X-API-Key": "supplied-key"},
|
||||
})
|
||||
var paths []string
|
||||
err := parser.Parse(strings.NewReader(oauthSecurityDocument(test.paths)), func(rr *httpTypes.RequestResponse) bool {
|
||||
paths = append(paths, rr.URL.Path)
|
||||
auth, _ := rr.Request.Headers.Get("Authorization")
|
||||
require.Empty(t, auth)
|
||||
key, _ := rr.Request.Headers.Get("X-Api-Key")
|
||||
if rr.URL.Path == "/key" {
|
||||
require.Equal(t, "supplied-key", key)
|
||||
} else {
|
||||
require.Empty(t, key)
|
||||
}
|
||||
return false
|
||||
}, "")
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, test.want, paths)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRequestsOAuthMissingTokenCLI(t *testing.T) {
|
||||
const helperEnv = "NUCLEI_TEST_OPENAPI_MISSING_TOKEN"
|
||||
if os.Getenv(helperEnv) == "1" {
|
||||
config.CurrentAppMode = config.AppModeCLI
|
||||
document := oauthSecurityDocument(`
|
||||
"/protected/{id}":{"get":{
|
||||
"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],
|
||||
"responses":{"200":{"description":"OK"}}
|
||||
}},
|
||||
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
|
||||
`)
|
||||
err := New().Parse(strings.NewReader(document), func(rr *httpTypes.RequestResponse) bool {
|
||||
fmt.Println("generated:", rr.URL.Path)
|
||||
return false
|
||||
}, "")
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestGenerateRequestsOAuthMissingTokenCLI$")
|
||||
cmd.Env = append(os.Environ(), helperEnv+"=1")
|
||||
cmd.Dir = t.TempDir()
|
||||
output, err := cmd.CombinedOutput()
|
||||
var exitErr *exec.ExitError
|
||||
require.ErrorAs(t, err, &exitErr)
|
||||
require.Equal(t, 1, exitErr.ExitCode())
|
||||
require.Contains(t, string(output), "generated: /public")
|
||||
require.NotContains(t, string(output), "generated: /protected")
|
||||
parameters, err := os.ReadFile(filepath.Join(cmd.Dir, formats.DefaultVarDumpFileName))
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(parameters), "Authorization=")
|
||||
require.Contains(t, string(parameters), "id=")
|
||||
}
|
||||
|
||||
func TestGenerateRequestsCredentials(t *testing.T) {
|
||||
for _, auth := range []struct {
|
||||
scheme string
|
||||
variable string
|
||||
}{
|
||||
{scheme: "oauth", variable: "Authorization"},
|
||||
{scheme: "key", variable: "X-API-Key"},
|
||||
} {
|
||||
for _, scope := range []string{"global", "operation"} {
|
||||
for _, skipValidation := range []bool{false, true} {
|
||||
for _, credential := range []struct {
|
||||
name string
|
||||
present bool
|
||||
value interface{}
|
||||
wantProtected bool
|
||||
}{
|
||||
{name: "absent"},
|
||||
{name: "nil", present: true},
|
||||
{name: "empty", present: true, value: ""},
|
||||
{name: "supplied", present: true, value: "opaque-token", wantProtected: true},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%s/%s/%s/skip-validation=%t", auth.scheme, scope, credential.name, skipValidation), func(t *testing.T) {
|
||||
document := oauthSecurityDocument(`
|
||||
"/protected":{"get":{"responses":{"200":{"description":"OK"}}}},
|
||||
"/public":{"get":{"security":[],"responses":{"200":{"description":"OK"}}}}
|
||||
`)
|
||||
schema, err := openapi3.NewLoader().LoadFromData([]byte(document))
|
||||
require.NoError(t, err)
|
||||
security := openapi3.SecurityRequirements{{auth.scheme: []string{}}}
|
||||
if scope == "global" {
|
||||
schema.Security = security
|
||||
} else {
|
||||
schema.Security = nil
|
||||
schema.Paths.Map()["/protected"].Get.Security = &security
|
||||
}
|
||||
variables := map[string]interface{}{}
|
||||
if credential.present {
|
||||
variables[auth.variable] = credential.value
|
||||
}
|
||||
var paths []string
|
||||
err = GenerateRequestsFromSchema(schema, formats.InputFormatOptions{
|
||||
Variables: variables,
|
||||
SkipFormatValidation: skipValidation,
|
||||
}, func(rr *httpTypes.RequestResponse) bool {
|
||||
paths = append(paths, rr.URL.Path)
|
||||
for _, header := range []string{"Authorization", "X-API-Key"} {
|
||||
value, _ := rr.Request.Headers.Get(http.CanonicalHeaderKey(header))
|
||||
if rr.URL.Path == "/protected" && header == auth.variable && credential.wantProtected {
|
||||
require.Equal(t, credential.value, value)
|
||||
} else {
|
||||
require.Empty(t, value)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
require.NoError(t, err)
|
||||
wantPaths := []string{"/public"}
|
||||
if credential.wantProtected {
|
||||
wantPaths = append(wantPaths, "/protected")
|
||||
}
|
||||
require.ElementsMatch(t, wantPaths, paths)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateParameterUnsupportedSecurityScheme(t *testing.T) {
|
||||
_, err := GenerateParameterFromSecurityScheme(&openapi3.SecuritySchemeRef{
|
||||
Value: &openapi3.SecurityScheme{Type: "mutualTLS"},
|
||||
})
|
||||
require.ErrorContains(t, err, "unsupported security scheme type (mutualTLS)")
|
||||
}
|
||||
Loaded 100 of 368 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user