mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
200
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857cc67702 | ||
|
|
a151a1d54f | ||
|
|
1db63fdb42 | ||
|
|
af61439dc5 | ||
|
|
9a13e64144 | ||
|
|
a5715288aa | ||
|
|
cc5645e674 | ||
|
|
40075b0b20 | ||
|
|
90a0cce807 | ||
|
|
1caa955c77 | ||
|
|
830e012a10 | ||
|
|
9b368f3eaa | ||
|
|
59e6ffa6fb | ||
|
|
b870cdcd67 | ||
|
|
3f776b5ff0 | ||
|
|
ad74f0094c | ||
|
|
4137bedd31 | ||
|
|
a48a58ef59 | ||
|
|
0de51237b7 | ||
|
|
4857883667 | ||
|
|
bccaa4410d | ||
|
|
d8253b97e9 | ||
|
|
65f60dd0a6 | ||
|
|
1115f18b68 | ||
|
|
458367feb7 | ||
|
|
0af8c98679 | ||
|
|
0217339d5d | ||
|
|
d23849e996 | ||
|
|
59dad1b619 | ||
|
|
889c8ff9c5 | ||
|
|
c990724404 | ||
|
|
50e0afd188 | ||
|
|
04d0caa646 | ||
|
|
80a001ca0f | ||
|
|
1b080a7d19 | ||
|
|
a847374f2c | ||
|
|
9c9247895d | ||
|
|
37052a7e3f | ||
|
|
c4825e98c9 | ||
|
|
0c0e886c2c | ||
|
|
b3e02efa7f | ||
|
|
aa3c7a361c | ||
|
|
afb2afaf4f | ||
|
|
6fde18d311 | ||
|
|
62721e589a | ||
|
|
66ef296067 | ||
|
|
496589e7d0 | ||
|
|
bd510ac6bf | ||
|
|
71457b4f69 | ||
|
|
97c8233b72 | ||
|
|
6bb5b5dbd1 | ||
|
|
8f23a8633f | ||
|
|
28d5b3e8c0 | ||
|
|
c2d38083dc | ||
|
|
1884ed920f | ||
|
|
d28dd2c745 | ||
|
|
3336556187 | ||
|
|
2c8f4a0318 | ||
|
|
8c8a1a9d78 | ||
|
|
81749cf750 | ||
|
|
846f4425b0 | ||
|
|
fa876a4d92 | ||
|
|
0ae69a7d53 | ||
|
|
80b58e6e9b | ||
|
|
4d7e91e4e0 | ||
|
|
6b9208cdbf | ||
|
|
26b28c69a4 | ||
|
|
a14b3de2a4 | ||
|
|
3c434fb73a | ||
|
|
f40fa195b6 | ||
|
|
a2b6d2abc7 | ||
|
|
b87b28d7ad | ||
|
|
4d9f2f9867 | ||
|
|
4177ca752b | ||
|
|
8ffccaf639 | ||
|
|
fd1da307b6 | ||
|
|
5674792a7c | ||
|
|
a80d0b881a | ||
|
|
0e243e3edb | ||
|
|
54f9310945 | ||
|
|
3a827b8c8b | ||
|
|
97e13fae6e | ||
|
|
1b8e3515bf | ||
|
|
0de1d12cc9 | ||
|
|
47f74706a5 | ||
|
|
dad93f0ed6 | ||
|
|
3adff6347b | ||
|
|
c5611b9ac4 | ||
|
|
77d3b6d314 | ||
|
|
c36316de36 | ||
|
|
010ee044b2 | ||
|
|
71f9782b2c | ||
|
|
536e427c01 | ||
|
|
ae203e7aad | ||
|
|
2b6b1d3371 | ||
|
|
3fdd8ca091 | ||
|
|
af77e344f0 | ||
|
|
1b244b2330 | ||
|
|
fb1a318c6a | ||
|
|
dbc69f7bf0 | ||
|
|
aed2cb4643 | ||
|
|
15e2d88ca5 | ||
|
|
4d967cba5c | ||
|
|
dece1c5ea9 | ||
|
|
5e5758b7d3 | ||
|
|
06fb71ebf2 | ||
|
|
c50f92fdd3 | ||
|
|
5b6510fb79 | ||
|
|
a5b59a9a4e | ||
|
|
14283b0e13 | ||
|
|
322a40e1db | ||
|
|
aa457f1c44 | ||
|
|
bd0c398887 | ||
|
|
b4a5cdab02 | ||
|
|
1981edbaac | ||
|
|
0e2fb326d6 | ||
|
|
ef0faa7d6f | ||
|
|
e8b53505e5 | ||
|
|
1d6b532bbc | ||
|
|
b6cea874d3 | ||
|
|
36dafa988b | ||
|
|
d7bdb9c058 | ||
|
|
f3c12c43eb | ||
|
|
4dad173f0a | ||
|
|
1b557852c4 | ||
|
|
e5052eb771 | ||
|
|
56411349d2 | ||
|
|
77dda01e67 | ||
|
|
a7faddf748 | ||
|
|
d47ace7701 | ||
|
|
37c06098ad | ||
|
|
12fd62163b | ||
|
|
748ff1381e | ||
|
|
026a1890b8 | ||
|
|
e14933f765 | ||
|
|
66068dbec2 | ||
|
|
bd639a5aff | ||
|
|
fcba75d191 | ||
|
|
33067508b1 | ||
|
|
d4a40d2b04 | ||
|
|
c063d77d2d | ||
|
|
ddb2edb2e9 | ||
|
|
5347be3255 | ||
|
|
59f4635e8c | ||
|
|
8f4c1fff90 | ||
|
|
6138a17677 | ||
|
|
fe14cf5d2c | ||
|
|
89db6cf43a | ||
|
|
a78ea81854 | ||
|
|
666dde153a | ||
|
|
6dfd955715 | ||
|
|
7b14d3db3c | ||
|
|
b69bf17ce1 | ||
|
|
a04aba1022 | ||
|
|
f91f5bb405 | ||
|
|
1cbdf4f4c7 | ||
|
|
97f24a1c3e | ||
|
|
e39543e57b | ||
|
|
c12b7f21c3 | ||
|
|
2d1914d808 | ||
|
|
7c4d90f9d3 | ||
|
|
88f78206b1 | ||
|
|
12aa925acc | ||
|
|
f2e1f2c830 | ||
|
|
f6abdcf2f2 | ||
|
|
37076fdc95 | ||
|
|
87cd437d67 | ||
|
|
f5d0ef81a5 | ||
|
|
911c2303fe | ||
|
|
dd2374604b | ||
|
|
4fb9d22b68 | ||
|
|
38dd02f13b | ||
|
|
fe7b1df128 | ||
|
|
33d71f9156 | ||
|
|
c126e2abcc | ||
|
|
d00461a2cc | ||
|
|
09c1bbf51b | ||
|
|
d7d7cf2b2c | ||
|
|
689026b756 | ||
|
|
d0e6a9d869 | ||
|
|
80f1839aa3 | ||
|
|
fcbaa8f4b2 | ||
|
|
4138b68134 | ||
|
|
f66a832d8e | ||
|
|
dc5f464282 | ||
|
|
2e22d4f612 | ||
|
|
7ebe19d60b | ||
|
|
2ba82f722f | ||
|
|
82071fe0c9 | ||
|
|
550b92da42 | ||
|
|
a34f810ffa | ||
|
|
6d4d1be80c | ||
|
|
46ea619bc6 | ||
|
|
87fbc2d045 | ||
|
|
6ec0e9e52c | ||
|
|
91ffec51a1 | ||
|
|
24a101fbdf | ||
|
|
eb10c9f160 | ||
|
|
e908de14fa | ||
|
|
e30c800e8c |
No files matched your search
@@ -13,8 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go/compat-checks@v1
|
||||
with:
|
||||
go-version: "stable"
|
||||
release-test: true
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
TARGET_URL: "http://honey.scanme.sh/-/?foo=bar"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- run: make build
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
outputs:
|
||||
matrix: ${{ steps.discover.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
with:
|
||||
go-version-file: go.tool.mod
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
GOFUZZ_PACKAGE: ${{ matrix.pkg }}
|
||||
FUZZ_DURATION: 15m
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
with:
|
||||
go-version-file: go.tool.mod
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
if: ${{ !endsWith(github.actor, '[bot]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/setup/git@v1
|
||||
- run: make syntax-docs
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
TARGET_LIST: "/tmp/targets.txt"
|
||||
PROFILE_MEM: "/tmp/nuclei-profile"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- run: |
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
env:
|
||||
OUTPUT: "/tmp/results.sarif"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
with:
|
||||
go-version-file: go.tool.mod
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
if: ${{ github.repository == 'projectdiscovery/nuclei' && !endsWith(github.actor, '[bot]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/setup/git@v1
|
||||
- run: make memogen
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
env:
|
||||
BENCH_OUT: "/tmp/bench.out"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- run: make build-test
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
needs: ["pgo"]
|
||||
runs-on: ubuntu-latest-16-cores
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v8
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
# A workflow can initially run from the branch that adds this file. The
|
||||
# API analyzes the repository default branch, so check out that same
|
||||
# branch before calculating the HEAD we wait for.
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@ 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]') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- uses: projectdiscovery/actions/golangci-lint/v2@v1
|
||||
@@ -42,19 +44,9 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- 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,12 +77,12 @@ jobs:
|
||||
|
||||
sdk:
|
||||
name: "Run example SDK"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- name: "Simple"
|
||||
@@ -106,7 +98,7 @@ jobs:
|
||||
|
||||
integration:
|
||||
name: "Integration tests"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
strategy:
|
||||
@@ -115,10 +107,22 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- 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:
|
||||
@@ -135,7 +139,7 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/python@v1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
@@ -147,26 +151,26 @@ jobs:
|
||||
|
||||
validate:
|
||||
name: "Template validate"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- run: make template-validate
|
||||
|
||||
codeql:
|
||||
name: "CodeQL analysis"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: 'go'
|
||||
@@ -201,18 +205,16 @@ jobs:
|
||||
|
||||
release:
|
||||
name: "Release test"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
with:
|
||||
go-version: "stable"
|
||||
- uses: projectdiscovery/actions/goreleaser@v1
|
||||
|
||||
flamegraph:
|
||||
name: "Flamegraph"
|
||||
needs: ["tests"]
|
||||
needs: ["lint"]
|
||||
uses: ./.github/workflows/flamegraph.yaml
|
||||
|
||||
# perf-regression:
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
|
||||
# Templates
|
||||
/*.yaml
|
||||
!/.sops.yaml
|
||||
!/.golangci.yaml
|
||||
|
||||
# Generated docs
|
||||
/dsl.md
|
||||
|
||||
@@ -12,6 +12,7 @@ builds:
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOEXPERIMENT=greenteagc
|
||||
- GODEBUG=tlssha1=1
|
||||
goos: [windows, linux, darwin]
|
||||
goarch: [amd64, '386', arm, arm64]
|
||||
ignore:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# SOPS Creation Rules for Secret File (ref: https://docs.projectdiscovery.io/tools/nuclei/authenticated-scans)
|
||||
|
||||
creation_rules:
|
||||
- encrypted_regex: ^(password|username|token|value|key|raw)$
|
||||
@@ -33,8 +33,9 @@ clean:
|
||||
|
||||
go-build: clean
|
||||
go-build:
|
||||
CGO_ENABLED=0 $(GOBUILD) -trimpath $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
|
||||
-o '${GOBUILD_OUTPUT}${GOBUILD_OUTPUT_EXT}' $(GOBUILD_PACKAGES)
|
||||
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GODEBUG=tlssha1=1 $(GOBUILD) \
|
||||
-trimpath $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
|
||||
-o '${GOBUILD_OUTPUT}${GOBUILD_OUTPUT_EXT}' $(GOBUILD_PACKAGES)
|
||||
|
||||
build: GOFLAGS = -pgo=auto
|
||||
build: GOBUILD_OUTPUT = ./bin/nuclei
|
||||
@@ -90,8 +91,10 @@ test: GOFLAGS = $(RACE) -v -timeout 1h -count 1
|
||||
test:
|
||||
$(GOTEST) $(GOFLAGS) ./...
|
||||
|
||||
# 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 1h ./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)
|
||||
@@ -109,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
|
||||
@@ -297,4 +300,4 @@ template-validate:
|
||||
./bin/nuclei -validate \
|
||||
-w workflows \
|
||||
-et http/technologies \
|
||||
-ept code
|
||||
-ept code
|
||||
@@ -145,6 +145,8 @@ TARGET:
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
@@ -173,6 +175,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +245,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +274,9 @@ 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")
|
||||
@@ -281,6 +294,8 @@ UNCOVER:
|
||||
RATE-LIMIT:
|
||||
-rl, -rate-limit int maximum number of requests to send per second (default 150)
|
||||
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
|
||||
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
|
||||
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
@@ -324,6 +339,7 @@ DEBUG:
|
||||
-dresp, -debug-resp show all received responses
|
||||
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
|
||||
-pi, -proxy-internal proxy all internal requests
|
||||
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
|
||||
-ldf, -list-dsl-function list all supported DSL function signatures
|
||||
-tlog, -trace-log string file to write sent requests trace log
|
||||
-elog, -error-log string file to write sent requests error log
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
+12
-1
@@ -173,6 +173,14 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -235,7 +243,7 @@ CONFIGURATIONS:
|
||||
-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 allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
@@ -264,6 +272,9 @@ 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")
|
||||
|
||||
@@ -1477,6 +1477,19 @@ DisableCookie is an optional setting that disables cookie reuse
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>disable-http-cache</code> <i>bool</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
DisableHTTPCache turns off HTTP caching for this request. It cannot turn caching on when -http-cache is unset.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>read-all</code> <i>bool</i>
|
||||
|
||||
</div>
|
||||
@@ -2462,6 +2475,136 @@ xpath:
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>prompt</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Prompt is the natural-language question for an llm matcher. The model
|
||||
judges the selected response part and returns a verdict; the matcher
|
||||
fires when the verdict equals Expect with at least MinConfidence.
|
||||
|
||||
{{...}} placeholders are interpolated from the template variables,
|
||||
-var and target. Response derived values are not interpolated, since
|
||||
the response is attacker controlled.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
prompt: Is this a working admin login form rather than a marketing page?
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>expect</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Expect is the verdict that counts as a match for an llm matcher.
|
||||
Defaults to "yes".
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>options</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Options restricts the model to a fixed set of verdicts for an llm
|
||||
matcher. Defaults to yes/no.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>min-confidence</code> <i>float64</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MinConfidence is the minimum confidence (0-1) an llm verdict needs to
|
||||
count as a match. Defaults to 0.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>inputs</code> <i>[]string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
Inputs compares several responses instead of a single part, for
|
||||
checks that are about the difference between responses (blind
|
||||
boolean injection, user enumeration, IDOR). Each entry is resolved
|
||||
against the runtime values, so {{body_1}} and {{body_2}} work with
|
||||
req-condition, and each response is framed separately for the model.
|
||||
Part is ignored when Inputs is set.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
- '{{body_1}}'
|
||||
- '{{body_2}}'
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>max-input-tokens</code> <i>int</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
MaxInputTokens caps how much of the response part is sent to the model
|
||||
for an llm matcher, as an approximate token count.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>allow-sole</code> <i>bool</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
AllowSole permits an llm matcher to be the only matcher on a high or
|
||||
critical template. Without it such a template is rejected, so a model is
|
||||
never the sole arbiter of a severe finding by accident.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -2587,6 +2730,8 @@ Enum Values:
|
||||
- <code>dsl</code>
|
||||
|
||||
- <code>xpath</code>
|
||||
|
||||
- <code>llm</code>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
+18
-1
@@ -273,6 +273,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
|
||||
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
|
||||
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("target-format", "Target-Format",
|
||||
@@ -304,6 +306,14 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableSelfContainedTemplates, "enable-self-contained", "esc", false, "enable loading self-contained templates"),
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
flagSet.IntVar(&options.LLMMaxCalls, "llm-max-calls", 5000, "maximum llm calls per scan (0 for unlimited)"),
|
||||
flagSet.IntVar(&options.LLMConcurrency, "llm-concurrency", 4, "maximum concurrent llm calls"),
|
||||
flagSet.BoolVar(&options.LLMCache, "llm-cache", true, "cache llm responses within a scan"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("filters", "Filtering",
|
||||
@@ -369,7 +379,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVar(&options.ZTLS, "ztls", false, "use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default"), //nolint:all
|
||||
flagSet.StringVar(&options.SNI, "sni", "", "tls sni hostname to use (default: input domain name)"),
|
||||
flagSet.DurationVarP(&options.DialerKeepAlive, "dialer-keep-alive", "dka", 0, "keep-alive duration for network requests."),
|
||||
flagSet.BoolVarP(&options.AllowLocalFileAccess, "allow-local-file-access", "lfa", false, "allows file (payload) access anywhere on the system"),
|
||||
flagSet.BoolVarP(&options.AllowLocalFileAccess, "allow-local-file-access", "lfa", false, "allow all templates in the run to read any file on the system (not just payloads)"),
|
||||
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)"),
|
||||
@@ -400,6 +410,9 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.DASTReport, "dast-report", "dtr", false, "write dast scan report to file"),
|
||||
flagSet.StringVarP(&options.DASTServerToken, "dast-server-token", "dtst", "", "dast server token (optional)"),
|
||||
flagSet.StringVarP(&options.DASTServerAddress, "dast-server-address", "dtsa", "localhost:9055", "dast server address"),
|
||||
flagSet.BoolVarP(&options.DASTProxy, "dast-proxy", "dtp", false, "enable intercepting dast proxy (CA/stats on -dtsa; POST /fuzz needs -dast-server)"),
|
||||
flagSet.StringVarP(&options.DASTProxyAddress, "dast-proxy-address", "dtpa", "127.0.0.1:9056", "dast proxy listen address"),
|
||||
flagSet.StringVarP(&options.DASTProxyAuth, "dast-proxy-auth", "dtpau", "", "dast proxy basic auth in user:pass format (required for non-loopback address)"),
|
||||
flagSet.BoolVarP(&options.DisplayFuzzPoints, "display-fuzz-points", "dfp", false, "display fuzz points in the output for debugging"),
|
||||
flagSet.IntVar(&options.FuzzParamFrequency, "fuzz-param-frequency", 10, "frequency of uninteresting parameters for fuzzing before skipping"),
|
||||
flagSet.StringVarP(&options.FuzzAggressionLevel, "fuzz-aggression", "fa", "low", "fuzzing aggression level controls payload count for fuzz (low, medium, high)"),
|
||||
@@ -419,6 +432,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("rate-limit", "Rate-Limit",
|
||||
flagSet.IntVarP(&options.RateLimit, "rate-limit", "rl", 150, "maximum number of requests to send per second"),
|
||||
flagSet.DurationVarP(&options.RateLimitDuration, "rate-limit-duration", "rld", time.Second, "maximum number of requests to send per second"),
|
||||
flagSet.BoolVar(&options.HostBackoff, "host-backoff", false, "slow down a host that starts blocking (429, repeated 403s, connection failures)"),
|
||||
flagSet.DurationVar(&options.HostBackoffMax, "host-backoff-max", 30*time.Second, "maximum delay applied to a host that is blocking"),
|
||||
flagSet.BoolVar(&options.PerHostRateLimit, "per-host-rate-limit", false, "enable per-host rate limiting (global rate limit becomes unlimited when enabled)"),
|
||||
flagSet.IntVarP(&options.RateLimitMinute, "rate-limit-minute", "rlm", 0, "maximum number of requests to send per minute (DEPRECATED)"),
|
||||
flagSet.IntVarP(&options.BulkSize, "bulk-size", "bs", 25, "maximum number of hosts to be analyzed in parallel per template"),
|
||||
@@ -450,6 +465,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.DisableHTTPProbe, "no-httpx", "nh", false, "disable httpx probing for non-url input"),
|
||||
flagSet.BoolVar(&options.PreflightPortScan, "preflight-portscan", false, "run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)"),
|
||||
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "disable stdin processing"),
|
||||
flagSet.BoolVar(&options.EnableHTTPCache, "http-cache", false, "enable HTTP cache (RFC 9111) for HTTP requests"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("headless", "Headless",
|
||||
@@ -468,6 +484,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.DebugResponse, "debug-resp", "dresp", false, "show all received responses"),
|
||||
flagSet.StringSliceVarP(&options.Proxy, "proxy", "p", nil, "list of http/socks5 proxy to use (comma separated or file input)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.ProxyInternal, "proxy-internal", "pi", false, "proxy all internal requests"),
|
||||
flagSet.StringVarP(&options.ReplayProxy, "replay-proxy", "rp", "", "http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)"),
|
||||
flagSet.BoolVarP(&options.ListDslSignatures, "list-dsl-function", "ldf", false, "list all supported DSL function signatures"),
|
||||
flagSet.StringVarP(&options.TraceLogFile, "trace-log", "tlog", "", "file to write sent requests trace log"),
|
||||
flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"),
|
||||
|
||||
@@ -1,45 +1,11 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.26
|
||||
|
||||
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.72
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectdiscovery/clistats v0.1.6
|
||||
github.com/projectdiscovery/fastdialer v0.5.18
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/interactsh v1.3.1
|
||||
github.com/projectdiscovery/rawhttp v0.1.91
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.24
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
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.57.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/text v0.40.0
|
||||
)
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
carvel.dev/ytt v0.52.0
|
||||
code.gitea.io/sdk/gitea v0.17.0
|
||||
filippo.io/age v1.2.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
|
||||
github.com/Azure/go-ntlmssp v0.1.1
|
||||
@@ -47,40 +13,53 @@ 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
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
github.com/bytedance/sonic v1.15.3
|
||||
github.com/cespare/xxhash v1.1.0
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/clbanning/mxj/v2 v2.7.0
|
||||
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/elazarl/goproxy v1.7.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/getkin/kin-openapi v0.144.0
|
||||
github.com/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
|
||||
@@ -89,64 +68,106 @@ 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.22
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.8.24
|
||||
github.com/projectdiscovery/fastdialer v0.5.25
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/goflags v0.2.0
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
|
||||
github.com/projectdiscovery/goflags v0.2.1
|
||||
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.72
|
||||
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/httpx v1.10.0
|
||||
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.48
|
||||
github.com/projectdiscovery/ratelimit v0.0.88
|
||||
github.com/projectdiscovery/networkpolicy v0.1.54
|
||||
github.com/projectdiscovery/ratelimit v0.0.93
|
||||
github.com/projectdiscovery/rawhttp v0.1.92
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.30
|
||||
github.com/projectdiscovery/sarif v0.1.0
|
||||
github.com/projectdiscovery/tlsx v1.3.2
|
||||
github.com/projectdiscovery/tlsx v1.4.1
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.2
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.95
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/projectdiscovery/utils v0.11.9
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4
|
||||
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
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
google.golang.org/grpc v1.82.1
|
||||
golang.org/x/text v0.41.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
|
||||
)
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.3.0 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/kms v1.25.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.8.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||
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
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
|
||||
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.12.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.13.0 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/akrylysov/pogreb v0.10.2 // indirect
|
||||
@@ -155,27 +176,29 @@ require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.37.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
@@ -183,9 +206,9 @@ require (
|
||||
github.com/bufbuild/protocompile v0.14.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.2 // 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
|
||||
@@ -196,7 +219,8 @@ 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/cloudflare/cfssl v1.6.4 // 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
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
@@ -204,18 +228,22 @@ require (
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/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
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
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
|
||||
@@ -225,8 +253,9 @@ 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
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
@@ -244,23 +273,40 @@ 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
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/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
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gosimple/slug v1.15.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
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
|
||||
@@ -272,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
|
||||
@@ -282,20 +329,23 @@ 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-20250827001030-24949be3fa54 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mackerelio/go-osstat v0.2.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
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
|
||||
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
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
|
||||
@@ -325,22 +375,27 @@ 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.2.50 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.1 // 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.3.2-0.20230802210424-5b0b94c5c0d3 // 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/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.6.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/tidwall/buntdb v1.3.2 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
@@ -356,12 +411,14 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/urfave/cli v1.22.16 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vmihailenco/bufpool v0.1.11 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
|
||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
|
||||
github.com/weppos/publicsuffix-go v0.50.3 // indirect
|
||||
github.com/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
|
||||
@@ -373,60 +430,47 @@ 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/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.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.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
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/mod v0.39.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.1.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.54.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.47.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1,8 +1,12 @@
|
||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||
aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
|
||||
aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
|
||||
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
|
||||
carvel.dev/ytt v0.52.0 h1:tkJPL8Gun5snVfypNXbmMKwnbwMyspcTi3Ypyso3nRY=
|
||||
carvel.dev/ytt v0.52.0/go.mod h1:QgmuU7E15EXW1r2wxTt7zExVz14IHwEG4WNMmaFBkJo=
|
||||
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@@ -18,6 +22,12 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
@@ -25,8 +35,20 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
||||
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@@ -36,15 +58,21 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
|
||||
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
|
||||
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=
|
||||
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=
|
||||
@@ -77,6 +105,14 @@ github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/
|
||||
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
|
||||
github.com/FalconOpsLLC/goexec v0.3.0 h1:ryLMkrGT6asnkqdc5rFMNOSTYdMH/iCfyEuwu0D6ZhA=
|
||||
github.com/FalconOpsLLC/goexec v0.3.0/go.mod h1:kiyxVbmFCGbbwXRyZmOSKlOy7PiK+JH2gq07Ztag/k8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -88,8 +124,8 @@ 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=
|
||||
@@ -97,8 +133,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||
github.com/PuerkitoBio/goquery v1.13.0 h1:mqHbjD7Jmnul4DTR24LKTjo1uUmHUh072kteGV+xpFM=
|
||||
github.com/PuerkitoBio/goquery v1.13.0/go.mod h1:Hip5mdBL8K2wEGKJdr27sRaNwIdDajmCwB/ExUPwW+g=
|
||||
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687 h1:HHkHNwakgAFX3qlMm88c090vIYNFWiO5+4WyAr0xJuM=
|
||||
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687/go.mod h1:l6FRaC2TliS/JMNWskO3J1tmrcKJyOaMFhWC6hHeWno=
|
||||
github.com/RumbleDiscovery/rumble-tools v0.0.0-20201105153123-f2adbb3244d2/go.mod h1:jD2+mU+E2SZUuAOHZvZj4xP4frlOo+N/YrXDvASFhkE=
|
||||
@@ -133,12 +169,14 @@ 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=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/andygrunwald/go-jira v1.16.1 h1:WoQEar5XoDRAibOgKzTFELlPNlKAtnfWr296R9zdFLA=
|
||||
github.com/andygrunwald/go-jira v1.16.1/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
@@ -159,20 +197,20 @@ github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 h1:EO13QJTCD1Ig2IrQnoHTRrn981H9mB7afXsZ89WptI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82/go.mod h1:AGh1NCg0SH+uyJamiJA5tTQcql4MMRDXGRdMmCxCXzY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.2 h1:9/HxDeIgA7DcKK6e6ZaP5PQiXugYbNERx3Z5u30mN+k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.2/go.mod h1:3N1RoxKNcVHmbOKVMMw8pvMs5TUhGYPQP/aq1zmAWqo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
@@ -183,14 +221,16 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3x
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.37.7 h1:dZmNIRtPUvtvUIIDVNpvtnJQ8N8Iqm7SQAxf18htZYw=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.37.7/go.mod h1:vj8PlfJH9mnGeIzd6uMLPi5VgiqzGG7AZoe1kf1uTXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
@@ -209,6 +249,8 @@ github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCk
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
@@ -228,14 +270,14 @@ github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
|
||||
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.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/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/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.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=
|
||||
@@ -270,16 +312,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.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||
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=
|
||||
@@ -297,6 +342,8 @@ github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
@@ -307,8 +354,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=
|
||||
@@ -328,6 +373,8 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@@ -338,13 +385,21 @@ 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=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
@@ -371,12 +426,16 @@ 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=
|
||||
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe h1:QKe/kmAYbndxwu91TcjHERsnMh5SgOB1x/qicvOdUJ8=
|
||||
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
|
||||
github.com/getsops/sops/v3 v3.9.2 h1:Cuuahc/UGu7W3+NXd+mrl+2oXGxxj/jIPsw6Ah2CbxA=
|
||||
github.com/getsops/sops/v3 v3.9.2/go.mod h1:J8CMEdmPWT8gxFaNmPBSrc5NFd4uNndf8aFQwhD/ZR8=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
@@ -448,6 +507,8 @@ github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw=
|
||||
@@ -513,8 +574,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
|
||||
github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs=
|
||||
github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo=
|
||||
github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -531,17 +592,21 @@ 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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
@@ -553,12 +618,18 @@ github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0Z
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
@@ -573,24 +644,45 @@ github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
|
||||
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
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=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
|
||||
github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf h1:umfGUaWdFP2s6457fz1+xXYIWDxdGc7HdkLS9aJ1skk=
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf/go.mod h1:V99KdStnMHZsvVOwIvhfcUzYgYkRZeQWUtumtL+SKxA=
|
||||
github.com/hdm/jarm-go v0.0.7/go.mod h1:kinGoS0+Sdn1Rr54OtanET5E5n7AlD6T6CrJAKDjJSQ=
|
||||
@@ -690,6 +782,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=
|
||||
@@ -703,8 +799,8 @@ github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 h1:z9RDOBcFcf3f2hSfK
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0=
|
||||
github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
@@ -725,8 +821,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=
|
||||
@@ -734,8 +830,8 @@ github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB
|
||||
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
|
||||
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
|
||||
github.com/minio/minio-go/v6 v6.0.46/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg=
|
||||
@@ -746,6 +842,10 @@ github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02G
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
@@ -835,28 +935,30 @@ 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.2.50 h1:KZBYT4jKNvlmlDKJqGMJ7r6XtwAim82oEZLaI+NAWWk=
|
||||
github.com/projectdiscovery/cdncheck v1.2.50/go.mod h1:9oE9KKxCSHNvUf0UaMeqqUwWpC38FkNaTll0ScIBT3w=
|
||||
github.com/projectdiscovery/clistats v0.1.6 h1:e2Hc7cVWKJ4sCbyQ7th5zuBvgqsZcLwRq1qY0vmQXY4=
|
||||
github.com/projectdiscovery/clistats v0.1.6/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.8.22 h1:ZfPunLaaYUJTBffE1ShuAAV9hkSH2thvlszK48FlZS0=
|
||||
github.com/projectdiscovery/dsl v0.8.22/go.mod h1:fr8WMU381As89e4Z+SQ8tYQ1NudniWAS6NLPHrhe+Vs=
|
||||
github.com/projectdiscovery/fastdialer v0.5.18 h1:bBGJdni/xruoUISBug86KwRpuewLg1lEUfQLTg3p0G0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.18/go.mod h1:fH4CDmC7Dk7Shm4v3Rsql7ql3OhlKeZfOdAlc/muP1I=
|
||||
github.com/projectdiscovery/cdncheck v1.3.1 h1:+qPKwqdf3mFtrw3ZoVti7hembmVaapnJBjRosYTNdRo=
|
||||
github.com/projectdiscovery/cdncheck v1.3.1/go.mod h1:5IkUMMlcGaeg1DWW5oJVd3r9X3EpJ88j5HxBeF4Me9M=
|
||||
github.com/projectdiscovery/clistats v0.1.7 h1:NXf4lUzx1QcbJd6QycNwwYHTv2FsW9MchQ2LsgSL6C0=
|
||||
github.com/projectdiscovery/clistats v0.1.7/go.mod h1:RxRGxhMzkT344VTTvZWIrojurUXdQQRN1yfwgg/88I8=
|
||||
github.com/projectdiscovery/dsl v0.8.24 h1:rN2w6LSIjA16qKGRjyj70oxdwXGaspe6sAjEkP7F2fk=
|
||||
github.com/projectdiscovery/dsl v0.8.24/go.mod h1:V+EludvS5PS19UNMlM3CuBwZ+sFnMaQhCFGxysDUanQ=
|
||||
github.com/projectdiscovery/fastdialer v0.5.25 h1:xGFpc5B7cmogqnBrcVYV5yXh6UFlqRA6u5KqautKgD0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.25/go.mod h1:CZZ0PwdTw2KCI/PrZTW9wyBY88MIjpT8WhsKWSw3bkw=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
|
||||
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
|
||||
github.com/projectdiscovery/freeport v0.0.7/go.mod h1:cOhWKvNBe9xM6dFJ3RrrLvJ5vXx2NQ36SecuwjenV2k=
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c h1:s+lLAlrOrgwlPZQ9DFqNw+kia2nteKnJZ2Ek313yoUc=
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c/go.mod h1:rN35/D3lVx2YDeENFFz06uj8j3XIqK1Ym9XcISF5fzg=
|
||||
github.com/projectdiscovery/goflags v0.2.0 h1:zj16hnTmLmlnVYm02hfOrCY6oYTu/AQDBE99yZkS+s8=
|
||||
github.com/projectdiscovery/goflags v0.2.0/go.mod h1:7nAP1r2Dqgn/rwmOE3EWbZWUCEJKNIhVSBGpuzJAIns=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534 h1:hYd1zQA/dxO2ASyQ6Re73TcJkW1LjLQvt4+86Hxefz8=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goflags v0.2.1 h1:XnSYvsCjUwHbZaaJUZ3YznCJPoXRk/IhTFP9Gecmx6M=
|
||||
github.com/projectdiscovery/goflags v0.2.1/go.mod h1:7nAP1r2Dqgn/rwmOE3EWbZWUCEJKNIhVSBGpuzJAIns=
|
||||
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.72 h1:PKk+aSx3jYOCBPi+FD+nTnhe+Vxx9u3K7Ega0GVlMCI=
|
||||
github.com/projectdiscovery/gologger v1.1.72/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
|
||||
github.com/projectdiscovery/gologger v1.1.73 h1:WX2nzq29cL7HdvOHqIttacoid/5lMwapyM7ukGU1jFY=
|
||||
github.com/projectdiscovery/gologger v1.1.73/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
|
||||
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
|
||||
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
|
||||
@@ -865,10 +967,12 @@ github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70b
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
|
||||
github.com/projectdiscovery/hmap v0.0.102 h1:ybPuf7UQvuOLiHLOUbVMvd+PSlItQU5iQpP1Erl8kaU=
|
||||
github.com/projectdiscovery/hmap v0.0.102/go.mod h1:IZ92wXFmxYpdn0euZ7VFmypzOBNDwq8c3yRyIIol6XE=
|
||||
github.com/projectdiscovery/httpx v1.10.0 h1:sZvfeJDeX4eJfB5Oh29My4gOHmLfvpCt2wCqhargJrE=
|
||||
github.com/projectdiscovery/httpx v1.10.0/go.mod h1:gfdNnFznLLlAgAgSQlYjUeEVvYzKrNV0jO5Qyo5w+FI=
|
||||
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
|
||||
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
|
||||
github.com/projectdiscovery/httpx v1.12.0 h1:NN5iDrw7NjPBDlACdjac4lePTQmZ5aOmgOzptMpP44c=
|
||||
github.com/projectdiscovery/httpx v1.12.0/go.mod h1:OgtyibNGdu9BrJxRdNKm280Davw1Z4s2FAhfPNNtLB4=
|
||||
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=
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb/go.mod h1:vmgC0DTFCfoCLp0RAfsfYTZZan0QMVs+cmTbH6blfjk=
|
||||
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 h1:eR+0HE//Ciyfwy3HC7fjRyKShSJHYoX2Pv7pPshjK/Q=
|
||||
@@ -877,34 +981,34 @@ 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.48 h1:s/d0nEyfkHYVBeZI/bgRiOCS66KDxv3+s7Hb+AvepHY=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.48/go.mod h1:bb2ZkDnOeVuR9Ok8BdCdoBEz8S9Y9EFvFFcn5FTUV2U=
|
||||
github.com/projectdiscovery/ratelimit v0.0.88 h1:AcurW9aLRzlEyPe9kSjnOpr3XzLMWTpiWAlW/w73ALU=
|
||||
github.com/projectdiscovery/ratelimit v0.0.88/go.mod h1:CU1s+68UUG2mctSl2wi32/DHLJA6TMg+4rxgP59LfVk=
|
||||
github.com/projectdiscovery/rawhttp v0.1.91 h1:6EIhZxCkBn27kJAl47FieCFCe4H2ZdbhoBVj0J6ZCVY=
|
||||
github.com/projectdiscovery/rawhttp v0.1.91/go.mod h1:VZYAM25UI/wVB3URZ95ZaftgOnsbphxyAw/XnQRRz4Y=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.54 h1:+v7yyuRBu2S7dzeIJToDENy1i+HjRhNzqnQrDlm00z8=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.54/go.mod h1:tN+YAFVlJjHv/znPEhkcSAjmBMkMhslC9XpWKTrKEHw=
|
||||
github.com/projectdiscovery/ratelimit v0.0.93 h1:k2rRpCR31DqeqrORSgGzJ+z0T7+QGCt0Z0+Ov/TVrgk=
|
||||
github.com/projectdiscovery/ratelimit v0.0.93/go.mod h1:Tqpx1QKdFI63L01T2gpSvV5Uro9CnZkZWr80ciSXSBY=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92 h1:38GRsVLNllES/dZcEcA6s6P+rYBp34H9mDphMJMDvvg=
|
||||
github.com/projectdiscovery/rawhttp v0.1.92/go.mod h1:spfXTPjxCj+bfWLynMVB7uOb+LS3PDNAI9HexMEV1Zg=
|
||||
github.com/projectdiscovery/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.24 h1:bODdeUEka/U5SS9wfnhoAjBRg1wq/AJdwb9CUNIOR88=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.24/go.mod h1:OL5zZEBd59GXEMaDjEHW/bfGlFsV441ADz64hWajgxc=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.30 h1:YnDCZP04/AKTiIBT5vA1nVhy5cRW5dPy1Du5nCjxHy4=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.30/go.mod h1:MXn1MzThNmHc05KldUbfks1MvwAnUKR4GGJcFhVIc08=
|
||||
github.com/projectdiscovery/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.3.2 h1:1Lh2ith79o8R+rOOSBZiMq8EiZ8j4D3T/8ENO+gAqXA=
|
||||
github.com/projectdiscovery/tlsx v1.3.2/go.mod h1:2wgTGC/sourHvoR+RX8cAok0Sa8YK6QbEP3xo5SDzPI=
|
||||
github.com/projectdiscovery/tlsx v1.4.1 h1:QuECHVJERBirNrmpjgIOAFQScjy/DxVjzXeL+L81mH0=
|
||||
github.com/projectdiscovery/tlsx v1.4.1/go.mod h1:WCrUXAHjdKbDNjsu5EbVBS7qvzap3fxi/E85v/Flyx8=
|
||||
github.com/projectdiscovery/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.2 h1:gfvXFBEHN4DgX8HBRzsoO1s2wH/2R+cH17cLpC865x4=
|
||||
github.com/projectdiscovery/utils v0.11.2/go.mod h1:HMxhxLigsAr+M9Oa8n9Z0ROZcs8wAJHGsgR7UGb+oUE=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.95 h1:XHjbQVrrxusezFq7ciWmtznPKWhpr/srWp7Yk2noxqg=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.95/go.mod h1:E2p8L90ysTTUkU5FUOgGoCQ7ucEUgqJ/tvYgiGGAlEM=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6 h1:GCEdIRlQjDux28xTXKszM7n3jlMf152d5nqVpVoetas=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6/go.mod h1:R5lWrNzP+7Oyn77NDVPnBsxx2/FyQZBBkIAaSaCQFxw=
|
||||
github.com/projectdiscovery/utils v0.11.9 h1:aKLje3zPcZuOGmopN51PasWxPX/CypPwzP2ke/Qa2bA=
|
||||
github.com/projectdiscovery/utils v0.11.9/go.mod h1:d3BcbsHhuxW9T3jpvrgzwshvqhaL9qPSkoi+3h/DY8E=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4 h1:yO+xel+C0d/vOFWcUKVZRpIDAfcEmsjpVDjQ0jifAUw=
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.4/go.mod h1:6xTRCUQhX8m+L1eqJDQIALJlRv2Bnhrmku80YU9bBXE=
|
||||
github.com/projectdiscovery/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=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
@@ -929,8 +1033,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=
|
||||
@@ -946,17 +1050,21 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/sandrolain/httpcache v1.4.0 h1:Jf4Vx62X2ybvNPSpPvI1kT3xvMdDG1AsApQjOQKO9E0=
|
||||
github.com/sandrolain/httpcache v1.4.0/go.mod h1:kHBuXveitSn39SNPBhdf/ybG272X706HJ2RJqOQ+Em0=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/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.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
@@ -992,8 +1100,8 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
@@ -1012,6 +1120,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
@@ -1061,6 +1170,8 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
|
||||
@@ -1133,6 +1244,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=
|
||||
@@ -1143,6 +1256,8 @@ 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=
|
||||
@@ -1170,24 +1285,34 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
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/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/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/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.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=
|
||||
@@ -1222,8 +1347,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
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/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=
|
||||
@@ -1261,8 +1386,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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||
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=
|
||||
@@ -1312,8 +1437,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
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=
|
||||
@@ -1443,13 +1568,13 @@ 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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
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/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=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1496,8 +1621,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1520,6 +1645,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.264.0 h1:+Fo3DQXBK8gLdf8rFZ3uLu39JpOnhvzJrLMQSoSYZJM=
|
||||
google.golang.org/api v0.264.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1556,8 +1683,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1570,8 +1701,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/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=
|
||||
@@ -1602,6 +1733,8 @@ gopkg.in/corvus-ch/zbase32.v1 v1.0.0/go.mod h1:T3oKkPOm4AV/bNXCNFUxRmlE9RUyBz/DS
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module nuclei-tools
|
||||
|
||||
go 1.26.1
|
||||
go 1.27.1
|
||||
|
||||
tool (
|
||||
github.com/dvyukov/go-fuzz/go-fuzz
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type ignoreFileLogRecord struct {
|
||||
message string
|
||||
level levels.Level
|
||||
}
|
||||
|
||||
type ignoreFileLogWriter struct {
|
||||
records []ignoreFileLogRecord
|
||||
}
|
||||
|
||||
func (w *ignoreFileLogWriter) Write(data []byte, level levels.Level) {
|
||||
w.records = append(w.records, ignoreFileLogRecord{message: string(data), level: level})
|
||||
}
|
||||
|
||||
func newIgnoreFileTestRunner(t *testing.T, root string, writer *ignoreFileLogWriter) *Runner {
|
||||
t.Helper()
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
logger := &gologger.Logger{}
|
||||
logger.SetFormatter(formatter.NewCLI(false))
|
||||
logger.SetWriter(writer)
|
||||
logger.SetMaxLevel(levels.LevelWarning)
|
||||
|
||||
options := types.DefaultOptions()
|
||||
options.Logger = logger
|
||||
return &Runner{options: options, Logger: logger}
|
||||
}
|
||||
|
||||
func TestLoadIgnoreFileWarnsAndContinuesWhenActiveFileIsMissing(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writer := &ignoreFileLogWriter{}
|
||||
runner := newIgnoreFileTestRunner(t, root, writer)
|
||||
|
||||
require.NoError(t, runner.loadIgnoreFile())
|
||||
require.Empty(t, runner.options.ExcludeTags)
|
||||
require.Empty(t, runner.options.ExcludedTemplates)
|
||||
require.Len(t, writer.records, 1)
|
||||
require.Equal(t, levels.LevelWarning, writer.records[0].level)
|
||||
require.Contains(t, writer.records[0].message, strconv.Quote(filepath.Join(root, config.NucleiIgnoreFileName)))
|
||||
require.Contains(t, writer.records[0].message, "continuing without ignore exclusions")
|
||||
}
|
||||
|
||||
func TestLoadIgnoreFileRejectsCorruptActiveFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, config.NucleiIgnoreFileName)
|
||||
require.NoError(t, os.WriteFile(path, []byte("tags: ["), 0o600))
|
||||
writer := &ignoreFileLogWriter{}
|
||||
runner := newIgnoreFileTestRunner(t, root, writer)
|
||||
|
||||
err := runner.loadIgnoreFile()
|
||||
require.ErrorContains(t, err, "error parsing")
|
||||
require.ErrorContains(t, err, strconv.Quote(path))
|
||||
require.Empty(t, writer.records)
|
||||
}
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/helpers/writer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
||||
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/utils/env"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
@@ -79,6 +81,8 @@ func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret
|
||||
}
|
||||
data := map[string]interface{}{}
|
||||
tmpl := tmpls[0]
|
||||
restore := skipSecretFileOnAuthTemplate(tmpl)
|
||||
defer restore()
|
||||
// add args to tmpl here
|
||||
vars := map[string]interface{}{}
|
||||
mainCtx := context.Background()
|
||||
@@ -149,3 +153,46 @@ func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret
|
||||
return finalErr
|
||||
}
|
||||
}
|
||||
|
||||
// skipSecretFileOnAuthTemplate disables secret-file auth on the login template for
|
||||
// the duration of the fetch. Login requests that hit the same domain as the
|
||||
// secret would otherwise ApplyAuth → Fetch and deadlock on sync.Once (#7639).
|
||||
// Previous SkipSecretFile values are restored so a cached template is unchanged.
|
||||
func skipSecretFileOnAuthTemplate(tmpl *templates.Template) func() {
|
||||
if tmpl == nil {
|
||||
return func() {}
|
||||
}
|
||||
var reqs []*httpprotocol.Request
|
||||
seen := make(map[*httpprotocol.Request]struct{})
|
||||
add := func(r *httpprotocol.Request) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[r]; ok {
|
||||
return
|
||||
}
|
||||
seen[r] = struct{}{}
|
||||
reqs = append(reqs, r)
|
||||
}
|
||||
for _, r := range tmpl.RequestsHTTP {
|
||||
add(r)
|
||||
}
|
||||
for _, r := range tmpl.RequestsWithHTTP {
|
||||
add(r)
|
||||
}
|
||||
for _, req := range tmpl.RequestsQueue {
|
||||
if r, ok := req.(*httpprotocol.Request); ok {
|
||||
add(r)
|
||||
}
|
||||
}
|
||||
prev := make([]bool, len(reqs))
|
||||
for i, r := range reqs {
|
||||
prev[i] = r.SkipSecretFile
|
||||
r.SkipSecretFile = true
|
||||
}
|
||||
return func() {
|
||||
for i, r := range reqs {
|
||||
r.SkipSecretFile = prev[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -137,3 +140,27 @@ func TestCallbackDynamicValues(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "sess-xyz-123", data["session"], "should pick the longest value")
|
||||
}
|
||||
|
||||
func TestSkipSecretFileOnAuthTemplate(t *testing.T) {
|
||||
t.Run("nil template is a no-op", func(t *testing.T) {
|
||||
restore := skipSecretFileOnAuthTemplate(nil)
|
||||
require.NotPanics(t, restore)
|
||||
})
|
||||
|
||||
t.Run("sets skip during fetch and restores previous values", func(t *testing.T) {
|
||||
alreadySkipped := &httpprotocol.Request{SkipSecretFile: true}
|
||||
needsSkip := &httpprotocol.Request{}
|
||||
tmpl := &templates.Template{
|
||||
RequestsHTTP: []*httpprotocol.Request{alreadySkipped, needsSkip},
|
||||
}
|
||||
tmpl.RequestsQueue = []protocols.Request{alreadySkipped, needsSkip}
|
||||
|
||||
restore := skipSecretFileOnAuthTemplate(tmpl)
|
||||
require.True(t, alreadySkipped.SkipSecretFile)
|
||||
require.True(t, needsSkip.SkipSecretFile)
|
||||
|
||||
restore()
|
||||
require.True(t, alreadySkipped.SkipSecretFile, "templates that already skipped secret-file must stay skipped")
|
||||
require.False(t, needsSkip.SkipSecretFile, "scan-time ApplyAuth must remain enabled after login fetch")
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"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/protocols/common/protocolinit"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
@@ -170,6 +171,9 @@ func ValidateOptions(options *types.Options) error {
|
||||
if err := loadProxyServers(options); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReplayProxy(options.ReplayProxy); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Validate {
|
||||
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
|
||||
}
|
||||
@@ -296,6 +300,11 @@ func validateDASTOptions(options *types.Options) error {
|
||||
if len(options.DASTServerToken) > 0 && len(options.DASTServerToken) < 16 {
|
||||
return fmt.Errorf("DAST server token must be at least 16 characters long")
|
||||
}
|
||||
if options.DASTProxy {
|
||||
if _, _, err := proxy.ParseAuth(options.DASTProxyAuth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -65,3 +65,21 @@ func loadProxyServers(options *types.Options) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateReplayProxy rejects a -replay-proxy value the replay client cannot dial
|
||||
func validateReplayProxy(raw string) error {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
proxyURL, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "invalid replay proxy %q", raw)
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS, proxyutils.SOCKS5:
|
||||
if proxyURL.Host != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid replay proxy %q: want an http, https or socks5 URL", raw)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateReplayProxy(t *testing.T) {
|
||||
for _, valid := range []string{"", "http://127.0.0.1:8080", "https://proxy.example:8443", "socks5://127.0.0.1:1080"} {
|
||||
require.NoError(t, validateReplayProxy(valid), valid)
|
||||
}
|
||||
for _, invalid := range []string{"127.0.0.1:8080", "ftp://127.0.0.1:21", "http://", "://bad"} {
|
||||
require.Error(t, validateReplayProxy(invalid), invalid)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/pdcp"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
@@ -45,6 +47,7 @@ import (
|
||||
fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
parsers "github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/projectfile"
|
||||
@@ -53,6 +56,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
|
||||
@@ -108,6 +112,7 @@ type Runner struct {
|
||||
httpApiEndpoint *httpapi.Server
|
||||
fuzzStats *fuzzStats.Tracker
|
||||
dastServer *server.DASTServer
|
||||
llmClient llmclient.Client
|
||||
}
|
||||
|
||||
// New creates a new client for running the enumeration process.
|
||||
@@ -121,6 +126,12 @@ func New(options *types.Options) (*Runner, error) {
|
||||
return nil, fmt.Errorf("initialize nuclei configuration: %w", err)
|
||||
}
|
||||
|
||||
llmClient, err := configureLLM(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("configure llm: %w", err)
|
||||
}
|
||||
runner.llmClient = llmClient
|
||||
|
||||
if options.HealthCheck {
|
||||
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
|
||||
os.Exit(0)
|
||||
@@ -359,14 +370,14 @@ func New(options *types.Options) (*Runner, error) {
|
||||
|
||||
runner.resumeCfg = resumeCfg
|
||||
|
||||
if options.DASTReport || options.DASTServer {
|
||||
if options.DASTReport || options.DASTServer || options.DASTProxy {
|
||||
var err error
|
||||
runner.fuzzStats, err = fuzzStats.NewTracker()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create fuzz stats db")
|
||||
}
|
||||
|
||||
if !options.DASTServer {
|
||||
if !options.DASTServer && !options.DASTProxy {
|
||||
dastServer, err := server.NewStatsServer(runner.fuzzStats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create dast server")
|
||||
@@ -577,7 +588,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
|
||||
// If the user has asked for DAST server mode, run the live
|
||||
// DAST fuzzing server.
|
||||
if r.options.DASTServer {
|
||||
if r.options.DASTServer || r.options.DASTProxy {
|
||||
execurOpts := &server.NucleiExecutorOptions{
|
||||
Options: r.options,
|
||||
Output: r.output,
|
||||
@@ -594,7 +605,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
FuzzStatsDB: r.fuzzStats,
|
||||
Logger: r.Logger,
|
||||
}
|
||||
dastServer, err := server.New(&server.Options{
|
||||
serverOptions := &server.Options{
|
||||
Address: r.options.DASTServerAddress,
|
||||
Templates: r.options.Templates,
|
||||
OutputWriter: r.output,
|
||||
@@ -603,11 +614,28 @@ func (r *Runner) RunEnumeration() error {
|
||||
InScope: r.options.Scope,
|
||||
OutScope: r.options.OutOfScope,
|
||||
NucleiExecutorOptions: execurOpts,
|
||||
})
|
||||
EnableFuzzAPI: r.options.DASTServer,
|
||||
ForwardProxy: r.options.AliveHttpProxy,
|
||||
}
|
||||
if r.options.DASTProxy {
|
||||
username, password, err := proxy.ParseAuth(r.options.DASTProxyAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverOptions.ProxyAddress = r.options.DASTProxyAddress
|
||||
serverOptions.ProxyCADir = config.DefaultConfig.GetConfigDir()
|
||||
serverOptions.ProxyUsername = username
|
||||
serverOptions.ProxyPassword = password
|
||||
}
|
||||
|
||||
dastServer, err := server.New(serverOptions)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.dastServer = dastServer
|
||||
|
||||
return dastServer.Start()
|
||||
}
|
||||
|
||||
@@ -617,16 +645,18 @@ func (r *Runner) RunEnumeration() error {
|
||||
r.options.Templates = append(r.options.Templates, arr...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.options.NewTemplatesWithVersion) > 0 {
|
||||
if arr := installer.GetNewTemplatesInVersions(r.options.NewTemplatesWithVersion...); len(arr) > 0 {
|
||||
r.options.Templates = append(r.options.Templates, arr...)
|
||||
}
|
||||
}
|
||||
// Exclude ignored file for validation
|
||||
|
||||
// Apply the active ignore policy only when templates can execute.
|
||||
if !r.options.Validate {
|
||||
ignoreFile := config.ReadIgnoreFile()
|
||||
r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
|
||||
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
|
||||
if err := r.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fuzzFreqCache := frequency.New(frequency.DefaultMaxTrackCount, r.options.FuzzParamFrequency)
|
||||
@@ -637,6 +667,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts := &protocols.ExecutorOptions{
|
||||
Output: r.output,
|
||||
Options: r.options,
|
||||
LLMClient: r.llmClient,
|
||||
Progress: r.progress,
|
||||
Catalog: r.catalog,
|
||||
IssuesClient: r.issuesClient,
|
||||
@@ -697,6 +728,10 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if r.options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
|
||||
}
|
||||
|
||||
executorEngine := core.New(r.options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -708,6 +743,13 @@ func (r *Runner) RunEnumeration() error {
|
||||
|
||||
// If using input-file flags, only load http fuzzing based templates.
|
||||
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
|
||||
profiles := r.targetProfiles()
|
||||
if profiles != nil {
|
||||
if r.options.AutomaticScan {
|
||||
return errors.New("per-target profiles cannot be combined with automatic scan")
|
||||
}
|
||||
loaderConfig.TargetFilter = profiles.LoadFilter()
|
||||
}
|
||||
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
r.options.DAST = true
|
||||
@@ -752,6 +794,10 @@ func (r *Runner) RunEnumeration() error {
|
||||
if err := store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
if profiles != nil {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
}
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
@@ -886,6 +932,24 @@ func (r *Runner) RunEnumeration() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Runner) loadIgnoreFile() error {
|
||||
ignoreFile, err := config.ReadIgnoreFile()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
r.Logger.Warning().Msgf("Could not read active .nuclei-ignore file: %s; continuing without ignore exclusions", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
|
||||
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shortDur(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return d.String()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
)
|
||||
|
||||
// targetProfiles returns the per-target profiles read from input, or nil when
|
||||
// no target has one.
|
||||
func (r *Runner) targetProfiles() *targetprofile.Registry {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
TargetProfiles() *targetprofile.Registry
|
||||
})
|
||||
if !ok || !provider.TargetProfiles().Scoped() {
|
||||
return nil
|
||||
}
|
||||
return provider.TargetProfiles()
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
@@ -106,6 +107,10 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if opts.Options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
|
||||
}
|
||||
|
||||
executorEngine := core.New(opts.Options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -117,7 +122,7 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
|
||||
// If using input-file flags, only load http fuzzing based templates.
|
||||
loaderConfig := loader.NewConfig(opts.Options, opts.Catalog, executorOpts)
|
||||
if (!strings.EqualFold(opts.Options.InputFileMode, "list") || opts.Options.DAST || opts.Options.DASTServer) && !opts.Options.OfflineHTTP {
|
||||
if (!strings.EqualFold(opts.Options.InputFileMode, "list") || opts.Options.DAST || opts.Options.DASTServer || opts.Options.DASTProxy) && !opts.Options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
opts.Options.DAST = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
)
|
||||
|
||||
const (
|
||||
caCertFileName = "dast-proxy-ca.pem"
|
||||
caKeyFileName = "dast-proxy-ca.key"
|
||||
|
||||
caValidity = 2 * 365 * 24 * time.Hour
|
||||
|
||||
// The key can mint a certificate that the user's browser trusts for any
|
||||
// host, so it must never be readable by other accounts on the machine.
|
||||
caKeyFileMode os.FileMode = 0o600
|
||||
caCertFileMode os.FileMode = 0o644
|
||||
)
|
||||
|
||||
// CA is the certificate authority used to sign intercepted TLS connections.
|
||||
type CA struct {
|
||||
Certificate *tls.Certificate
|
||||
CertPath string
|
||||
CertPEM []byte
|
||||
}
|
||||
|
||||
// LoadOrCreateCA loads the interception CA from dir, generating and persisting
|
||||
// a new one on first use.
|
||||
//
|
||||
// goproxy ships a built-in CA whose private key is published with the library;
|
||||
// it is deliberately never used here. A per-installation key is the only thing
|
||||
// stopping a third party from minting certificates the user already trusts.
|
||||
func LoadOrCreateCA(dir string) (*CA, error) {
|
||||
certPath := filepath.Join(dir, caCertFileName)
|
||||
keyPath := filepath.Join(dir, caKeyFileName)
|
||||
|
||||
certExists, keyExists := fileutil.FileExists(certPath), fileutil.FileExists(keyPath)
|
||||
if certExists && keyExists {
|
||||
return loadCA(certPath, keyPath)
|
||||
}
|
||||
// Half a keypair is unusable, and regenerating over it would invalidate a
|
||||
// certificate the user may already have installed and trusted. Say which
|
||||
// file is orphaned instead of guessing.
|
||||
if certExists != keyExists {
|
||||
orphan := certPath
|
||||
if keyExists {
|
||||
orphan = keyPath
|
||||
}
|
||||
return nil, errors.Errorf("dast proxy CA is incomplete, remove %s to generate a new keypair", orphan)
|
||||
}
|
||||
return createCA(dir, certPath, keyPath)
|
||||
}
|
||||
|
||||
func loadCA(certPath, keyPath string) (*CA, error) {
|
||||
if err := verifyKeyPermissions(keyPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not read dast proxy CA certificate")
|
||||
}
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not read dast proxy CA key")
|
||||
}
|
||||
|
||||
certificate, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse dast proxy CA keypair")
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(certificate.Certificate[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse dast proxy CA certificate")
|
||||
}
|
||||
if !leaf.IsCA {
|
||||
return nil, errors.Errorf("%s is not a certificate authority", certPath)
|
||||
}
|
||||
// Regenerating silently would swap the CA out from under a certificate the
|
||||
// user has already installed and trusted, so this is left to the user.
|
||||
if time.Now().After(leaf.NotAfter) {
|
||||
return nil, errors.Errorf("dast proxy CA expired on %s, remove %s and %s to generate a new one",
|
||||
leaf.NotAfter.Format(time.RFC3339), certPath, keyPath)
|
||||
}
|
||||
certificate.Leaf = leaf
|
||||
|
||||
return &CA{Certificate: &certificate, CertPath: certPath, CertPEM: certPEM}, nil
|
||||
}
|
||||
|
||||
func createCA(dir, certPath, keyPath string) (*CA, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, errors.Wrap(err, "could not create dast proxy CA directory")
|
||||
}
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not generate dast proxy CA key")
|
||||
}
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not generate dast proxy CA serial number")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "Nuclei DAST Proxy CA",
|
||||
Organization: []string{"ProjectDiscovery"},
|
||||
},
|
||||
NotBefore: now.Add(-1 * time.Hour), // tolerate clock skew on the client
|
||||
NotAfter: now.Add(caValidity),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
// Sign leaf certificates only: a CA reachable through this one must not
|
||||
// be able to issue further authorities.
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create dast proxy CA certificate")
|
||||
}
|
||||
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not marshal dast proxy CA key")
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
||||
|
||||
// O_EXCL so a pre-existing path, including a symlink planted by another
|
||||
// user, can never be written through.
|
||||
if err := writeNewFile(keyPath, keyPEM, caKeyFileMode); err != nil {
|
||||
return nil, errors.Wrap(err, "could not write dast proxy CA key")
|
||||
}
|
||||
if err := writeNewFile(certPath, certPEM, caCertFileMode); err != nil {
|
||||
// Leaving the key behind would wedge every later start on the
|
||||
// incomplete-keypair check above.
|
||||
_ = os.Remove(keyPath)
|
||||
return nil, errors.Wrap(err, "could not write dast proxy CA certificate")
|
||||
}
|
||||
|
||||
leaf, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not parse generated dast proxy CA certificate")
|
||||
}
|
||||
return &CA{
|
||||
Certificate: &tls.Certificate{Certificate: [][]byte{der}, PrivateKey: privateKey, Leaf: leaf},
|
||||
CertPath: certPath,
|
||||
CertPEM: certPEM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func writeNewFile(path string, contents []byte, mode os.FileMode) error {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := file.Write(contents); err != nil {
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func verifyKeyPermissions(keyPath string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(keyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not stat dast proxy CA key")
|
||||
}
|
||||
if mode := info.Mode().Perm(); mode&0o077 != 0 {
|
||||
return errors.Errorf("dast proxy CA key %s is readable by other users (%#o), run: chmod 600 %s",
|
||||
keyPath, mode, keyPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadOrCreateCAGeneratesUsableAuthority(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
ca, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ca.Certificate.Leaf)
|
||||
require.True(t, ca.Certificate.Leaf.IsCA)
|
||||
require.True(t, ca.Certificate.Leaf.MaxPathLenZero, "CA must not be able to issue sub authorities")
|
||||
require.Contains(t, string(ca.CertPEM), "BEGIN CERTIFICATE")
|
||||
require.NotContains(t, string(ca.CertPEM), "PRIVATE KEY")
|
||||
require.Equal(t, filepath.Join(dir, caCertFileName), ca.CertPath)
|
||||
}
|
||||
|
||||
func TestLoadOrCreateCAKeyIsOwnerReadableOnly(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("unix file modes only")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
_, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := os.Stat(filepath.Join(dir, caKeyFileName))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, caKeyFileMode, info.Mode().Perm())
|
||||
}
|
||||
|
||||
func TestLoadOrCreateCAReusesExistingAuthority(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
first, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err)
|
||||
second, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, first.Certificate.Leaf.SerialNumber, second.Certificate.Leaf.SerialNumber)
|
||||
}
|
||||
|
||||
func TestLoadOrCreateCARejectsGroupReadableKey(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("unix file modes only")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
|
||||
_, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chmod(filepath.Join(dir, caKeyFileName), 0o644))
|
||||
|
||||
_, err = LoadOrCreateCA(dir)
|
||||
require.ErrorContains(t, err, "readable by other users")
|
||||
}
|
||||
|
||||
func TestLoadOrCreateCARejectsIncompleteKeypair(t *testing.T) {
|
||||
for _, orphan := range []string{caCertFileName, caKeyFileName} {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, orphan), []byte("x"), 0o600))
|
||||
|
||||
_, err := LoadOrCreateCA(dir)
|
||||
require.ErrorContains(t, err, "incomplete", orphan)
|
||||
require.ErrorContains(t, err, orphan)
|
||||
}
|
||||
}
|
||||
|
||||
// A failed certificate write must not leave the key behind, or the
|
||||
// incomplete-keypair check would wedge every later start.
|
||||
func TestLoadOrCreateCACleansUpKeyWhenCertificateWriteFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// A directory at the certificate path makes the write fail.
|
||||
require.NoError(t, os.Mkdir(filepath.Join(dir, caCertFileName), 0o755))
|
||||
|
||||
_, err := LoadOrCreateCA(dir)
|
||||
require.Error(t, err)
|
||||
require.NoFileExists(t, filepath.Join(dir, caKeyFileName), "orphaned key must be removed")
|
||||
|
||||
require.NoError(t, os.Remove(filepath.Join(dir, caCertFileName)))
|
||||
ca, err := LoadOrCreateCA(dir)
|
||||
require.NoError(t, err, "startup must recover once the blocker is gone")
|
||||
require.True(t, ca.Certificate.Leaf.IsCA)
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
// Package proxy implements an intercepting HTTP proxy that feeds live traffic
|
||||
// into the nuclei DAST scanner. Requests are forwarded untouched; a copy of
|
||||
// each intercepted request is handed to the scanner out of band.
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
)
|
||||
|
||||
const (
|
||||
// Requests with a larger body are not mirrored at all: see captureRequest.
|
||||
maxCapturedBodySize = 4 << 20
|
||||
|
||||
readHeaderTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// hopByHopHeaders are stripped from mirrored requests: they belong to the
|
||||
// client-to-proxy leg and must not be replayed at a target.
|
||||
var hopByHopHeaders = []string{
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Proxy-Authorization",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Connection",
|
||||
"TE",
|
||||
"Trailer",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
// Options configures the intercepting proxy.
|
||||
type Options struct {
|
||||
// Address is the host:port to listen on.
|
||||
Address string
|
||||
// CADir is the directory holding the interception CA.
|
||||
CADir string
|
||||
// Username and Password gate the proxy with basic authentication. They are
|
||||
// mandatory when Address is not a loopback address.
|
||||
Username string
|
||||
Password string
|
||||
// Verbose enables goproxy's own request logging.
|
||||
Verbose bool
|
||||
// Intercept reports whether the TLS connection to host should be decrypted.
|
||||
// Hosts it rejects are tunnelled through without inspection. Optional;
|
||||
// every host is intercepted when nil.
|
||||
Intercept func(host string) bool
|
||||
// Submit hands a captured request to the scanner. It must not block, and
|
||||
// returns false when the request could not be queued.
|
||||
Submit func(rawHTTP, targetURL string) bool
|
||||
// ForwardProxy is an optional HTTP(S) proxy URL for the forwarding leg.
|
||||
// Ambient HTTP_PROXY / HTTPS_PROXY are ignored so a leftover environment
|
||||
// variable cannot silently chain this listener.
|
||||
ForwardProxy string
|
||||
}
|
||||
|
||||
// Proxy is an intercepting proxy that mirrors traffic into the scanner.
|
||||
type Proxy struct {
|
||||
options *Options
|
||||
ca *CA
|
||||
server *http.Server
|
||||
|
||||
intercepted atomic.Int64
|
||||
tunnelsPassedThrough atomic.Int64
|
||||
dropped atomic.Int64
|
||||
}
|
||||
|
||||
// requestState is what a proxied request carries between the goproxy hooks.
|
||||
// A CONNECT tunnel reuses one instance for every request inside it.
|
||||
type requestState struct {
|
||||
tunnelAuthorized bool
|
||||
rawHTTP string
|
||||
}
|
||||
|
||||
// Stats reports what the proxy has done with the traffic it has seen.
|
||||
type Stats struct {
|
||||
Intercepted int64
|
||||
// TunnelsPassedThrough counts CONNECT tunnels left encrypted, not requests:
|
||||
// their contents are never seen.
|
||||
TunnelsPassedThrough int64
|
||||
Dropped int64
|
||||
}
|
||||
|
||||
// New returns a proxy ready to be started.
|
||||
func New(options *Options) (*Proxy, error) {
|
||||
if options.Submit == nil {
|
||||
return nil, errors.New("proxy requires a submit callback")
|
||||
}
|
||||
if err := validateBinding(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := parseForwardProxy(options.ForwardProxy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca, err := LoadOrCreateCA(options.CADir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := &Proxy{options: options, ca: ca}
|
||||
proxy.server = &http.Server{
|
||||
Handler: proxy.handler(),
|
||||
// Read and write timeouts would break long lived CONNECT tunnels, so
|
||||
// only the header read is bounded.
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
}
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// CertPath is the on-disk path of the CA certificate users need to trust.
|
||||
func (p *Proxy) CertPath() string { return p.ca.CertPath }
|
||||
|
||||
// CertPEM is the PEM encoded CA certificate. It never contains the key.
|
||||
func (p *Proxy) CertPEM() []byte { return p.ca.CertPEM }
|
||||
|
||||
// Stats returns the current proxy counters.
|
||||
func (p *Proxy) Stats() Stats {
|
||||
return Stats{
|
||||
Intercepted: p.intercepted.Load(),
|
||||
TunnelsPassedThrough: p.tunnelsPassedThrough.Load(),
|
||||
Dropped: p.dropped.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
// Start listens and serves until Close is called.
|
||||
func (p *Proxy) Start() error {
|
||||
listener, err := net.Listen("tcp", p.options.Address)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not listen on dast proxy address")
|
||||
}
|
||||
if err := p.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts the proxy listener down.
|
||||
func (p *Proxy) Close() error {
|
||||
if p.server == nil {
|
||||
return nil
|
||||
}
|
||||
if err := p.server.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Proxy) handler() http.Handler {
|
||||
handler := goproxy.NewProxyHttpServer()
|
||||
handler.Verbose = p.options.Verbose
|
||||
handler.Logger = proxyLogger{}
|
||||
handler.CertStore = newCertCache()
|
||||
// Stated rather than inherited from goproxy's default, because it is a
|
||||
// deliberate choice: the forwarding leg does not verify target certificates,
|
||||
// matching the rest of nuclei (see httpclientpool), since DAST targets
|
||||
// routinely serve self-signed ones. Interception inherently replaces the
|
||||
// browser's own validation, so this is reported at startup.
|
||||
forward, _ := parseForwardProxy(p.options.ForwardProxy)
|
||||
handler.Tr = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // #nosec G402
|
||||
Proxy: forward,
|
||||
}
|
||||
|
||||
// Every action carries our own CA. goproxy's package level OkConnect and
|
||||
// MitmConnect close over its built-in CA, whose private key ships with the
|
||||
// library, so none of them are reused here.
|
||||
signWithOurCA := goproxy.TLSConfigFromCA(p.ca.Certificate)
|
||||
mitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: signWithOurCA}
|
||||
tunnel := &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: signWithOurCA}
|
||||
// Answering CONNECT with a challenge rather than dropping the connection is
|
||||
// what lets a browser prompt for the proxy credentials.
|
||||
challenge := &goproxy.ConnectAction{
|
||||
Action: goproxy.ConnectHijack,
|
||||
Hijack: func(req *http.Request, client net.Conn, _ *goproxy.ProxyCtx) {
|
||||
defer func() { _ = client.Close() }()
|
||||
if err := authRequiredResponse(req).Write(client); err != nil {
|
||||
gologger.Debug().Msgf("Could not write proxy auth challenge: %s", err)
|
||||
}
|
||||
},
|
||||
TLSConfig: signWithOurCA,
|
||||
}
|
||||
|
||||
handler.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
if !p.authorized(ctx.Req) {
|
||||
return challenge, host
|
||||
}
|
||||
// A client only sends Proxy-Authorization on CONNECT, so the successful
|
||||
// check is recorded on the context that every request inside the tunnel
|
||||
// goes on to share, rather than being repeated per request.
|
||||
ctx.UserData = &requestState{tunnelAuthorized: true}
|
||||
|
||||
if p.options.Intercept != nil && !p.options.Intercept(host) {
|
||||
p.tunnelsPassedThrough.Add(1)
|
||||
return tunnel, host
|
||||
}
|
||||
return mitm, host
|
||||
})
|
||||
|
||||
handler.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
state, _ := ctx.UserData.(*requestState)
|
||||
if state == nil || !state.tunnelAuthorized {
|
||||
if !p.authorized(req) {
|
||||
return req, authRequiredResponse(req)
|
||||
}
|
||||
state = &requestState{}
|
||||
ctx.UserData = state
|
||||
}
|
||||
// The body has to be captured before the transport consumes it, but the
|
||||
// response is only available later, so the raw request rides along on
|
||||
// the context until then.
|
||||
if raw, ok := captureRequest(req); ok {
|
||||
state.rawHTTP = raw
|
||||
}
|
||||
return req, nil
|
||||
})
|
||||
|
||||
handler.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
|
||||
p.submit(ctx)
|
||||
return resp
|
||||
})
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// submit mirrors an intercepted request into the scanner. It runs while the
|
||||
// client is still waiting for its response, so it must stay non-blocking.
|
||||
func (p *Proxy) submit(ctx *goproxy.ProxyCtx) {
|
||||
if ctx == nil || ctx.Req == nil {
|
||||
return
|
||||
}
|
||||
state, ok := ctx.UserData.(*requestState)
|
||||
if !ok || state.rawHTTP == "" {
|
||||
return
|
||||
}
|
||||
raw := state.rawHTTP
|
||||
// Cleared so a tunnel that reuses this state cannot mirror it twice.
|
||||
state.rawHTTP = ""
|
||||
|
||||
p.intercepted.Add(1)
|
||||
if !p.options.Submit(raw, ctx.Req.URL.String()) {
|
||||
p.dropped.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) authorized(req *http.Request) bool {
|
||||
if p.options.Username == "" && p.options.Password == "" {
|
||||
return true
|
||||
}
|
||||
if req == nil {
|
||||
return false
|
||||
}
|
||||
username, password, ok := parseProxyAuth(req.Header.Get("Proxy-Authorization"))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// Both comparisons always run so the result does not leak which half failed.
|
||||
userMatch := subtle.ConstantTimeCompare([]byte(username), []byte(p.options.Username))
|
||||
passMatch := subtle.ConstantTimeCompare([]byte(password), []byte(p.options.Password))
|
||||
return userMatch&passMatch == 1
|
||||
}
|
||||
|
||||
func parseProxyAuth(header string) (username, password string, ok bool) {
|
||||
const prefix = "Basic "
|
||||
if len(header) < len(prefix) || !strings.EqualFold(header[:len(prefix)], prefix) {
|
||||
return "", "", false
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(header[len(prefix):]))
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
username, password, found := strings.Cut(string(decoded), ":")
|
||||
if !found {
|
||||
return "", "", false
|
||||
}
|
||||
return username, password, true
|
||||
}
|
||||
|
||||
func authRequiredResponse(req *http.Request) *http.Response {
|
||||
resp := goproxy.NewResponse(req, "text/plain", http.StatusProxyAuthRequired, "proxy authentication required\n")
|
||||
resp.Header.Set("Proxy-Authenticate", `Basic realm="nuclei dast proxy"`)
|
||||
// goproxy leaves the protocol version unset, which writes a "HTTP/0.0"
|
||||
// status line that strict clients reject when this is hijacked onto a raw
|
||||
// connection to answer CONNECT.
|
||||
resp.Proto, resp.ProtoMajor, resp.ProtoMinor = "HTTP/1.1", 1, 1
|
||||
resp.Close = true
|
||||
return resp
|
||||
}
|
||||
|
||||
// captureRequest renders the request as raw HTTP for the scanner, leaving the
|
||||
// request that gets forwarded untouched.
|
||||
func captureRequest(req *http.Request) (string, bool) {
|
||||
if req == nil || req.URL == nil {
|
||||
return "", false
|
||||
}
|
||||
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||
return "", false
|
||||
}
|
||||
// A body with no declared length, or one above the cap, is not mirrored at
|
||||
// all: fuzzing a large upload is not useful, buffering it would let proxied
|
||||
// traffic drive the scanner's memory use, and capturing only the headers
|
||||
// would hand the scanner a request that contradicts its own Content-Length.
|
||||
if hasBody(req) && (req.ContentLength < 0 || req.ContentLength > maxCapturedBodySize) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
body, err := bufferBody(req)
|
||||
if err != nil {
|
||||
gologger.Debug().Msgf("Could not buffer proxied request body: %s", err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
capture := req.Clone(req.Context())
|
||||
capture.Body = io.NopCloser(bytes.NewReader(body))
|
||||
capture.ContentLength = int64(len(body))
|
||||
// The proxy's own absolute-form request URI is not what the target sees.
|
||||
capture.RequestURI = ""
|
||||
// goproxy only strips hop-by-hop headers after this hook has run, so the
|
||||
// copy is sanitised here. The proxy credential in particular must never be
|
||||
// replayed to a target or written into scan output.
|
||||
stripHopByHopHeaders(capture.Header)
|
||||
|
||||
dumped, err := httputil.DumpRequest(capture, true)
|
||||
if err != nil {
|
||||
gologger.Debug().Msgf("Could not capture proxied request: %s", err)
|
||||
return "", false
|
||||
}
|
||||
return string(dumped), true
|
||||
}
|
||||
|
||||
func stripHopByHopHeaders(header http.Header) {
|
||||
for _, extra := range header.Values("Connection") {
|
||||
for _, name := range strings.Split(extra, ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
header.Del(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range hopByHopHeaders {
|
||||
header.Del(name)
|
||||
}
|
||||
}
|
||||
|
||||
func parseForwardProxy(raw string) (func(*http.Request) (*url.URL, error), error) {
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return nil, errors.Errorf("invalid dast proxy forward proxy %q", raw)
|
||||
}
|
||||
return http.ProxyURL(parsed), nil
|
||||
}
|
||||
|
||||
func hasBody(req *http.Request) bool {
|
||||
return req.Body != nil && req.Body != http.NoBody
|
||||
}
|
||||
|
||||
// bufferBody reads the body and restores it on req so the request can still be
|
||||
// forwarded after being mirrored.
|
||||
func bufferBody(req *http.Request) ([]byte, error) {
|
||||
if !hasBody(req) {
|
||||
return nil, nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(req.Body, maxCapturedBodySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func validateBinding(options *Options) error {
|
||||
host, _, err := net.SplitHostPort(options.Address)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid dast proxy address %q", options.Address)
|
||||
}
|
||||
if (options.Username == "") != (options.Password == "") {
|
||||
return errors.New("dast proxy auth needs both a username and a password")
|
||||
}
|
||||
if options.Username != "" || isLoopback(host) {
|
||||
return nil
|
||||
}
|
||||
// An unauthenticated proxy on a routable address is an open relay for
|
||||
// anyone who can reach it.
|
||||
return errors.Errorf("refusing to bind dast proxy to %q without authentication, pass -dast-proxy-auth user:pass or bind to localhost", options.Address)
|
||||
}
|
||||
|
||||
func isLoopback(host string) bool {
|
||||
if host == "" {
|
||||
// A bare port binds every interface.
|
||||
return false
|
||||
}
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip.IsLoopback()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// certCache memoises the leaf certificate minted for each intercepted host.
|
||||
type certCache struct {
|
||||
certs sync.Map
|
||||
}
|
||||
|
||||
func newCertCache() *certCache { return &certCache{} }
|
||||
|
||||
func (c *certCache) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) {
|
||||
if cached, ok := c.certs.Load(hostname); ok {
|
||||
return cached.(*tls.Certificate), nil
|
||||
}
|
||||
cert, err := gen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actual, _ := c.certs.LoadOrStore(hostname, cert)
|
||||
return actual.(*tls.Certificate), nil
|
||||
}
|
||||
|
||||
type proxyLogger struct{}
|
||||
|
||||
func (proxyLogger) Printf(format string, v ...any) {
|
||||
gologger.Debug().Msgf(format, v...)
|
||||
}
|
||||
|
||||
// ParseAuth splits a user:pass credential pair as accepted on the command line.
|
||||
func ParseAuth(value string) (username, password string, err error) {
|
||||
if value == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
username, password, found := strings.Cut(value, ":")
|
||||
if !found || username == "" || password == "" {
|
||||
return "", "", errors.New("dast proxy auth must be in user:pass format")
|
||||
}
|
||||
return username, password, nil
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type captured struct {
|
||||
rawHTTP string
|
||||
targetURL string
|
||||
}
|
||||
|
||||
// newTestProxy returns a proxy served over a local listener along with the
|
||||
// requests it mirrors into the scanner.
|
||||
func newTestProxy(t *testing.T, options *Options) (*Proxy, *url.URL, <-chan captured) {
|
||||
t.Helper()
|
||||
|
||||
mirrored := make(chan captured, 8)
|
||||
options.CADir = t.TempDir()
|
||||
options.Submit = func(rawHTTP, targetURL string) bool {
|
||||
mirrored <- captured{rawHTTP: rawHTTP, targetURL: targetURL}
|
||||
return true
|
||||
}
|
||||
if options.Address == "" {
|
||||
options.Address = "127.0.0.1:0"
|
||||
}
|
||||
|
||||
instance, err := New(options)
|
||||
require.NoError(t, err)
|
||||
|
||||
frontend := httptest.NewServer(instance.server.Handler)
|
||||
t.Cleanup(frontend.Close)
|
||||
|
||||
proxyURL, err := url.Parse(frontend.URL)
|
||||
require.NoError(t, err)
|
||||
return instance, proxyURL, mirrored
|
||||
}
|
||||
|
||||
// skipInstance drops the proxy handle for tests that only drive it over HTTP.
|
||||
func skipInstance(_ *Proxy, proxyURL *url.URL, mirrored <-chan captured) (*url.URL, <-chan captured) {
|
||||
return proxyURL, mirrored
|
||||
}
|
||||
|
||||
func clientThrough(proxyURL *url.URL) *http.Client {
|
||||
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
|
||||
}
|
||||
|
||||
func TestProxyForwardsAndMirrorsRequest(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, "upstream-body")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
proxyURL, mirrored := skipInstance(newTestProxy(t, &Options{}))
|
||||
|
||||
resp, err := clientThrough(proxyURL).Post(upstream.URL+"/login?next=/admin", "application/json", strings.NewReader(`{"user":"admin"}`))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "upstream-body", string(body), "proxy must forward the upstream response untouched")
|
||||
|
||||
got := <-mirrored
|
||||
require.Equal(t, upstream.URL+"/login?next=/admin", got.targetURL)
|
||||
|
||||
// The scanner consumes the mirrored traffic through this parser, so the
|
||||
// capture is only useful if it round-trips.
|
||||
parsed, err := types.ParseRawRequestWithURL(got.rawHTTP, got.targetURL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.MethodPost, parsed.Request.Method)
|
||||
require.Equal(t, `{"user":"admin"}`, parsed.Request.Body)
|
||||
require.Equal(t, "/login", parsed.URL.Path)
|
||||
}
|
||||
|
||||
func TestProxyRequiresAuthWhenConfigured(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "upstream-body")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
proxyURL, mirrored := skipInstance(newTestProxy(t, &Options{Username: "nuclei", Password: "s3cret"}))
|
||||
|
||||
resp, err := clientThrough(proxyURL).Get(upstream.URL + "/")
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusProxyAuthRequired, resp.StatusCode)
|
||||
require.Contains(t, resp.Header.Get("Proxy-Authenticate"), "Basic")
|
||||
require.Empty(t, mirrored, "an unauthenticated request must not reach the scanner")
|
||||
|
||||
authenticated := proxyURL
|
||||
authenticated.User = url.UserPassword("nuclei", "s3cret")
|
||||
resp, err = clientThrough(authenticated).Get(upstream.URL + "/")
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, upstream.URL+"/", (<-mirrored).targetURL)
|
||||
}
|
||||
|
||||
func TestProxyRejectsWrongCredentials(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}))
|
||||
defer upstream.Close()
|
||||
|
||||
proxyURL, _ := skipInstance(newTestProxy(t, &Options{Username: "nuclei", Password: "s3cret"}))
|
||||
proxyURL.User = url.UserPassword("nuclei", "wrong")
|
||||
|
||||
resp, err := clientThrough(proxyURL).Get(upstream.URL + "/")
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusProxyAuthRequired, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestNewRefusesUnauthenticatedNonLoopbackBind(t *testing.T) {
|
||||
for _, address := range []string{"0.0.0.0:9056", "192.0.2.10:9056", ":9056"} {
|
||||
_, err := New(&Options{
|
||||
Address: address,
|
||||
CADir: t.TempDir(),
|
||||
Submit: func(string, string) bool { return true },
|
||||
})
|
||||
require.ErrorContains(t, err, "refusing to bind", address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAllowsLoopbackAndAuthenticatedBinds(t *testing.T) {
|
||||
for _, options := range []*Options{
|
||||
{Address: "127.0.0.1:9056"},
|
||||
{Address: "localhost:9056"},
|
||||
{Address: "[::1]:9056"},
|
||||
{Address: "0.0.0.0:9056", Username: "nuclei", Password: "s3cret"},
|
||||
} {
|
||||
options.CADir = t.TempDir()
|
||||
options.Submit = func(string, string) bool { return true }
|
||||
_, err := New(options)
|
||||
require.NoError(t, err, options.Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRequiresBothAuthHalves(t *testing.T) {
|
||||
_, err := New(&Options{
|
||||
Address: "127.0.0.1:9056",
|
||||
CADir: t.TempDir(),
|
||||
Username: "nuclei",
|
||||
Submit: func(string, string) bool { return true },
|
||||
})
|
||||
require.ErrorContains(t, err, "both a username and a password")
|
||||
}
|
||||
|
||||
func TestParseAuth(t *testing.T) {
|
||||
username, password, err := ParseAuth("nuclei:s3cret")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "nuclei", username)
|
||||
require.Equal(t, "s3cret", password)
|
||||
|
||||
username, password, err = ParseAuth("")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, username)
|
||||
require.Empty(t, password)
|
||||
|
||||
for _, invalid := range []string{"nuclei", ":s3cret", "nuclei:"} {
|
||||
_, _, err := ParseAuth(invalid)
|
||||
require.ErrorContains(t, err, "user:pass", invalid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureRequestSkipsUnboundedBody(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodPost, "http://example.com/upload", strings.NewReader("payload"))
|
||||
require.NoError(t, err)
|
||||
request.ContentLength = maxCapturedBodySize + 1
|
||||
|
||||
_, ok := captureRequest(request)
|
||||
require.False(t, ok, "an oversized body must not be buffered")
|
||||
|
||||
request.ContentLength = int64(len("payload"))
|
||||
raw, ok := captureRequest(request)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, raw, "payload")
|
||||
}
|
||||
|
||||
func TestCaptureRequestLeavesForwardedRequestIntact(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodPost, "http://example.com/login", strings.NewReader("user=admin"))
|
||||
require.NoError(t, err)
|
||||
|
||||
raw, ok := captureRequest(request)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, raw, "user=admin")
|
||||
|
||||
// The forwarded request still has to be readable after being mirrored.
|
||||
forwarded, err := io.ReadAll(request.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user=admin", string(forwarded))
|
||||
}
|
||||
|
||||
func TestCaptureRequestDropsProxyCredentials(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
request.RequestURI = "http://example.com/" // absolute form, as a proxy receives it
|
||||
request.Header.Set("Proxy-Authorization", "Basic bnVjbGVpOnMzY3JldA==")
|
||||
request.Header.Set("Proxy-Connection", "keep-alive")
|
||||
request.Header.Set("Authorization", "Bearer target-token")
|
||||
|
||||
raw, ok := captureRequest(request)
|
||||
require.True(t, ok)
|
||||
require.NotContains(t, raw, "Proxy-Authorization", "the proxy credential must never reach the scanner")
|
||||
require.NotContains(t, raw, "bnVjbGVpOnMzY3JldA==")
|
||||
require.NotContains(t, raw, "Proxy-Connection")
|
||||
require.Contains(t, raw, "Bearer target-token", "the target's own credentials must be preserved")
|
||||
require.True(t, strings.HasPrefix(raw, "GET / HTTP/1.1"), "request line must be origin form, got %q", raw)
|
||||
}
|
||||
|
||||
func TestCaptureRequestDropsHopByHopHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
request.Header.Set("Connection", "close, X-Proxy-Debug")
|
||||
request.Header.Set("X-Proxy-Debug", "1")
|
||||
request.Header.Set("Keep-Alive", "timeout=5")
|
||||
request.Header.Set("TE", "trailers")
|
||||
request.Header.Set("X-Custom", "keep")
|
||||
|
||||
raw, ok := captureRequest(request)
|
||||
require.True(t, ok)
|
||||
require.NotContains(t, raw, "X-Proxy-Debug")
|
||||
require.NotContains(t, raw, "Keep-Alive")
|
||||
require.NotContains(t, raw, "TE:")
|
||||
require.NotContains(t, raw, "Connection:")
|
||||
require.Contains(t, raw, "X-Custom: keep")
|
||||
}
|
||||
|
||||
func TestNewRejectsInvalidForwardProxy(t *testing.T) {
|
||||
_, err := New(&Options{
|
||||
Address: "127.0.0.1:9056",
|
||||
CADir: t.TempDir(),
|
||||
ForwardProxy: "://bad",
|
||||
Submit: func(string, string) bool { return true },
|
||||
})
|
||||
require.ErrorContains(t, err, "forward proxy")
|
||||
}
|
||||
|
||||
func TestCaptureRequestRejectsNonHTTPScheme(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
require.NoError(t, err)
|
||||
request.URL.Scheme = "ftp"
|
||||
|
||||
_, ok := captureRequest(request)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
// A client only sends Proxy-Authorization on CONNECT, so requests inside an
|
||||
// established tunnel must not be challenged again.
|
||||
func TestProxyAuthorizesTunnelOnceOnConnect(t *testing.T) {
|
||||
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "upstream-body")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
instance, proxyURL, mirrored := newTestProxy(t, &Options{Username: "nuclei", Password: "s3cret"})
|
||||
proxyURL.User = url.UserPassword("nuclei", "s3cret")
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
require.True(t, roots.AppendCertsFromPEM(instance.CertPEM()))
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12},
|
||||
}}
|
||||
|
||||
resp, err := client.Get(upstream.URL + "/admin?id=1")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "a request inside an authorized tunnel must not be challenged")
|
||||
require.Equal(t, "upstream-body", string(body))
|
||||
|
||||
got := <-mirrored
|
||||
require.Equal(t, upstream.URL+"/admin?id=1", got.targetURL)
|
||||
require.True(t, strings.HasPrefix(got.rawHTTP, "GET /admin?id=1 HTTP/1.1"), "got %q", got.rawHTTP)
|
||||
}
|
||||
|
||||
func TestProxyChallengesUnauthenticatedConnect(t *testing.T) {
|
||||
_, proxyURL, _ := newTestProxy(t, &Options{Username: "nuclei", Password: "s3cret"})
|
||||
|
||||
conn, err := net.Dial("tcp", proxyURL.Host)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
_, err = io.WriteString(conn, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// A malformed status line leaves a browser unable to prompt for credentials.
|
||||
require.Equal(t, http.StatusProxyAuthRequired, resp.StatusCode)
|
||||
require.Equal(t, 1, resp.ProtoMajor)
|
||||
require.Equal(t, 1, resp.ProtoMinor)
|
||||
require.Contains(t, resp.Header.Get("Proxy-Authenticate"), "Basic")
|
||||
}
|
||||
|
||||
func TestProxyPassesThroughExcludedHostsWithoutMITM(t *testing.T) {
|
||||
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "upstream-body")
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
upstreamURL, err := url.Parse(upstream.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
instance, proxyURL, mirrored := newTestProxy(t, &Options{
|
||||
Intercept: func(host string) bool {
|
||||
return host != upstreamURL.Host
|
||||
},
|
||||
})
|
||||
|
||||
// Trust only the upstream certificate. An intercepted connection is
|
||||
// signed by the nuclei CA and would fail this handshake.
|
||||
upstreamRoots := x509.NewCertPool()
|
||||
upstreamRoots.AddCert(upstream.Certificate())
|
||||
client := &http.Client{Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{RootCAs: upstreamRoots, MinVersion: tls.VersionTLS12},
|
||||
}}
|
||||
resp, err := client.Get(upstream.URL + "/secret")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "upstream-body", string(body))
|
||||
require.True(t, resp.TLS.PeerCertificates[0].Equal(upstream.Certificate()),
|
||||
"pass-through must present the target certificate, not a nuclei-minted one")
|
||||
|
||||
require.Equal(t, int64(1), instance.Stats().TunnelsPassedThrough)
|
||||
require.Zero(t, instance.Stats().Intercepted)
|
||||
require.Empty(t, mirrored)
|
||||
}
|
||||
@@ -75,3 +75,15 @@ func (m *Manager) validateURL(URL string) (bool, error) {
|
||||
}
|
||||
return inScopeMatched, nil
|
||||
}
|
||||
|
||||
// IsExplicitlyOutOfScope reports whether the URL matches an out-of-scope rule.
|
||||
// It exists for decisions that can only see a host, such as whether to decrypt
|
||||
// a CONNECT tunnel, where in-scope rules carrying a path can never match.
|
||||
func (m *Manager) IsExplicitlyOutOfScope(URL *url.URL) bool {
|
||||
for _, item := range m.outOfScope {
|
||||
if item.MatchString(URL.String()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package scope
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
@@ -24,3 +25,20 @@ func TestManagerValidate(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestManagerIsExplicitlyOutOfScope(t *testing.T) {
|
||||
manager, err := NewManager(
|
||||
[]string{`^https://app\.example\.com/api/`},
|
||||
[]string{`^https://telemetry\.example\.com/`},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
excluded, err := url.Parse("https://telemetry.example.com/")
|
||||
require.NoError(t, err)
|
||||
require.True(t, manager.IsExplicitlyOutOfScope(excluded))
|
||||
|
||||
// Not matching an in-scope rule is not the same as being excluded.
|
||||
unlisted, err := url.Parse("https://app.example.com/")
|
||||
require.NoError(t, err)
|
||||
require.False(t, manager.IsExplicitlyOutOfScope(unlisted))
|
||||
}
|
||||
+148
-10
@@ -3,8 +3,10 @@ package server
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/scope"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
|
||||
@@ -29,6 +32,7 @@ type DASTServer struct {
|
||||
tasksPool *pond.WorkerPool
|
||||
deduplicator *requestDeduplicator
|
||||
scopeManager *scope.Manager
|
||||
proxy *proxy.Proxy
|
||||
startTime time.Time
|
||||
|
||||
// metrics
|
||||
@@ -49,6 +53,21 @@ type Options struct {
|
||||
// Verbose is a flag that controls verbose output
|
||||
Verbose bool
|
||||
|
||||
// ProxyAddress enables the intercepting proxy front-end on this address
|
||||
ProxyAddress string
|
||||
// ProxyCADir is the directory holding the proxy interception CA
|
||||
ProxyCADir string
|
||||
// ProxyUsername and ProxyPassword gate the intercepting proxy with basic
|
||||
// authentication. Mandatory when ProxyAddress is not a loopback address.
|
||||
ProxyUsername string
|
||||
ProxyPassword string
|
||||
// EnableFuzzAPI exposes POST /fuzz. Proxy-only mode leaves it off so the
|
||||
// stats/CA listener cannot be used to inject arbitrary raw HTTP.
|
||||
EnableFuzzAPI bool
|
||||
// ForwardProxy is an optional HTTP(S) proxy URL for the intercepting
|
||||
// proxy's forwarding leg. Ambient HTTP_PROXY is not used.
|
||||
ForwardProxy string
|
||||
|
||||
// Scope fields for fuzzer
|
||||
InScope []string
|
||||
OutScope []string
|
||||
@@ -97,17 +116,70 @@ func New(options *Options) (*DASTServer, error) {
|
||||
}
|
||||
server.scopeManager = scopeManager
|
||||
|
||||
if options.ProxyAddress != "" {
|
||||
if err := server.setupProxy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
gologger.Debug().Msgf("Using %d parallel tasks with %d buffer", maxWorkers, bufferSize)
|
||||
if options.Token != "" {
|
||||
builder.WriteString(" (with token)")
|
||||
}
|
||||
gologger.Info().Msgf("DAST Server API: %s", server.buildURL("/fuzz"))
|
||||
if options.EnableFuzzAPI {
|
||||
gologger.Info().Msgf("DAST Server API: %s", server.buildURL("/fuzz"))
|
||||
}
|
||||
gologger.Info().Msgf("DAST Server Stats URL: %s", server.buildURL("/stats"))
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// setupProxy wires the intercepting proxy front-end into the same queue the
|
||||
// HTTP API feeds.
|
||||
func (s *DASTServer) setupProxy() error {
|
||||
interceptingProxy, err := proxy.New(&proxy.Options{
|
||||
Address: s.options.ProxyAddress,
|
||||
CADir: s.options.ProxyCADir,
|
||||
Username: s.options.ProxyUsername,
|
||||
Password: s.options.ProxyPassword,
|
||||
Verbose: s.options.Verbose,
|
||||
Intercept: s.shouldIntercept,
|
||||
Submit: s.Submit,
|
||||
ForwardProxy: s.options.ForwardProxy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.proxy = interceptingProxy
|
||||
|
||||
gologger.Info().Msgf("DAST Proxy: http://%s", s.options.ProxyAddress)
|
||||
gologger.Info().Msgf("DAST Proxy CA certificate: %s (install it to intercept https)", interceptingProxy.CertPath())
|
||||
gologger.Info().Msgf("DAST Proxy CA download: %s", s.buildURL("/ca"))
|
||||
// Info, not Warning: gologger orders LevelWarning above LevelInfo, so
|
||||
// warnings are filtered out at default verbosity and these two notices
|
||||
// describe what interception does to the user's traffic.
|
||||
gologger.Info().Msgf("DAST Proxy: intercepted https is re-originated by nuclei, target certificates are not verified")
|
||||
if len(s.options.InScope) == 0 && len(s.options.OutScope) == 0 {
|
||||
gologger.Info().Msgf("DAST Proxy: no scope set, every proxied https host will be decrypted (narrow it with -fuzz-scope / -fuzz-out-scope)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldIntercept decides whether a CONNECT tunnel is decrypted. Only an
|
||||
// explicit out-of-scope match leaves a host encrypted, because in-scope
|
||||
// patterns may carry a path that a CONNECT host can never match against.
|
||||
func (s *DASTServer) shouldIntercept(hostPort string) bool {
|
||||
host := hostPort
|
||||
// Only the default port is dropped, so the synthesised URL has the same
|
||||
// shape as the request URLs these rules are applied to once a request has
|
||||
// actually been captured.
|
||||
if parsedHost, port, err := net.SplitHostPort(hostPort); err == nil && port == "443" {
|
||||
host = parsedHost
|
||||
}
|
||||
return !s.scopeManager.IsExplicitlyOutOfScope(&url.URL{Scheme: "https", Host: host, Path: "/"})
|
||||
}
|
||||
|
||||
func NewStatsServer(fuzzStatsDB *stats.Tracker) (*DASTServer, error) {
|
||||
server := &DASTServer{
|
||||
options: &Options{},
|
||||
@@ -124,6 +196,11 @@ func NewStatsServer(fuzzStatsDB *stats.Tracker) (*DASTServer, error) {
|
||||
}
|
||||
|
||||
func (s *DASTServer) Close() {
|
||||
if s.proxy != nil {
|
||||
if err := s.proxy.Close(); err != nil {
|
||||
gologger.Warning().Msgf("Could not close dast proxy: %s", err)
|
||||
}
|
||||
}
|
||||
if s.nucleiExecutor != nil {
|
||||
s.nucleiExecutor.Close()
|
||||
}
|
||||
@@ -160,16 +237,18 @@ func (s *DASTServer) optionsOrDefault() *Options {
|
||||
}
|
||||
|
||||
func (s *DASTServer) setupHandlers(onlyStats bool) {
|
||||
opts := s.optionsOrDefault()
|
||||
mux := http.NewServeMux()
|
||||
// POST /fuzz - Queue a request for fuzzing
|
||||
if !onlyStats {
|
||||
if !onlyStats && opts.EnableFuzzAPI {
|
||||
mux.HandleFunc("POST /fuzz", s.handleRequest)
|
||||
}
|
||||
mux.HandleFunc("GET /stats", s.handleStats)
|
||||
mux.HandleFunc("GET /stats.json", s.handleStatsJSON)
|
||||
if !onlyStats && opts.ProxyAddress != "" {
|
||||
mux.HandleFunc("GET /ca", s.handleProxyCA)
|
||||
}
|
||||
|
||||
handler := http.Handler(mux)
|
||||
opts := s.optionsOrDefault()
|
||||
if opts.Token != "" {
|
||||
handler = s.tokenAuthMiddleware(handler)
|
||||
}
|
||||
@@ -187,12 +266,37 @@ func (s *DASTServer) Start() error {
|
||||
s.setupHandlers(false)
|
||||
}
|
||||
s.httpServer.Addr = s.optionsOrDefault().Address
|
||||
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
|
||||
if s.proxy == nil {
|
||||
return s.serveAPI()
|
||||
}
|
||||
|
||||
// Both front-ends feed the same queue; whichever fails first ends the run.
|
||||
errs := make(chan error, 2)
|
||||
go func() { errs <- s.serveAPI() }()
|
||||
go func() { errs <- s.proxy.Start() }()
|
||||
return <-errs
|
||||
}
|
||||
|
||||
func (s *DASTServer) serveAPI() error {
|
||||
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleProxyCA serves the interception CA certificate so it can be installed.
|
||||
// Only the certificate is ever served, never the private key.
|
||||
func (s *DASTServer) handleProxyCA(w http.ResponseWriter, _ *http.Request) {
|
||||
if s.proxy == nil {
|
||||
writeServerJSON(w, http.StatusNotFound, map[string]string{"error": "proxy is not enabled"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="nuclei-dast-proxy-ca.pem"`)
|
||||
_, _ = w.Write(s.proxy.CertPEM())
|
||||
}
|
||||
|
||||
// PostRequestsHandlerRequest is the request body for the /fuzz POST handler.
|
||||
type PostRequestsHandlerRequest struct {
|
||||
RawHTTP string `json:"raw_http"`
|
||||
@@ -214,13 +318,27 @@ func (s *DASTServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.endpointsInQueue.Add(1)
|
||||
s.tasksPool.Submit(func() {
|
||||
s.consumeTaskRequest(req)
|
||||
})
|
||||
if !s.Submit(req.RawHTTP, req.URL) {
|
||||
writeServerJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "scan queue is full"})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Submit queues a captured request for fuzzing and reports whether it was
|
||||
// accepted. It never blocks: a saturated scanner must not stall the live
|
||||
// traffic flowing through the proxy, so excess requests are dropped instead.
|
||||
func (s *DASTServer) Submit(rawHTTP, targetURL string) bool {
|
||||
req := PostRequestsHandlerRequest{RawHTTP: rawHTTP, URL: targetURL}
|
||||
|
||||
s.endpointsInQueue.Add(1)
|
||||
if !s.tasksPool.TrySubmit(func() { s.consumeTaskRequest(req) }) {
|
||||
s.endpointsInQueue.Add(-1)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type StatsResponse struct {
|
||||
DASTServerInfo DASTServerInfo `json:"dast_server_info"`
|
||||
DASTScanStatistics DASTScanStatistics `json:"dast_scan_statistics"`
|
||||
@@ -235,6 +353,8 @@ type DASTServerInfo struct {
|
||||
NucleiTemplateVersion string `json:"nuclei_template_version"`
|
||||
NucleiDastServerAPI string `json:"nuclei_dast_server_api"`
|
||||
ServerAuthEnabled bool `json:"sever_auth_enabled"`
|
||||
NucleiDastProxyAddr string `json:"nuclei_dast_proxy_address,omitempty"`
|
||||
ProxyAuthEnabled bool `json:"proxy_auth_enabled"`
|
||||
}
|
||||
|
||||
type DASTScanStatistics struct {
|
||||
@@ -247,17 +367,29 @@ type DASTScanStatistics struct {
|
||||
TotalEndpointsTested int64 `json:"total_endpoints_tested"`
|
||||
TotalFuzzedRequests int64 `json:"total_fuzzed_requests"`
|
||||
TotalErroredRequests int64 `json:"total_errored_requests"`
|
||||
|
||||
// Proxy counters, populated only in proxy mode. A rising drop count means
|
||||
// the scanner cannot keep up with the proxied traffic.
|
||||
ProxyRequestsIntercepted int64 `json:"proxy_requests_intercepted"`
|
||||
ProxyTunnelsPassedThrough int64 `json:"proxy_tunnels_passed_through"`
|
||||
ProxyRequestsDropped int64 `json:"proxy_requests_dropped"`
|
||||
}
|
||||
|
||||
func (s *DASTServer) getStats() (StatsResponse, error) {
|
||||
cfg := config.DefaultConfig
|
||||
|
||||
api := ""
|
||||
if s.options.EnableFuzzAPI {
|
||||
api = s.buildURL("/fuzz")
|
||||
}
|
||||
resp := StatsResponse{
|
||||
DASTServerInfo: DASTServerInfo{
|
||||
NucleiVersion: config.Version,
|
||||
NucleiTemplateVersion: cfg.TemplateVersion,
|
||||
NucleiDastServerAPI: s.buildURL("/fuzz"),
|
||||
NucleiDastServerAPI: api,
|
||||
ServerAuthEnabled: s.options.Token != "",
|
||||
NucleiDastProxyAddr: s.options.ProxyAddress,
|
||||
ProxyAuthEnabled: s.options.ProxyUsername != "",
|
||||
},
|
||||
DASTScanStartTime: s.startTime,
|
||||
DASTScanStatistics: DASTScanStatistics{
|
||||
@@ -266,6 +398,12 @@ func (s *DASTServer) getStats() (StatsResponse, error) {
|
||||
TotalTemplatesLoaded: int64(len(s.nucleiExecutor.store.Templates())),
|
||||
},
|
||||
}
|
||||
if s.proxy != nil {
|
||||
proxyStats := s.proxy.Stats()
|
||||
resp.DASTScanStatistics.ProxyRequestsIntercepted = proxyStats.Intercepted
|
||||
resp.DASTScanStatistics.ProxyTunnelsPassedThrough = proxyStats.TunnelsPassedThrough
|
||||
resp.DASTScanStatistics.ProxyRequestsDropped = proxyStats.Dropped
|
||||
}
|
||||
if s.nucleiExecutor.executorOpts.FuzzStatsDB != nil {
|
||||
fuzzStats := s.nucleiExecutor.executorOpts.FuzzStatsDB.GetStats()
|
||||
resp.DASTScanSeverityBreakdown = fuzzStats.SeverityCounts
|
||||
|
||||
@@ -3,8 +3,11 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/server/scope"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -64,3 +67,69 @@ func TestStatsServerCloseDoesNotRequireFullExecutorOptions(t *testing.T) {
|
||||
|
||||
require.NotPanics(t, server.Close)
|
||||
}
|
||||
|
||||
func TestShouldInterceptOnlySkipsExplicitlyExcludedHosts(t *testing.T) {
|
||||
scopeManager, err := scope.NewManager(
|
||||
// An in-scope rule carrying a path can never match a CONNECT host, so
|
||||
// it must not be what decides whether the tunnel is decrypted.
|
||||
[]string{`^https://app\.example\.com/api/`},
|
||||
[]string{`^https://telemetry\.example\.com/`},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
server := &DASTServer{options: &Options{}, scopeManager: scopeManager}
|
||||
|
||||
require.True(t, server.shouldIntercept("app.example.com:443"))
|
||||
require.True(t, server.shouldIntercept("unrelated.example.com:443"))
|
||||
require.False(t, server.shouldIntercept("telemetry.example.com:443"))
|
||||
|
||||
// A non-default port stays in the URL, matching what the same rules see
|
||||
// once a request off that connection is captured.
|
||||
staging, err := scope.NewManager(nil, []string{`^https://staging\.example\.com:8443/`})
|
||||
require.NoError(t, err)
|
||||
server.scopeManager = staging
|
||||
require.False(t, server.shouldIntercept("staging.example.com:8443"))
|
||||
require.True(t, server.shouldIntercept("staging.example.com:443"))
|
||||
}
|
||||
|
||||
func TestSubmitDropsInsteadOfBlockingWhenQueueIsFull(t *testing.T) {
|
||||
occupied, release := make(chan struct{}), make(chan struct{})
|
||||
|
||||
// One worker and no queue slot: once the worker is busy there is nowhere
|
||||
// left for a task to go.
|
||||
server := &DASTServer{options: &Options{}, tasksPool: pond.New(1, 0)}
|
||||
server.tasksPool.Submit(func() {
|
||||
close(occupied)
|
||||
<-release
|
||||
})
|
||||
<-occupied
|
||||
defer close(release)
|
||||
|
||||
require.False(t, server.Submit("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "http://example.com/"),
|
||||
"a saturated queue must drop rather than block live proxy traffic")
|
||||
require.Zero(t, server.endpointsInQueue.Load(), "a dropped request must not stay counted as queued")
|
||||
}
|
||||
|
||||
func TestProxyOnlyDoesNotExposeFuzzAPI(t *testing.T) {
|
||||
server := &DASTServer{options: &Options{ProxyAddress: "127.0.0.1:9056"}}
|
||||
server.setupHandlers(false)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/fuzz", strings.NewReader(`{"raw_http":"GET / HTTP/1.1\r\n\r\n","url":"http://example.com/"}`))
|
||||
response := httptest.NewRecorder()
|
||||
server.httpServer.Handler.ServeHTTP(response, request)
|
||||
require.Equal(t, http.StatusNotFound, response.Code)
|
||||
|
||||
request = httptest.NewRequest(http.MethodGet, "/ca", nil)
|
||||
response = httptest.NewRecorder()
|
||||
server.httpServer.Handler.ServeHTTP(response, request)
|
||||
require.Equal(t, http.StatusNotFound, response.Code, "CA download needs a running proxy instance")
|
||||
}
|
||||
|
||||
func TestFuzzAPIEnabledWhenRequested(t *testing.T) {
|
||||
server := &DASTServer{options: &Options{EnableFuzzAPI: true}}
|
||||
server.setupHandlers(false)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/fuzz", strings.NewReader(`{"raw_http":"","url":""}`))
|
||||
response := httptest.NewRecorder()
|
||||
server.httpServer.Handler.ServeHTTP(response, request)
|
||||
require.Equal(t, http.StatusBadRequest, response.Code)
|
||||
}
|
||||
@@ -241,6 +241,10 @@ Dynamic Application Security Testing (DAST) API Server
|
||||
<div class="terminal-line"><span class="key">Template Version</span><span class="value">{{.DASTServerInfo.NucleiTemplateVersion}}</span></div>
|
||||
<div class="terminal-line"><span class="key">DAST Server API</span><span class="value">{{.DASTServerInfo.NucleiDastServerAPI}}</span></div>
|
||||
<div class="terminal-line"><span class="key">Auth Status</span><span class="value">{{if .DASTServerInfo.ServerAuthEnabled}}ENABLED{{else}}DISABLED{{end}}</span></div>
|
||||
{{if .DASTServerInfo.NucleiDastProxyAddr}}
|
||||
<div class="terminal-line"><span class="key">DAST Proxy</span><span class="value">{{.DASTServerInfo.NucleiDastProxyAddr}}</span></div>
|
||||
<div class="terminal-line"><span class="key">Proxy Auth Status</span><span class="value">{{if .DASTServerInfo.ProxyAuthEnabled}}ENABLED{{else}}DISABLED{{end}}</span></div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@@ -256,6 +260,15 @@ Dynamic Application Security Testing (DAST) API Server
|
||||
<div class="terminal-line"><span class="key">Total Errors</span><span class="value">{{.DASTScanStatistics.TotalErroredRequests}}</span></div>
|
||||
</div>
|
||||
|
||||
{{if .DASTServerInfo.NucleiDastProxyAddr}}
|
||||
<div class="section">
|
||||
<div class="section-header">[~] Proxy Traffic</div>
|
||||
<div class="terminal-line"><span class="key">Requests Intercepted</span><span class="value">{{.DASTScanStatistics.ProxyRequestsIntercepted}}</span></div>
|
||||
<div class="terminal-line"><span class="key">Tunnels Passed Through</span><span class="value">{{.DASTScanStatistics.ProxyTunnelsPassedThrough}}</span></div>
|
||||
<div class="terminal-line"><span class="key">Requests Dropped</span><span class="value">{{.DASTScanStatistics.ProxyRequestsDropped}}</span></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">[!] Security Findings</div>
|
||||
<div class="grid">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@@ -96,6 +97,8 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/response-data-literal-reuse.yaml", TestCase: &httpResponseDataLiteralReuse{}},
|
||||
{Path: "protocols/http/raw-path-single-slash.yaml", TestCase: &httpRawPathSingleSlash{}},
|
||||
{Path: "protocols/http/raw-unsafe-path-single-slash.yaml", TestCase: &httpRawUnsafePathSingleSlash{}},
|
||||
{Path: "protocols/http/disable-http-cache.yaml", TestCase: &httpDisableCache{}},
|
||||
{Path: "protocols/http/http-cache.yaml", TestCase: &httpCache{}},
|
||||
}
|
||||
|
||||
type httpMultiVarSharing struct{}
|
||||
@@ -1849,3 +1852,67 @@ func (h *httpRawUnsafePathSingleSlash) Execute(filepath string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpCache struct{}
|
||||
|
||||
func (h *httpCache) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
var requestCount int32
|
||||
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
atomic.AddInt32(&requestCount, 1)
|
||||
w.Header().Set("Cache-Control", "max-age=2")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, requestCount)
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-http-cache")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We expect 2 results because we made 2 requests and both should match
|
||||
if err := expectResultsCount(results, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We expect only 1 actual request to the server because of caching
|
||||
if count := atomic.LoadInt32(&requestCount); count != 1 {
|
||||
return fmt.Errorf("expected 1 request to server, got %d", count)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpDisableCache struct{}
|
||||
|
||||
func (h *httpDisableCache) Execute(filePath string) error {
|
||||
var requestCount int32
|
||||
router := httprouter.New()
|
||||
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
atomic.AddInt32(&requestCount, 1)
|
||||
w.Header().Set("Cache-Control", "max-age=2")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, requestCount)
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-http-cache")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We expect 2 results because we made 2 requests and both should match
|
||||
if err := expectResultsCount(results, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We expect 2 actual requests to the server because cache is disabled
|
||||
if count := atomic.LoadInt32(&requestCount); count != 2 {
|
||||
return fmt.Errorf("expected 2 requests to server, got %d", count)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -73,17 +73,27 @@ func TestMain(m *testing.M) {
|
||||
templatesDir := filepath.Join(homeDir, config.NucleiTemplatesDirName)
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
|
||||
baseEnv := []string{
|
||||
"NUCLEI_TEMPLATES_DIR=" + templatesDir,
|
||||
"XDG_CONFIG_HOME=" + filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME=" + filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME=" + filepath.Join(tempDir, "cache"),
|
||||
}
|
||||
// cases run nuclei with -duc, so the official templates some of them read
|
||||
// are installed once here rather than by whichever cases start first, which
|
||||
// queued them behind each other's downloads inside their own timeouts
|
||||
if err := installTemplates(binaryPath, baseEnv); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
previousRunner := testutils.DefaultRunner()
|
||||
runner := testutils.NewRunner(
|
||||
testutils.WithBinaryPath(binaryPath),
|
||||
testutils.WithWorkingDir(workingFixturesDir),
|
||||
testutils.WithExtraArgs(integrationExtraArgs...),
|
||||
testutils.WithBaseEnv(
|
||||
"NUCLEI_TEMPLATES_DIR="+templatesDir,
|
||||
"XDG_CONFIG_HOME="+filepath.Join(tempDir, "config"),
|
||||
"XDG_STATE_HOME="+filepath.Join(tempDir, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(tempDir, "cache"),
|
||||
),
|
||||
testutils.WithBaseEnv(baseEnv...),
|
||||
)
|
||||
testutils.SetDefaultRunner(runner)
|
||||
|
||||
@@ -124,6 +134,16 @@ func buildNucleiBinary(repoRoot, binaryPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installTemplates(binaryPath string, env []string) error {
|
||||
cmd := exec.Command(binaryPath, "-update-templates", "-no-color")
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install nuclei templates: %w\n%s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDebugMode() bool {
|
||||
for _, envVar := range []string{"DEBUG", "ACTIONS_STEP_DEBUG", "ACTIONS_RUNNER_DEBUG", "RUNNER_DEBUG"} {
|
||||
if envTruthy(envVar) {
|
||||
|
||||
@@ -37,6 +37,8 @@ var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
|
||||
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
|
||||
{Path: "protocols/javascript/websocket-echo.yaml", TestCase: &javascriptWebSocketEcho{}},
|
||||
{Path: "protocols/javascript/websocket-denied.yaml", TestCase: &javascriptWebSocketDenied{}},
|
||||
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
|
||||
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
|
||||
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
)
|
||||
|
||||
// startWebSocketEchoServer starts a WebSocket server on a random loopback port
|
||||
// that accepts the "chat" subprotocol, greets, then echoes every message.
|
||||
func startWebSocketEchoServer() (string, func(), error) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("websocket listen: %w", err)
|
||||
}
|
||||
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := ws.HTTPUpgrader{Protocol: func(protocol string) bool { return protocol == "chat" }}
|
||||
conn, _, _, err := upgrader.Upgrade(r, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if err := wsutil.WriteServerText(conn, []byte("welcome")); err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
payload, op, err := wsutil.ReadClientData(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerMessage(conn, op, payload); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})}
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return lis.Addr().String(), func() { _ = srv.Close() }, nil
|
||||
}
|
||||
|
||||
// javascriptWebSocketEcho exercises the nuclei/websocket library end to end
|
||||
// against a real WebSocket server.
|
||||
type javascriptWebSocketEcho struct{}
|
||||
|
||||
func (j *javascriptWebSocketEcho) Execute(filePath string) error {
|
||||
address, stop, err := startWebSocketEchoServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stop()
|
||||
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// javascriptWebSocketDenied asserts the nuclei/websocket library refuses a host
|
||||
// on the exclude list (network-policy enforcement) before connecting.
|
||||
type javascriptWebSocketDenied struct{}
|
||||
|
||||
func (j *javascriptWebSocketDenied) Execute(filePath string) error {
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -92,7 +93,14 @@ func executeNucleiAsLibrary(templatePath, templateURL string) ([]string, error)
|
||||
actualTemplatePath = fixturePath(actualTemplatePath)
|
||||
}
|
||||
defaultOpts.Templates = goflags.StringSlice{actualTemplatePath}
|
||||
defaultOpts.ExcludeTags = config.ReadIgnoreFile().Tags
|
||||
ignoreFile, err := config.ReadIgnoreFile()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
defaultOpts.Logger.Warning().Msgf("%s; continuing without ignore exclusions\n", err)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defaultOpts.ExcludeTags = ignoreFile.Tags
|
||||
}
|
||||
|
||||
outputWriter := testutils.NewMockOutputWriter(defaultOpts.OmitTemplate)
|
||||
var results []string
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -174,6 +175,11 @@ func executeIntegrationCase(testCase integrationCase) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// a killed nuclei is not a flake; retrying it just spends the package
|
||||
// timeout that every remaining case shares
|
||||
if errors.Is(err, testutils.ErrCommandTimeout) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if retries > 1 {
|
||||
return fmt.Errorf("test %s failed after %d attempts: %w", testCase.Path, retries, err)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
id: disable-http-cache
|
||||
|
||||
info:
|
||||
name: Disable HTTP Cache Test
|
||||
author: dwisiswant0
|
||||
severity: info
|
||||
description: Tests if disable-http-cache works as expected
|
||||
tags: test
|
||||
|
||||
requests:
|
||||
- raw:
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
disable-http-cache: true
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- body_1 == "1"
|
||||
- body_2 == "2"
|
||||
@@ -0,0 +1,23 @@
|
||||
id: http-cache
|
||||
|
||||
info:
|
||||
name: HTTP Cache Test
|
||||
author: dwisiswant0
|
||||
severity: info
|
||||
description: Tests if HTTP cache (RFC 9111) works as expected
|
||||
tags: test
|
||||
|
||||
requests:
|
||||
- raw:
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
- |
|
||||
GET / HTTP/1.1
|
||||
Host: {{Hostname}}
|
||||
disable-http-cache: false
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- body_1 == "1"
|
||||
- body_2 == "1"
|
||||
+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"
|
||||
@@ -11,7 +11,6 @@ network:
|
||||
inputs:
|
||||
- data: "50494e47"
|
||||
type: hex
|
||||
- data: "\r\n"
|
||||
|
||||
read-size: 4
|
||||
matchers:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,12 +12,29 @@ import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/projectdiscovery/utils/conversion"
|
||||
)
|
||||
|
||||
// defaultCommandTimeout bounds a single nuclei process in integration tests.
|
||||
// The CI job is 50m; without this a hung scan leaves the suite nameless until
|
||||
// the runner kills the step. It must stay well above the slowest legitimate
|
||||
// case: raw-unsafe-path scans scanme.sh over the network and can take minutes.
|
||||
const defaultCommandTimeout = 10 * time.Minute
|
||||
|
||||
// commandWaitDelay bounds the wait for output pipes after the process is
|
||||
// killed. A grandchild that inherited stdout/stderr keeps the pipe open, and
|
||||
// without this Output/CombinedOutput blocks forever despite the killed process.
|
||||
const commandWaitDelay = 10 * time.Second
|
||||
|
||||
// ErrCommandTimeout reports that nuclei was killed by commandTimeout. Callers
|
||||
// treat it as fatal rather than flaky: retrying a hang only burns the budget
|
||||
// the whole suite shares.
|
||||
var ErrCommandTimeout = errors.New("nuclei timed out")
|
||||
|
||||
type Runner struct {
|
||||
BinaryPath string
|
||||
WorkingDir string
|
||||
@@ -26,6 +44,7 @@ type Runner struct {
|
||||
AllowLocalFileAccess bool
|
||||
InteractionsPollDuration string
|
||||
InteractionsCooldownPeriod string
|
||||
CommandTimeout time.Duration
|
||||
}
|
||||
|
||||
type RunnerOption func(*Runner)
|
||||
@@ -38,6 +57,7 @@ func NewRunner(options ...RunnerOption) *Runner {
|
||||
AllowLocalFileAccess: true,
|
||||
InteractionsPollDuration: "1",
|
||||
InteractionsCooldownPeriod: "10",
|
||||
CommandTimeout: defaultCommandTimeout,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(runner)
|
||||
@@ -56,6 +76,7 @@ func (r *Runner) Clone(options ...RunnerOption) *Runner {
|
||||
AllowLocalFileAccess: r.AllowLocalFileAccess,
|
||||
InteractionsPollDuration: r.InteractionsPollDuration,
|
||||
InteractionsCooldownPeriod: r.InteractionsCooldownPeriod,
|
||||
CommandTimeout: r.CommandTimeout,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(clone)
|
||||
@@ -106,7 +127,14 @@ func SetDefaultRunner(runner *Runner) {
|
||||
defaultRunner = runner.Clone()
|
||||
}
|
||||
|
||||
func (r *Runner) command(binaryPath string, args ...string) *exec.Cmd {
|
||||
func (r *Runner) commandTimeout() time.Duration {
|
||||
if r != nil && r.CommandTimeout > 0 {
|
||||
return r.CommandTimeout
|
||||
}
|
||||
return defaultCommandTimeout
|
||||
}
|
||||
|
||||
func (r *Runner) command(ctx context.Context, binaryPath string, args ...string) *exec.Cmd {
|
||||
resolvedBinary := strings.TrimSpace(binaryPath)
|
||||
if resolvedBinary == "" {
|
||||
resolvedBinary = strings.TrimSpace(r.BinaryPath)
|
||||
@@ -116,7 +144,8 @@ func (r *Runner) command(binaryPath string, args ...string) *exec.Cmd {
|
||||
resolvedBinary = "nuclei"
|
||||
}
|
||||
|
||||
cmd := exec.Command(resolvedBinary, args...)
|
||||
cmd := exec.CommandContext(ctx, resolvedBinary, args...)
|
||||
cmd.WaitDelay = commandWaitDelay
|
||||
if r.WorkingDir != "" {
|
||||
cmd.Dir = r.WorkingDir
|
||||
}
|
||||
@@ -124,6 +153,28 @@ func (r *Runner) command(binaryPath string, args ...string) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *Runner) runCommand(combined bool, binaryPath string, setup func(cmd *exec.Cmd)) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.commandTimeout())
|
||||
defer cancel()
|
||||
|
||||
cmd := r.command(ctx, binaryPath)
|
||||
setup(cmd)
|
||||
|
||||
var (
|
||||
output []byte
|
||||
err error
|
||||
)
|
||||
if combined {
|
||||
output, err = cmd.CombinedOutput()
|
||||
} else {
|
||||
output, err = cmd.Output()
|
||||
}
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return output, fmt.Errorf("%w after %s: %v", ErrCommandTimeout, r.commandTimeout(), err)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (r *Runner) buildArgs(args []string, pollDuration string) []string {
|
||||
builtArgs := append([]string{}, args...)
|
||||
|
||||
@@ -188,20 +239,17 @@ func (r *Runner) WorkflowResults(workflow, url string, debug bool, extra ...stri
|
||||
}
|
||||
|
||||
func (r *Runner) BareResults(debug bool, env []string, args ...string) ([]string, error) {
|
||||
cmd := r.command("")
|
||||
cmd.Args = append(cmd.Args[:1], r.buildArgs(args, r.InteractionsPollDuration)...)
|
||||
|
||||
cmd.Env = r.buildEnv(env)
|
||||
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
cmd.Stderr = os.Stderr
|
||||
fmt.Println(cmd.String())
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "-silent")
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
output, err := r.runCommand(false, "", func(cmd *exec.Cmd) {
|
||||
cmd.Args = append(cmd.Args[:1], r.buildArgs(args, r.InteractionsPollDuration)...)
|
||||
cmd.Env = r.buildEnv(env)
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
cmd.Stderr = os.Stderr
|
||||
fmt.Println(cmd.String())
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "-silent")
|
||||
}
|
||||
})
|
||||
if debug && len(output) > 0 {
|
||||
fmt.Println(strings.TrimSpace(conversion.String(output)))
|
||||
}
|
||||
@@ -210,19 +258,20 @@ func (r *Runner) BareResults(debug bool, env []string, args ...string) ([]string
|
||||
}
|
||||
|
||||
func (r *Runner) ArgsResults(debug bool, args ...string) ([]string, error) {
|
||||
cmd := r.command("", append(append([]string{}, args...), r.ExtraArgs...)...)
|
||||
|
||||
cmd.Env = r.buildEnv(nil)
|
||||
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
cmd.Stderr = os.Stderr
|
||||
fmt.Println(cmd.String())
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "-silent")
|
||||
}
|
||||
|
||||
output, err := cmd.Output()
|
||||
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
|
||||
fmt.Println(cmd.String())
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "-silent")
|
||||
}
|
||||
})
|
||||
if debug && len(output) > 0 {
|
||||
fmt.Println(strings.TrimSpace(conversion.String(output)))
|
||||
}
|
||||
@@ -231,12 +280,11 @@ func (r *Runner) ArgsResults(debug bool, args ...string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (r *Runner) ArgsErrors(debug bool, env []string, args ...string) ([]string, error) {
|
||||
cmd := r.command("")
|
||||
cmd.Args = append(cmd.Args[:1], r.buildArgs(args, r.InteractionsPollDuration)...)
|
||||
cmd.Args = append(cmd.Args, "-nc")
|
||||
cmd.Env = r.buildEnv(env)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := r.runCommand(true, "", func(cmd *exec.Cmd) {
|
||||
cmd.Args = append(cmd.Args[:1], r.buildArgs(args, r.InteractionsPollDuration)...)
|
||||
cmd.Args = append(cmd.Args, "-nc")
|
||||
cmd.Env = r.buildEnv(env)
|
||||
})
|
||||
if debug && len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
@@ -263,25 +311,26 @@ func (r *Runner) ArgsResultsWithEnv(debug bool, env []string, args ...string) ([
|
||||
}
|
||||
|
||||
func (r *Runner) LoadedTemplates(binaryPath string, debug bool, args []string) (string, error) {
|
||||
cmd := r.command(binaryPath, 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")
|
||||
fmt.Println(cmd.String())
|
||||
}
|
||||
|
||||
data, err := cmd.CombinedOutput()
|
||||
data, err := r.runCommand(true, binaryPath, 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")
|
||||
fmt.Println(cmd.String())
|
||||
}
|
||||
})
|
||||
if debug && len(data) > 0 {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
out := strings.TrimSpace(string(data))
|
||||
if out != "" {
|
||||
return "", fmt.Errorf("%w: %s", err, out)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -295,16 +344,14 @@ func (r *Runner) LoadedTemplates(binaryPath string, debug bool, args []string) (
|
||||
|
||||
func (r *Runner) CombinedOutput(debug bool, args []string) (string, error) {
|
||||
builtArgs := r.buildArgs(args, r.InteractionsPollDuration)
|
||||
cmd := r.command("", builtArgs...)
|
||||
|
||||
cmd.Env = r.buildEnv(nil)
|
||||
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
fmt.Println(cmd.String())
|
||||
}
|
||||
|
||||
data, err := cmd.CombinedOutput()
|
||||
data, err := r.runCommand(true, "", func(cmd *exec.Cmd) {
|
||||
cmd.Args = append(cmd.Args[:1], builtArgs...)
|
||||
cmd.Env = r.buildEnv(nil)
|
||||
if debug {
|
||||
cmd.Args = append(cmd.Args, "-debug")
|
||||
fmt.Println(cmd.String())
|
||||
}
|
||||
})
|
||||
if debug && len(data) > 0 {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
@@ -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,28 @@
|
||||
package nuclei
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewNucleiEngineRejectsCorruptActiveIgnoreFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, config.NucleiIgnoreFileName)
|
||||
require.NoError(t, os.WriteFile(path, []byte("tags: ["), 0o600))
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
engine, err := NewNucleiEngineCtx(context.Background())
|
||||
require.Nil(t, engine)
|
||||
require.ErrorContains(t, err, "error parsing")
|
||||
require.ErrorContains(t, err, strconv.Quote(path))
|
||||
}
|
||||
@@ -50,6 +50,9 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
// would otherwise route all findings to whichever call last won the race.
|
||||
DoNotCache: true,
|
||||
}
|
||||
if base.interactshClient != nil {
|
||||
u.executerOpts.InteractshScope = base.interactshClient.NewRequestScope()
|
||||
}
|
||||
if opts.ShouldUseHostError() && base.hostErrCache != nil {
|
||||
u.executerOpts.HostErrorsCache = base.hostErrCache
|
||||
}
|
||||
@@ -64,6 +67,32 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// restoreBaseExcludeTags re-adds any of the engine's baseline excluded tags that
|
||||
// a per-execution option dropped, appending only what is missing so the result
|
||||
// is order-stable and repeated application is a no-op.
|
||||
//
|
||||
// The current ExcludeTags slice is cloned before merging so a caller-owned
|
||||
// backing array (e.g. a reused WithTemplateFilters ExcludeTags value with spare
|
||||
// capacity) is never mutated or shared across concurrent executions.
|
||||
func restoreBaseExcludeTags(base []string, opts *types.Options) {
|
||||
if len(base) == 0 {
|
||||
return
|
||||
}
|
||||
merged := append([]string(nil), opts.ExcludeTags...)
|
||||
present := make(map[string]struct{}, len(merged)+len(base))
|
||||
for _, tag := range merged {
|
||||
present[tag] = struct{}{}
|
||||
}
|
||||
for _, tag := range base {
|
||||
if _, ok := present[tag]; ok {
|
||||
continue
|
||||
}
|
||||
merged = append(merged, tag)
|
||||
present[tag] = struct{}{}
|
||||
}
|
||||
opts.ExcludeTags = merged
|
||||
}
|
||||
|
||||
// resolveEphemeralOutput combines the base/global writer with any per-call result callbacks.
|
||||
// Global callbacks keep working; per-call callbacks are isolated to this execution.
|
||||
func resolveEphemeralOutput(base, call *NucleiEngine) output.Writer {
|
||||
@@ -90,6 +119,9 @@ func resolveEphemeralOutput(base, call *NucleiEngine) output.Writer {
|
||||
|
||||
// closeEphemeralObjects closes all resources used by ephemeral nuclei objects/instances/types
|
||||
func closeEphemeralObjects(u *unsafeOptions) {
|
||||
if u.executerOpts.InteractshScope != nil {
|
||||
u.executerOpts.InteractshScope.Close()
|
||||
}
|
||||
if u.executerOpts.RateLimiter != nil {
|
||||
u.executerOpts.RateLimiter.Stop()
|
||||
}
|
||||
@@ -98,6 +130,7 @@ func closeEphemeralObjects(u *unsafeOptions) {
|
||||
u.executerOpts.Output = nil
|
||||
u.executerOpts.IssuesClient = nil
|
||||
u.executerOpts.Interactsh = nil
|
||||
u.executerOpts.InteractshScope = nil
|
||||
u.executerOpts.HostErrorsCache = nil
|
||||
u.executerOpts.Progress = nil
|
||||
u.executerOpts.Catalog = nil
|
||||
@@ -161,6 +194,18 @@ func (e *ThreadSafeNucleiEngine) ExecuteNucleiWithOptsCtx(ctx context.Context, t
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Per-execution options land on a COPY of the engine's options, so an option
|
||||
// that assigns a whole filter set clears every field it does not name —
|
||||
// WithTemplateFilters does exactly that. ExcludeTags is one of those fields,
|
||||
// and it carries the .nuclei-ignore deny-list that applyRequiredDefaults
|
||||
// installed at construction; nothing re-reads the ignore file afterwards, so
|
||||
// the exclusions would be lost for this execution and templates tagged dos,
|
||||
// bruteforce, fuzz, local or txt-service would run.
|
||||
//
|
||||
// Restore the engine's baseline exclusions so a per-execution filter can add
|
||||
// to them but never silently discard them. IncludeTags remains the explicit
|
||||
// per-tag override for callers who do want an ignored template to run.
|
||||
restoreBaseExcludeTags(e.eng.opts.ExcludeTags, tmpEngine.opts)
|
||||
|
||||
// create ephemeral nuclei objects/instances/types using base nuclei engine
|
||||
unsafeOpts, err := createEphemeralObjects(ctx, e.eng, tmpEngine.opts, resolveEphemeralOutput(e.eng, tmpEngine))
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestExecuteNucleiWithOptsCtxKeepsBaseExcludeTags covers the case where a
|
||||
// per-execution filter that names only Tags used to clear the engine's
|
||||
// ExcludeTags — which carry the .nuclei-ignore deny-list installed at
|
||||
// construction — letting templates tagged dos (and local, fuzz, bruteforce,
|
||||
// txt-service) execute against the target.
|
||||
//
|
||||
// The engine's construction-time ExcludeTags stand in for that deny-list, and
|
||||
// the per-execution filter deliberately asks for the excluded tag: exclusion
|
||||
// must still win, because IncludeTags is the documented way to override it.
|
||||
func TestExecuteNucleiWithOptsCtxKeepsBaseExcludeTags(t *testing.T) {
|
||||
templatePath := writeSDKDosTaggedTemplate(t)
|
||||
|
||||
var reached atomic.Bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
reached.Store(true)
|
||||
_, _ = w.Write([]byte("token-dos"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(),
|
||||
nuclei.WithTemplateFilters(nuclei.TemplateFilters{ExcludeTags: []string{"dos"}}),
|
||||
)
|
||||
require.NoError(t, err, "could not create thread-safe nuclei engine")
|
||||
t.Cleanup(ne.Close)
|
||||
|
||||
var matches atomic.Int64
|
||||
ne.GlobalResultCallback(func(*output.ResultEvent) { matches.Add(1) })
|
||||
|
||||
err = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srv.URL},
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
|
||||
nuclei.WithTemplateFilters(nuclei.TemplateFilters{Tags: []string{"dos"}}),
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, nuclei.ErrNoTemplatesAvailable,
|
||||
"the dos-tagged template must stay excluded, leaving nothing to run")
|
||||
assert.Zero(t, matches.Load(), "an excluded template must not report results")
|
||||
assert.False(t, reached.Load(), "an excluded template must not reach the target")
|
||||
}
|
||||
|
||||
// TestExecuteNucleiWithOptsCtxHonoursIncludeTags is the other half of the
|
||||
// contract: restoring the baseline exclusions must not break the documented
|
||||
// per-tag override, or callers lose the only way to run an ignored template.
|
||||
func TestExecuteNucleiWithOptsCtxHonoursIncludeTags(t *testing.T) {
|
||||
templatePath := writeSDKDosTaggedTemplate(t)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("token-dos"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(),
|
||||
nuclei.WithTemplateFilters(nuclei.TemplateFilters{ExcludeTags: []string{"dos"}}),
|
||||
)
|
||||
require.NoError(t, err, "could not create thread-safe nuclei engine")
|
||||
t.Cleanup(ne.Close)
|
||||
|
||||
var matches atomic.Int64
|
||||
ne.GlobalResultCallback(func(*output.ResultEvent) { matches.Add(1) })
|
||||
|
||||
err = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srv.URL},
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
|
||||
nuclei.WithTemplateFilters(nuclei.TemplateFilters{IncludeTags: []string{"dos"}}),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, matches.Load(), "IncludeTags should still override the exclusion")
|
||||
}
|
||||
|
||||
func writeSDKDosTaggedTemplate(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "sdk-dos-tagged.yaml")
|
||||
content := `id: sdk-dos-tagged
|
||||
info:
|
||||
name: SDK dos tagged
|
||||
author: nuclei-sdk-test
|
||||
severity: info
|
||||
tags: dos
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/"
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "token-"
|
||||
`
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
||||
return path
|
||||
}
|
||||
|
||||
// TestExecuteNucleiWithOptsCtxKeepsIgnoreFileExcludeTags is the end-to-end path
|
||||
// for #7695 once #7697 is on the tree: tags from a real .nuclei-ignore must
|
||||
// survive a per-execution WithTemplateFilters that only sets Tags.
|
||||
func TestExecuteNucleiWithOptsCtxKeepsIgnoreFileExcludeTags(t *testing.T) {
|
||||
previousConfigDir := config.DefaultConfig.GetConfigDir()
|
||||
previousTemplatesDir := config.DefaultConfig.TemplatesDirectory
|
||||
previousStateDir := config.DefaultConfig.GetStateDir()
|
||||
t.Cleanup(func() {
|
||||
config.DefaultConfig.SetConfigDir(previousConfigDir)
|
||||
config.DefaultConfig.SetStateDir(previousStateDir)
|
||||
config.DefaultConfig.SetTemplatesDir(previousTemplatesDir)
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
config.DefaultConfig.SetConfigDir(dir)
|
||||
config.DefaultConfig.SetTemplatesDir(dir)
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
config.DefaultConfig.GetActiveIgnoreFilePath(),
|
||||
[]byte("tags:\n - dos\n"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
templatePath := writeSDKDosTaggedTemplate(t)
|
||||
|
||||
var reached atomic.Bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
reached.Store(true)
|
||||
_, _ = w.Write([]byte("token-dos"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
ne, err := nuclei.NewThreadSafeNucleiEngineCtx(context.Background(),
|
||||
nuclei.WithOptions(types.DefaultOptions()),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
)
|
||||
require.NoError(t, err, "could not create thread-safe nuclei engine")
|
||||
t.Cleanup(ne.Close)
|
||||
var matches atomic.Int64
|
||||
ne.GlobalResultCallback(func(*output.ResultEvent) { matches.Add(1) })
|
||||
|
||||
err = ne.ExecuteNucleiWithOptsCtx(context.Background(), []string{srv.URL},
|
||||
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{Templates: []string{templatePath}}),
|
||||
nuclei.WithTemplateFilters(nuclei.TemplateFilters{Tags: []string{"dos"}}),
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, nuclei.ErrNoTemplatesAvailable,
|
||||
"ignore-file dos exclusion must survive a Tags-only per-execution filter")
|
||||
assert.Zero(t, matches.Load())
|
||||
assert.False(t, reached.Load())
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package nuclei
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRestoreBaseExcludeTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base []string
|
||||
current []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "per-execution filter cleared the deny-list",
|
||||
base: []string{"dos", "fuzz"},
|
||||
current: nil,
|
||||
expected: []string{"dos", "fuzz"},
|
||||
},
|
||||
{
|
||||
name: "per-execution exclusions are kept alongside the baseline",
|
||||
base: []string{"dos"},
|
||||
current: []string{"custom"},
|
||||
expected: []string{"custom", "dos"},
|
||||
},
|
||||
{
|
||||
name: "entries already present are not duplicated",
|
||||
base: []string{"dos", "fuzz"},
|
||||
current: []string{"fuzz"},
|
||||
expected: []string{"fuzz", "dos"},
|
||||
},
|
||||
{
|
||||
name: "no baseline is a no-op",
|
||||
base: nil,
|
||||
current: []string{"custom"},
|
||||
expected: []string{"custom"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
opts := &types.Options{ExcludeTags: tt.current}
|
||||
restoreBaseExcludeTags(tt.base, opts)
|
||||
assert.Equal(t, tt.expected, []string(opts.ExcludeTags))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreBaseExcludeTagsIsIdempotent matters because the helper runs on
|
||||
// every execution against a long-lived engine.
|
||||
func TestRestoreBaseExcludeTagsIsIdempotent(t *testing.T) {
|
||||
opts := &types.Options{}
|
||||
restoreBaseExcludeTags([]string{"dos", "fuzz"}, opts)
|
||||
restoreBaseExcludeTags([]string{"dos", "fuzz"}, opts)
|
||||
|
||||
assert.Equal(t, []string{"dos", "fuzz"}, []string(opts.ExcludeTags))
|
||||
}
|
||||
|
||||
// TestRestoreBaseExcludeTagsDoesNotMutateCallerSlice guards the concurrent
|
||||
// ThreadSafeNucleiEngine case where WithTemplateFilters assigns a caller-owned
|
||||
// ExcludeTags slice with spare capacity: a naive append would race and mutate
|
||||
// the caller's backing array.
|
||||
func TestRestoreBaseExcludeTagsDoesNotMutateCallerSlice(t *testing.T) {
|
||||
shared := make([]string, 1, 8)
|
||||
shared[0] = "custom"
|
||||
base := []string{"dos", "fuzz"}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 32; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
opts := &types.Options{ExcludeTags: shared}
|
||||
restoreBaseExcludeTags(base, opts)
|
||||
if got := []string(opts.ExcludeTags); len(got) != 3 || got[0] != "custom" || got[1] != "dos" || got[2] != "fuzz" {
|
||||
panic(got)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
assert.Equal(t, []string{"custom"}, shared[:1])
|
||||
assert.Equal(t, make([]string, 7), shared[1:cap(shared)])
|
||||
}
|
||||
+27
-4
@@ -135,13 +135,32 @@ func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
|
||||
e.opts.ExcludeTags = []string{}
|
||||
}
|
||||
|
||||
// these templates are known to have weak matchers
|
||||
// and idea is to disable them to avoid false positives
|
||||
e.opts.ExcludeTags = append(e.opts.ExcludeTags, config.ReadIgnoreFile().Tags...)
|
||||
|
||||
e.inputProvider = provider.NewSimpleInputProvider()
|
||||
}
|
||||
|
||||
func (e *NucleiEngine) loadIgnoreFile() error {
|
||||
ignoreFile, err := config.ReadIgnoreFile()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
e.Logger.Warning().Msgf("Could not read active .nuclei-ignore file: %s; continuing without ignore exclusions", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// .nuclei-ignore blocks templates that should not run by default, by tag
|
||||
// (dos, fuzz, ...) and by path (templates known to have weak matchers, which
|
||||
// would otherwise generate false positives). Both sections are applied, as
|
||||
// the CLI runner does — applying only the tags leaves the `files` section
|
||||
// inert for every SDK consumer.
|
||||
e.opts.ExcludeTags = append(e.opts.ExcludeTags, ignoreFile.Tags...)
|
||||
e.opts.ExcludedTemplates = append(e.opts.ExcludedTemplates, ignoreFile.Files...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// init
|
||||
func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
if err := config.DefaultConfig.InitializationError(); err != nil {
|
||||
@@ -165,6 +184,10 @@ 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
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -120,3 +121,37 @@ func TestWithOptionsRateLimitSetsRuntimeLimiter(t *testing.T) {
|
||||
require.NotNil(t, execOpts.RateLimiter, "rate limiter should be initialized")
|
||||
require.Equal(t, opts.RateLimit, int(execOpts.RateLimiter.GetLimit()), "runtime limiter should match rate limit from WithOptions")
|
||||
}
|
||||
|
||||
// TestIgnoreFileFilesSectionIsApplied verifies the SDK applies BOTH sections of
|
||||
// .nuclei-ignore, matching the CLI runner. The files section suppresses
|
||||
// templates known to have weak matchers, so skipping it hands SDK consumers
|
||||
// false positives the CLI would never report.
|
||||
func TestIgnoreFileFilesSectionIsApplied(t *testing.T) {
|
||||
previousConfigDir := config.DefaultConfig.GetConfigDir()
|
||||
previousTemplatesDir := config.DefaultConfig.TemplatesDirectory
|
||||
previousStateDir := config.DefaultConfig.GetStateDir()
|
||||
t.Cleanup(func() {
|
||||
config.DefaultConfig.SetConfigDir(previousConfigDir)
|
||||
config.DefaultConfig.SetStateDir(previousStateDir)
|
||||
config.DefaultConfig.SetTemplatesDir(previousTemplatesDir)
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
config.DefaultConfig.SetConfigDir(dir)
|
||||
config.DefaultConfig.SetTemplatesDir(dir)
|
||||
|
||||
ignoreFile := "tags:\n - dos\nfiles:\n - http/fuzzing/wordpress-themes-detect.yaml\n"
|
||||
require.NoError(t, os.WriteFile(config.DefaultConfig.GetActiveIgnoreFilePath(), []byte(ignoreFile), 0o600))
|
||||
|
||||
ne, err := nuclei.NewNucleiEngineCtx(context.Background(),
|
||||
nuclei.WithOptions(types.DefaultOptions()),
|
||||
nuclei.DisableUpdateCheck(),
|
||||
)
|
||||
require.NoError(t, err, "could not create nuclei engine")
|
||||
defer ne.Close()
|
||||
|
||||
require.Contains(t, ne.Options().ExcludeTags, "dos",
|
||||
"tags section should reach ExcludeTags")
|
||||
require.Contains(t, ne.Options().ExcludedTemplates, "http/fuzzing/wordpress-themes-detect.yaml",
|
||||
"files section should reach ExcludedTemplates")
|
||||
}
|
||||
@@ -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
|
||||
@@ -28,6 +31,9 @@ var knownLeaks = []goleak.Option{
|
||||
goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),
|
||||
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),
|
||||
goleak.IgnoreAnyFunction("github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate.StartActiveMemGuardian.func1"),
|
||||
// NOTE(dwisiswant0): This is an indirect call and not used anywhere, not
|
||||
// even within SOPS (#5841). See `go mod why go.opencensus.io/stats/view`.
|
||||
goleak.IgnoreAnyContainingPkg("go.opencensus.io/stats/view"),
|
||||
}
|
||||
|
||||
func TestSimpleNuclei(t *testing.T) {
|
||||
|
||||
+65
-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"
|
||||
@@ -880,6 +896,11 @@
|
||||
"title": "optional disable cookie reuse",
|
||||
"description": "Optional setting that disables cookie reuse"
|
||||
},
|
||||
"disable-http-cache": {
|
||||
"type": "boolean",
|
||||
"title": "disable HTTP cache",
|
||||
"description": "Turns off HTTP caching for this request; cannot enable cache when -http-cache is unset"
|
||||
},
|
||||
"read-all": {
|
||||
"type": "boolean",
|
||||
"title": "force read all body",
|
||||
@@ -1154,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": [
|
||||
@@ -1193,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)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
||||
@@ -19,6 +23,9 @@ type LazyFetchSecret func(d *Dynamic) error
|
||||
type fetchState struct {
|
||||
once sync.Once
|
||||
err error
|
||||
// owner is the goroutine running the fetch callback. Nested Fetch/GetStrategies
|
||||
// on that goroutine must not wait on once (sync.Once deadlocks on re-entry).
|
||||
owner atomic.Int64
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -191,6 +198,11 @@ func (d *Dynamic) applyValuesToSecret(secret *Secret) error {
|
||||
|
||||
// GetStrategies returns the auth strategies for the dynamic secret
|
||||
func (d *Dynamic) GetStrategies() []AuthStrategy {
|
||||
// Nested lookup from inside the fetch callback (login template ApplyAuth)
|
||||
// must not wait on Fetch and must not apply still-unresolved secrets.
|
||||
if d.fetchingOnThisGoroutine() {
|
||||
return nil
|
||||
}
|
||||
// Ensure fetch has completed before returning strategies.
|
||||
// Fetch errors are treated as non-fatal here so a failed dynamic auth fetch
|
||||
// does not terminate the entire scan process.
|
||||
@@ -219,7 +231,13 @@ func (d *Dynamic) Fetch(isFatal bool) error {
|
||||
return errkit.New("dynamic secret not validated: call Validate() before Fetch()")
|
||||
}
|
||||
|
||||
if d.fetchingOnThisGoroutine() {
|
||||
return d.fetchState.err
|
||||
}
|
||||
|
||||
d.fetchState.once.Do(func() {
|
||||
d.fetchState.owner.Store(goroutineID())
|
||||
defer d.fetchState.owner.Store(0)
|
||||
if d.fetchCallback == nil {
|
||||
d.fetchState.err = errkit.New("dynamic secret fetch callback not set: call SetLazyFetchCallback() before Fetch()")
|
||||
return
|
||||
@@ -240,3 +258,31 @@ func (d *Dynamic) Error() error {
|
||||
}
|
||||
return d.fetchState.err
|
||||
}
|
||||
|
||||
func (d *Dynamic) fetchingOnThisGoroutine() bool {
|
||||
if d == nil || d.fetchState == nil {
|
||||
return false
|
||||
}
|
||||
owner := d.fetchState.owner.Load()
|
||||
return owner != 0 && owner == goroutineID()
|
||||
}
|
||||
|
||||
func goroutineID() int64 {
|
||||
var buf [64]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
s := buf[:n]
|
||||
const prefix = "goroutine "
|
||||
if !bytes.HasPrefix(s, []byte(prefix)) {
|
||||
return 0
|
||||
}
|
||||
s = s[len(prefix):]
|
||||
i := bytes.IndexByte(s, ' ')
|
||||
if i <= 0 {
|
||||
return 0
|
||||
}
|
||||
id, err := strconv.ParseInt(string(s[:i]), 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return id
|
||||
}
|
||||
@@ -226,6 +226,78 @@ func TestDynamicFetchConcurrent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDynamicFetchReentry(t *testing.T) {
|
||||
t.Run("nested Fetch from callback does not deadlock", func(t *testing.T) {
|
||||
d := &Dynamic{
|
||||
TemplatePath: "auth.yaml",
|
||||
Variables: []KV{{Key: "user", Value: "admin"}},
|
||||
}
|
||||
require.NoError(t, d.Validate())
|
||||
|
||||
var nestedErr error
|
||||
d.SetLazyFetchCallback(func(dynamic *Dynamic) error {
|
||||
nestedErr = dynamic.Fetch(false)
|
||||
dynamic.Extracted = map[string]interface{}{"token": "abc"}
|
||||
return nil
|
||||
})
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- d.Fetch(false)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, nestedErr)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Fetch deadlocked on re-entry")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nested GetStrategies does not apply unresolved secrets", func(t *testing.T) {
|
||||
d := &Dynamic{
|
||||
Secret: &Secret{
|
||||
Type: "Header",
|
||||
Domains: []string{"example.com"},
|
||||
Headers: []KV{{Key: "X-Token", Value: "{{token}}"}},
|
||||
},
|
||||
TemplatePath: "auth.yaml",
|
||||
Variables: []KV{{Key: "user", Value: "admin"}},
|
||||
}
|
||||
require.NoError(t, d.Validate())
|
||||
|
||||
d.SetLazyFetchCallback(func(dynamic *Dynamic) error {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
strategy := &DynamicAuthStrategy{Dynamic: *dynamic}
|
||||
strategy.Apply(req)
|
||||
require.Empty(t, req.Header.Get("X-Token"), "login request must not receive secret-file auth during fetch")
|
||||
require.Nil(t, dynamic.GetStrategies())
|
||||
dynamic.Extracted = map[string]interface{}{"token": "abc"}
|
||||
return nil
|
||||
})
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- d.Fetch(false)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("GetStrategies deadlocked on re-entry")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
strategy := &DynamicAuthStrategy{Dynamic: *d}
|
||||
strategy.Apply(req)
|
||||
require.Equal(t, "abc", req.Header.Get("X-Token"), "scan requests must still receive secrets after fetch")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDynamicValidate(t *testing.T) {
|
||||
t.Run("missing template path", func(t *testing.T) {
|
||||
d := &Dynamic{
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/getsops/sops/v3"
|
||||
"github.com/getsops/sops/v3/decrypt"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
"github.com/projectdiscovery/utils/generic"
|
||||
@@ -205,18 +208,30 @@ func (c *Cookie) Parse() error {
|
||||
|
||||
// GetAuthDataFromFile reads the auth data from file
|
||||
func GetAuthDataFromFile(file string) (*Authx, error) {
|
||||
ext := filepath.Ext(file)
|
||||
if !generic.EqualsAny(ext, ".yml", ".yaml", ".json") {
|
||||
return nil, fmt.Errorf("invalid file extension: supported extensions are .yml,.yaml and .json got %s", ext)
|
||||
var data []byte
|
||||
|
||||
ext := strings.TrimPrefix(filepath.Ext(file), ".")
|
||||
if !generic.EqualsAny(ext, "yml", "yaml", "json") {
|
||||
return nil, fmt.Errorf(`unsupported extensions: valid extensions are "yml", "yaml", and "json"; got %q`, ext)
|
||||
}
|
||||
bin, err := os.ReadFile(file)
|
||||
|
||||
data, err := decrypt.File(file, ext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.Is(err, sops.MetadataNotFound) {
|
||||
data, err = os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read secret file: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not decrypt file: %w", err)
|
||||
}
|
||||
}
|
||||
if ext == ".yml" || ext == ".yaml" {
|
||||
return GetAuthDataFromYAML(bin)
|
||||
|
||||
if ext == "yml" || ext == "yaml" {
|
||||
return GetAuthDataFromYAML(data)
|
||||
}
|
||||
return GetAuthDataFromJSON(bin)
|
||||
|
||||
return GetAuthDataFromJSON(data)
|
||||
}
|
||||
|
||||
// GetTemplatePathsFromSecretFile reads the template IDs from the secret file
|
||||
|
||||
@@ -1,20 +1,100 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/getsops/sops/v3"
|
||||
"github.com/getsops/sops/v3/aes"
|
||||
sopsage "github.com/getsops/sops/v3/age"
|
||||
"github.com/getsops/sops/v3/config"
|
||||
"github.com/getsops/sops/v3/stores/yaml"
|
||||
"github.com/getsops/sops/v3/version"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSecretsUnmarshal(t *testing.T) {
|
||||
loc := "testData/example-auth.yaml"
|
||||
data, err := GetAuthDataFromFile(loc)
|
||||
require.Nil(t, err, "could not read secrets file")
|
||||
data, err := GetAuthDataFromFile("testData/example-auth.yaml")
|
||||
require.NoError(t, err, "could not read secrets file")
|
||||
require.NotNil(t, data, "could not read secrets file")
|
||||
|
||||
t.Run("encrypted", func(t *testing.T) {
|
||||
loc := encryptAuthFile(t, "testData/example-auth.yaml")
|
||||
decryptedData, err := GetAuthDataFromFile(loc)
|
||||
require.NoError(t, err, "could not read encrypted secrets file")
|
||||
require.NotNil(t, decryptedData, "could not read secrets file")
|
||||
require.Equal(t, data, decryptedData, "decrypted data should match original data")
|
||||
|
||||
for _, s := range decryptedData.Secrets {
|
||||
require.NoError(t, s.Validate(), "could not validate secret")
|
||||
}
|
||||
|
||||
for _, d := range decryptedData.Dynamic {
|
||||
require.NoError(t, d.Validate(), "could not validate dynamic")
|
||||
}
|
||||
})
|
||||
|
||||
for _, s := range data.Secrets {
|
||||
require.Nil(t, s.Validate(), "could not validate secret")
|
||||
require.NoError(t, s.Validate(), "could not validate secret")
|
||||
}
|
||||
|
||||
for _, d := range data.Dynamic {
|
||||
require.Nil(t, d.Validate(), "could not validate dynamic")
|
||||
require.NoError(t, d.Validate(), "could not validate dynamic")
|
||||
}
|
||||
}
|
||||
|
||||
func encryptAuthFile(t *testing.T, source string) string {
|
||||
t.Helper()
|
||||
|
||||
plaintext, err := os.ReadFile(source)
|
||||
require.NoError(t, err)
|
||||
|
||||
identity, err := age.GenerateX25519Identity()
|
||||
require.NoError(t, err)
|
||||
t.Setenv(sopsage.SopsAgeKeyEnv, identity.String())
|
||||
|
||||
masterKey, err := sopsage.MasterKeyFromRecipient(identity.Recipient().String())
|
||||
require.NoError(t, err)
|
||||
|
||||
dataKey := make([]byte, 32)
|
||||
_, err = rand.Read(dataKey)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, masterKey.Encrypt(dataKey))
|
||||
|
||||
store := yaml.NewStore(&config.YAMLStoreConfig{})
|
||||
branches, err := store.LoadPlainFile(plaintext)
|
||||
require.NoError(t, err)
|
||||
|
||||
tree := sops.Tree{
|
||||
Branches: branches,
|
||||
Metadata: sops.Metadata{
|
||||
EncryptedRegex: `^(password|username|token|value|key|raw)$`,
|
||||
KeyGroups: []sops.KeyGroup{{masterKey}},
|
||||
Version: version.Version,
|
||||
},
|
||||
}
|
||||
|
||||
cipher := aes.NewCipher()
|
||||
mac, err := tree.Encrypt(dataKey, cipher)
|
||||
require.NoError(t, err)
|
||||
tree.Metadata.LastModified = time.Now().UTC()
|
||||
tree.Metadata.MessageAuthenticationCode, err = cipher.Encrypt(
|
||||
mac,
|
||||
dataKey,
|
||||
tree.Metadata.LastModified.Format(time.RFC3339),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
encrypted, err := store.EmitEncryptedFile(tree)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(encrypted), "sops:")
|
||||
require.NotContains(t, string(encrypted), "1a2b3c4d5e6f7g8h9i0j")
|
||||
|
||||
path := filepath.Join(t.TempDir(), "example-auth.yaml")
|
||||
require.NoError(t, os.WriteFile(path, encrypted, 0600))
|
||||
return path
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,50 +1,141 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
)
|
||||
|
||||
// maxIgnoreFileBytes caps untrusted ignore-file payloads (PDTM downloads,
|
||||
// template archives) before YAML decode.
|
||||
const maxIgnoreFileBytes = 1 << 20
|
||||
|
||||
var errMultipleIgnoreFileDocuments = errors.New("multiple YAML documents are not supported")
|
||||
|
||||
// IgnoreFile is an internal nuclei template blocking configuration file
|
||||
type IgnoreFile struct {
|
||||
Tags []string `yaml:"tags"`
|
||||
Files []string `yaml:"files"`
|
||||
}
|
||||
|
||||
// ReadIgnoreFile reads the .nuclei-ignore file returning blocked tags and paths.
|
||||
func ReadIgnoreFile() IgnoreFile {
|
||||
// ReadIgnoreFile reads the active .nuclei-ignore file and returns blocked tags
|
||||
// and paths. Callers must decide whether a read error is recoverable. Malformed
|
||||
// content is returned as an error to prevent a fail-open scan.
|
||||
func ReadIgnoreFile() (IgnoreFile, error) {
|
||||
path := DefaultConfig.GetActiveIgnoreFilePath()
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Could not read .nuclei-ignore file %q: %s\n", path, err)
|
||||
return IgnoreFile{}
|
||||
return IgnoreFile{}, fmt.Errorf("error opening %q: %w", path, err)
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
ignore := IgnoreFile{}
|
||||
if err := yaml.NewDecoder(file).Decode(&ignore); err != nil {
|
||||
gologger.Error().Msgf("Could not parse .nuclei-ignore file %q: %s\n", path, err)
|
||||
return IgnoreFile{}
|
||||
ignore, err := decodeIgnoreFile(file, false)
|
||||
if err != nil {
|
||||
return IgnoreFile{}, fmt.Errorf("error parsing %q: %w", path, err)
|
||||
}
|
||||
|
||||
return ignore
|
||||
return ignore, nil
|
||||
}
|
||||
|
||||
// WriteActiveIgnoreFile atomically replaces the active root ignore file.
|
||||
func (c *Config) WriteActiveIgnoreFile(data []byte) error {
|
||||
if err := atomicWriteFile(c.GetActiveIgnoreFilePath(), data, 0o600); err != nil {
|
||||
path := c.GetActiveIgnoreFilePath()
|
||||
if err := validateIgnoreFileContents(data); err != nil {
|
||||
return fmt.Errorf("invalid active .nuclei-ignore file %q: %w", path, err)
|
||||
}
|
||||
|
||||
if err := atomicWriteFile(path, data, 0o600); err != nil {
|
||||
return fmt.Errorf("write active .nuclei-ignore file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIgnoreFileContents(data []byte) error {
|
||||
if len(data) > maxIgnoreFileBytes {
|
||||
return fmt.Errorf("payload exceeds %d bytes", maxIgnoreFileBytes)
|
||||
}
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return errors.New("empty document")
|
||||
}
|
||||
|
||||
if _, err := decodeIgnoreFile(bytes.NewReader(data), true); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return errors.New("empty document")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
var raw map[string]any
|
||||
if err := decoder.Decode(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw == nil {
|
||||
return errors.New("document must be a YAML mapping")
|
||||
}
|
||||
|
||||
allowed := ignoreFileFieldNames()
|
||||
found := false
|
||||
for key := range raw {
|
||||
if !slices.Contains(allowed, key) {
|
||||
return fmt.Errorf("unknown field %q", key)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("document must define at least one of %s", strings.Join(allowed, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeIgnoreFile(r io.Reader, knownFields bool) (IgnoreFile, error) {
|
||||
decoder := yaml.NewDecoder(r)
|
||||
decoder.KnownFields(knownFields)
|
||||
|
||||
ignore := IgnoreFile{}
|
||||
if err := decoder.Decode(&ignore); err != nil {
|
||||
return IgnoreFile{}, err
|
||||
}
|
||||
|
||||
var additionalDocument any
|
||||
if err := decoder.Decode(&additionalDocument); !errors.Is(err, io.EOF) {
|
||||
if err != nil {
|
||||
return IgnoreFile{}, err
|
||||
}
|
||||
|
||||
return IgnoreFile{}, errMultipleIgnoreFileDocuments
|
||||
}
|
||||
|
||||
return ignore, nil
|
||||
}
|
||||
|
||||
func ignoreFileFieldNames() []string {
|
||||
typ := reflect.TypeOf(IgnoreFile{})
|
||||
names := make([]string, 0, typ.NumField())
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
name, _, _ := strings.Cut(typ.Field(i).Tag.Get("yaml"), ",")
|
||||
if name == "" || name == "-" {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// IgnoreFileNeedsUpdate reports whether the active ignore file is
|
||||
// missing or differs from the latest known hash.
|
||||
func (c *Config) IgnoreFileNeedsUpdate(latestHash string) bool {
|
||||
|
||||
@@ -1,33 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadIgnoreFileAllowsMissingOrInvalidActiveFile(t *testing.T) {
|
||||
func setActiveIgnoreFileRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
root := t.TempDir()
|
||||
cfg := DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
return root
|
||||
}
|
||||
|
||||
if ignore := ReadIgnoreFile(); len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
|
||||
func TestReadIgnoreFileReportsMissingActiveFile(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
|
||||
ignore, err := ReadIgnoreFile()
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("read missing ignore file error = %v, want os.ErrNotExist", err)
|
||||
}
|
||||
if len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
|
||||
t.Fatalf("missing ignore file = %+v, want empty", ignore)
|
||||
}
|
||||
if message := err.Error(); !strings.Contains(message, strconv.Quote(path)) || !strings.Contains(message, "error opening") {
|
||||
t.Fatalf("missing ignore file error = %q, want open failure and active path", message)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(root, NucleiIgnoreFileName), []byte("tags: ["), 0o600); err != nil {
|
||||
func TestReadIgnoreFileRejectsCorruptActiveFile(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
|
||||
if err := os.WriteFile(path, []byte("tags: ["), 0o600); err != nil {
|
||||
t.Fatalf("write invalid ignore file: %v", err)
|
||||
}
|
||||
if ignore := ReadIgnoreFile(); len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
|
||||
t.Fatalf("invalid ignore file = %+v, want empty", ignore)
|
||||
ignore, err := ReadIgnoreFile()
|
||||
if err == nil {
|
||||
t.Fatalf("read corrupt ignore file = %+v, want error", ignore)
|
||||
}
|
||||
if message := err.Error(); !strings.Contains(message, strconv.Quote(path)) || !strings.Contains(message, "error parsing") {
|
||||
t.Fatalf("corrupt ignore file error = %q, want parse failure and active path", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadIgnoreFileRejectsMalformedTrailingDocument(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
|
||||
if err := os.WriteFile(path, []byte("tags: [weak]\n---\ntags: ["), 0o600); err != nil {
|
||||
t.Fatalf("write ignore file: %v", err)
|
||||
}
|
||||
ignore, err := ReadIgnoreFile()
|
||||
if err == nil {
|
||||
t.Fatalf("read ignore file = %+v, want trailing parse error", ignore)
|
||||
}
|
||||
if len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
|
||||
t.Fatalf("ignore file after trailing parse error = %+v, want empty", ignore)
|
||||
}
|
||||
if message := err.Error(); !strings.Contains(message, strconv.Quote(path)) || !strings.Contains(message, "error parsing") {
|
||||
t.Fatalf("trailing parse error = %q, want parse failure and active path", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadIgnoreFileRejectsAdditionalDocument(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
|
||||
if err := os.WriteFile(path, []byte("tags: [weak]\n---\nfiles: [blocked.yaml]\n"), 0o600); err != nil {
|
||||
t.Fatalf("write ignore file: %v", err)
|
||||
}
|
||||
ignore, err := ReadIgnoreFile()
|
||||
if err == nil {
|
||||
t.Fatalf("read ignore file = %+v, want multiple-document error", ignore)
|
||||
}
|
||||
if len(ignore.Tags) != 0 || len(ignore.Files) != 0 {
|
||||
t.Fatalf("ignore file with additional document = %+v, want empty", ignore)
|
||||
}
|
||||
if message := err.Error(); !strings.Contains(message, strconv.Quote(path)) || !strings.Contains(message, "multiple YAML documents") {
|
||||
t.Fatalf("multiple-document error = %q, want active path and document count failure", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadIgnoreFileParsesActiveFile(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(root, NucleiIgnoreFileName), []byte("tags: [weak]\nfiles: [blocked.yaml]\n"), 0o600); err != nil {
|
||||
t.Fatalf("write ignore file: %v", err)
|
||||
}
|
||||
ignore := ReadIgnoreFile()
|
||||
ignore, err := ReadIgnoreFile()
|
||||
if err != nil {
|
||||
t.Fatalf("read ignore file: %v", err)
|
||||
}
|
||||
if len(ignore.Tags) != 1 || ignore.Tags[0] != "weak" {
|
||||
t.Fatalf("ignore tags = %v, want [weak]", ignore.Tags)
|
||||
}
|
||||
@@ -36,6 +109,77 @@ func TestReadIgnoreFileAllowsMissingOrInvalidActiveFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteActiveIgnoreFileRejectsInvalidPayloads(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
cfg := DefaultConfig
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
valid := []byte("tags: [weak]\nfiles: [blocked.yaml]\n")
|
||||
if err := os.WriteFile(path, valid, 0o600); err != nil {
|
||||
t.Fatalf("seed ignore file: %v", err)
|
||||
}
|
||||
|
||||
cases := map[string][]byte{
|
||||
"proxy html": []byte("<!DOCTYPE HTML>\n<html>\n<head>\n<meta name=\"description\" content=\"Zscaler: blocked\">\n</head>\n</html>\n"),
|
||||
"unrelated yaml": []byte("error: access denied\nmessage: blocked by proxy\n"),
|
||||
"empty": []byte(" \n"),
|
||||
"empty mapping": []byte("{}\n"),
|
||||
"null document": []byte("null\n"),
|
||||
"template yaml": []byte("id: example\ninfo:\n name: example\n"),
|
||||
"additional document": []byte("tags: [weak]\n---\nfiles: [blocked.yaml]\n"),
|
||||
"malformed yaml": []byte("tags: ["),
|
||||
"oversized": bytes.Repeat([]byte("a"), maxIgnoreFileBytes+1),
|
||||
}
|
||||
|
||||
for name, payload := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := cfg.WriteActiveIgnoreFile(payload)
|
||||
if err == nil {
|
||||
t.Fatal("write invalid ignore file succeeded")
|
||||
}
|
||||
if message := err.Error(); !strings.Contains(message, strconv.Quote(path)) {
|
||||
t.Fatalf("invalid write error = %q, want active path", message)
|
||||
}
|
||||
got, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
t.Fatalf("read ignore file: %v", readErr)
|
||||
}
|
||||
if !bytes.Equal(got, valid) {
|
||||
t.Fatalf("invalid payload overwrote ignore file: got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteActiveIgnoreFileAcceptsKnownShapes(t *testing.T) {
|
||||
root := setActiveIgnoreFileRoot(t)
|
||||
cfg := DefaultConfig
|
||||
path := filepath.Join(root, NucleiIgnoreFileName)
|
||||
|
||||
cases := map[string][]byte{
|
||||
"tags and files": []byte("tags: [weak]\nfiles: [blocked.yaml]\n"),
|
||||
"tags only": []byte("tags: []\n"),
|
||||
"files only": []byte("files: [blocked.yaml]\n"),
|
||||
}
|
||||
|
||||
for name, payload := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := cfg.WriteActiveIgnoreFile(payload); err != nil {
|
||||
t.Fatalf("write ignore file: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read ignore file: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("ignore file = %q, want %q", got, payload)
|
||||
}
|
||||
if _, err := ReadIgnoreFile(); err != nil {
|
||||
t.Fatalf("read back written ignore file: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTemplatesDirDoesNotCopyLegacyIgnoreFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configDir := filepath.Join(dir, "config")
|
||||
|
||||
@@ -315,6 +315,7 @@ func (c *Config) WriteTemplatesIndex(index map[string]string) error {
|
||||
indexFile := c.GetTemplateIndexFilePath()
|
||||
|
||||
var buff bytes.Buffer
|
||||
|
||||
for k, v := range index {
|
||||
_, _ = buff.WriteString(k + "," + v + "\n")
|
||||
}
|
||||
@@ -329,7 +330,6 @@ func (c *Config) createConfigDirIfNotExists() error {
|
||||
return errkit.Newf("could not create nuclei config directory at %s: %v", c.configDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
func replaceTemplatesConfigFile(temporaryPath, configFilePath string) error {
|
||||
return os.Rename(temporaryPath, configFilePath)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var replaceFile = windows.NewLazySystemDLL("kernel32.dll").NewProc("ReplaceFileW")
|
||||
|
||||
func replaceTemplatesConfigFile(temporaryPath, configFilePath string) error {
|
||||
temporary, err := windows.UTF16PtrFromString(temporaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target, err := windows.UTF16PtrFromString(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(configFilePath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return windows.MoveFileEx(temporary, target, windows.MOVEFILE_WRITE_THROUGH)
|
||||
}
|
||||
|
||||
replaced, _, callErr := replaceFile.Call(
|
||||
uintptr(unsafe.Pointer(target)),
|
||||
uintptr(unsafe.Pointer(temporary)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
if replaced != 0 {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(callErr, windows.ERROR_FILE_NOT_FOUND) {
|
||||
return windows.MoveFileEx(temporary, target, windows.MOVEFILE_WRITE_THROUGH)
|
||||
}
|
||||
|
||||
return callErr
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplaceTemplatesConfigFile(t *testing.T) {
|
||||
for _, targetExists := range []bool{false, true} {
|
||||
name := "creates"
|
||||
if targetExists {
|
||||
name = "replaces"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
target := filepath.Join(directory, "config.json")
|
||||
if targetExists {
|
||||
if err := os.WriteFile(target, []byte("previous"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
temporary := filepath.Join(directory, "config.tmp")
|
||||
if err := os.WriteFile(temporary, []byte("current"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := replaceTemplatesConfigFile(temporary, target); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(contents) != "current" {
|
||||
t.Fatalf("expected replacement contents, got %q", contents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
@@ -134,7 +163,7 @@ func atomicWriteFile(path string, data []byte, mode os.FileMode) error {
|
||||
|
||||
temp = nil
|
||||
|
||||
if err := os.Rename(tempPath, path); err != nil {
|
||||
if err := replaceTemplatesConfigFile(tempPath, path); err != nil {
|
||||
return fmt.Errorf("replace %q: %w", path, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,3 +139,94 @@ func writeTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
||||
t.Fatalf("write test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReloadTemplateVersion(t *testing.T) {
|
||||
newConfig := func(t *testing.T, templatesDir string) *Config {
|
||||
t.Helper()
|
||||
cfg := &Config{stateDir: t.TempDir()}
|
||||
cfg.setTemplatesDir(templatesDir)
|
||||
return cfg
|
||||
}
|
||||
|
||||
t.Run("adopts the version another process installed", func(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
cfg := newConfig(t, templatesDir)
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(templatesDir)
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v2.0.0" {
|
||||
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ignores state of another templates directory", func(t *testing.T) {
|
||||
cfg := newConfig(t, t.TempDir())
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(t.TempDir())
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v1.0.0" {
|
||||
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps the loaded version without state", func(t *testing.T) {
|
||||
cfg := newConfig(t, t.TempDir())
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v1.0.0" {
|
||||
t.Fatalf("template version = %q, want v1.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("adopts the version written through the other name of a symlink", func(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
link := filepath.Join(t.TempDir(), "templates-link")
|
||||
if err := os.Symlink(templatesDir, link); err != nil {
|
||||
t.Skipf("symlinks are unavailable: %v", err)
|
||||
}
|
||||
|
||||
cfg := newConfig(t, link)
|
||||
other := &Config{stateDir: cfg.stateDir, TemplateVersion: "v2.0.0"}
|
||||
other.setTemplatesDir(templatesDir)
|
||||
if err := other.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v2.0.0" {
|
||||
t.Fatalf("template version = %q, want v2.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
|
||||
cfg.TemplateVersion = "v1.0.0"
|
||||
cfg.setTemplatesDir(templatesDir)
|
||||
writer := &Config{stateDir: cfg.stateDir, TemplateVersion: "v3.0.0"}
|
||||
writer.setTemplatesDir(link)
|
||||
if err := writer.WriteTemplatesConfig(); err != nil {
|
||||
t.Fatalf("write templates state through the link: %v", err)
|
||||
}
|
||||
if err := cfg.ReloadTemplateVersion(); err != nil {
|
||||
t.Fatalf("reload templates version: %v", err)
|
||||
}
|
||||
if cfg.TemplateVersion != "v3.0.0" {
|
||||
t.Fatalf("template version = %q, want v3.0.0", cfg.TemplateVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CanonicalTemplatesPath is the identity of a templates directory. A symlink
|
||||
// and its target are the same directory, including while the target does not
|
||||
// exist yet, so a lock and a reloaded version follow either name.
|
||||
func CanonicalTemplatesPath(path string) string {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return ""
|
||||
}
|
||||
return resolveTemplatesPath(path, map[string]struct{}{})
|
||||
}
|
||||
|
||||
func resolveTemplatesPath(path string, seen map[string]struct{}) string {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
abs = filepath.Clean(path)
|
||||
}
|
||||
if _, ok := seen[abs]; ok {
|
||||
return abs
|
||||
}
|
||||
seen[abs] = struct{}{}
|
||||
|
||||
dir := abs
|
||||
var missing []string
|
||||
for {
|
||||
info, err := os.Lstat(dir)
|
||||
if err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
return filepath.Join(append([]string{resolved}, missing...)...)
|
||||
}
|
||||
target, readErr := os.Readlink(dir)
|
||||
if readErr != nil {
|
||||
return filepath.Join(append([]string{dir}, missing...)...)
|
||||
}
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(filepath.Dir(dir), target)
|
||||
}
|
||||
resolvedTarget := resolveTemplatesPath(target, seen)
|
||||
return filepath.Join(append([]string{resolvedTarget}, missing...)...)
|
||||
}
|
||||
resolved := dir
|
||||
if r, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
resolved = r
|
||||
}
|
||||
return filepath.Join(append([]string{resolved}, missing...)...)
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return filepath.Join(append([]string{dir}, missing...)...)
|
||||
}
|
||||
missing = append([]string{filepath.Base(dir)}, missing...)
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const (
|
||||
|
||||
// IndexVersion is the schema version for cache invalidation on breaking
|
||||
// changes.
|
||||
IndexVersion = 2
|
||||
IndexVersion = 3
|
||||
|
||||
// DefaultMaxSize is the default maximum number of templates to cache.
|
||||
DefaultMaxSize = 50000
|
||||
@@ -70,6 +70,7 @@ func NewIndex(cacheDir string) (*Index, error) {
|
||||
weight += len(value.Name)
|
||||
weight += len(value.Severity)
|
||||
weight += len(value.ProtocolType)
|
||||
weight += len(value.Product)
|
||||
weight += len(value.TemplateVerifier)
|
||||
weight += len(value.VerifierFingerprint)
|
||||
weight += len(value.ContentDigest)
|
||||
|
||||
@@ -905,6 +905,7 @@ func TestNewMetadataFromTemplate(t *testing.T) {
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Low,
|
||||
},
|
||||
Metadata: map[string]any{"product": " Tomcat "},
|
||||
},
|
||||
Verified: true,
|
||||
TemplateVerifier: "verifier",
|
||||
@@ -922,4 +923,5 @@ func TestNewMetadataFromTemplate(t *testing.T) {
|
||||
require.Equal(t, tmpl.Type().String(), metadata.ProtocolType)
|
||||
require.Equal(t, tmpl.Verified, metadata.Verified)
|
||||
require.Equal(t, tmpl.TemplateVerifier, metadata.TemplateVerifier)
|
||||
require.Equal(t, "tomcat", metadata.Product)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package index
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
@@ -48,6 +49,9 @@ type Metadata struct {
|
||||
// ProtocolType is the primary protocol type of the template.
|
||||
ProtocolType string `gob:"protocol_type"`
|
||||
|
||||
// Product is the lowercased info.metadata.product of the template.
|
||||
Product string `gob:"product,omitempty"`
|
||||
|
||||
// Verified indicates whether the template is verified.
|
||||
Verified bool `gob:"verified"`
|
||||
|
||||
@@ -114,6 +118,7 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
|
||||
Severity: tpl.Info.SeverityHolder.Severity.String(),
|
||||
|
||||
ProtocolType: tpl.Type().String(),
|
||||
Product: templateProduct(tpl),
|
||||
|
||||
Verified: tpl.Verified,
|
||||
TemplateVerifier: tpl.TemplateVerifier,
|
||||
@@ -122,6 +127,11 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
func templateProduct(tpl *templates.Template) string {
|
||||
product, _ := tpl.Info.Metadata["product"].(string)
|
||||
return strings.ToLower(strings.TrimSpace(product))
|
||||
}
|
||||
|
||||
// IsValid checks if the cached metadata is still valid by comparing the file
|
||||
// modification time.
|
||||
func (m *Metadata) IsValid() bool {
|
||||
|
||||
@@ -76,6 +76,10 @@ type Config struct {
|
||||
// MetadataIndex is an optional shared index borrowed by the store. The
|
||||
// caller remains responsible for persisting it.
|
||||
MetadataIndex *index.Index
|
||||
|
||||
// TargetFilter narrows loading to the templates per-target profiles
|
||||
// select for at least one target; nil loads every filtered template.
|
||||
TargetFilter index.FilterFunc
|
||||
}
|
||||
|
||||
// Store is a storage for loaded nuclei templates
|
||||
@@ -387,6 +391,11 @@ func (store *Store) buildIndexFilter() *index.Filter {
|
||||
}
|
||||
}
|
||||
|
||||
// selectedByTargets reports whether some target's profile selects the template.
|
||||
func (store *Store) selectedByTargets(metadata *index.Metadata) bool {
|
||||
return store.config.TargetFilter == nil || store.config.TargetFilter(metadata)
|
||||
}
|
||||
|
||||
func (store *Store) loadTemplatesIndex() *index.Index {
|
||||
var metadataIdx *index.Index
|
||||
|
||||
@@ -823,7 +832,14 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
|
||||
// tags, so it must be matched against the ignore-file tags specifically:
|
||||
// otherwise user-requested -exclude-tags drops would be mislabeled as
|
||||
// .nuclei-ignore exclusions.
|
||||
ignoreFileTags := config.ReadIgnoreFile().Tags
|
||||
ignoreFile, err := config.ReadIgnoreFile()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
store.logger.Warning().Msgf("Could not read active .nuclei-ignore file: %s; continuing without ignore exclusions", err)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ignoreFileTags := ignoreFile.Tags
|
||||
|
||||
noteExcludedByTag := func(templatePath string, metadata *index.Metadata) {
|
||||
if len(ignoreFileTags) == 0 || !slices.ContainsFunc(ignoreFileTags, metadata.HasTag) {
|
||||
return
|
||||
@@ -875,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
|
||||
}
|
||||
@@ -909,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 {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -60,6 +61,37 @@ func TestLoadTemplates(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadTemplatesWithTagsRejectsCorruptIgnoreFileInValidationMode(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
ignoreFilePath := filepath.Join(root, config.NucleiIgnoreFileName)
|
||||
require.NoError(t, os.WriteFile(ignoreFilePath, []byte("tags: ["), 0o600))
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Validate = true
|
||||
metadataIndex, err := metadataindex.NewIndex(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
store, err := New(&Config{
|
||||
Catalog: disk.NewCatalog(""),
|
||||
ExecutorOptions: &protocols.ExecutorOptions{
|
||||
Options: options,
|
||||
Parser: templates.NewParser(),
|
||||
},
|
||||
Logger: options.Logger,
|
||||
MetadataIndex: metadataIndex,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.LoadTemplatesWithTags(nil, nil)
|
||||
require.ErrorContains(t, err, "error parsing")
|
||||
require.ErrorContains(t, err, strconv.Quote(ignoreFilePath))
|
||||
}
|
||||
|
||||
func TestNewUsesConfiguredMetadataIndex(t *testing.T) {
|
||||
indexDir := t.TempDir()
|
||||
metadataIndex, err := metadataindex.NewIndex(indexDir)
|
||||
|
||||
+60
-6
@@ -1,12 +1,38 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// TemplateExecutionState identifies a template execution lifecycle transition.
|
||||
type TemplateExecutionState uint8
|
||||
|
||||
const (
|
||||
// TemplateExecutionStarted is emitted immediately before a template starts on a target.
|
||||
TemplateExecutionStarted TemplateExecutionState = iota + 1
|
||||
// TemplateExecutionFinished is emitted after that template execution returns.
|
||||
TemplateExecutionFinished
|
||||
)
|
||||
|
||||
// TemplateExecutionEvent identifies one template execution on one target.
|
||||
type TemplateExecutionEvent struct {
|
||||
TemplateID string
|
||||
TemplatePath string
|
||||
Target string
|
||||
State TemplateExecutionState
|
||||
ContextErr error
|
||||
}
|
||||
|
||||
// TemplateExecutionCallback observes template execution lifecycle transitions.
|
||||
// Nuclei can invoke the callback concurrently; implementations must be concurrency-safe.
|
||||
type TemplateExecutionCallback func(TemplateExecutionEvent)
|
||||
|
||||
// Engine is an executer for running Nuclei Templates/Workflows.
|
||||
//
|
||||
// The engine contains multiple thread pools which allow using different
|
||||
@@ -16,11 +42,16 @@ import (
|
||||
// templates to leading to the final execution by the work pool, it is
|
||||
// handled by the engine.
|
||||
type Engine struct {
|
||||
workPool *WorkPool
|
||||
options *types.Options
|
||||
executerOpts *protocols.ExecutorOptions
|
||||
Callback func(*output.ResultEvent) // Executed on results
|
||||
Logger *gologger.Logger
|
||||
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
|
||||
templateExecutionCallback TemplateExecutionCallback
|
||||
Logger *gologger.Logger
|
||||
}
|
||||
|
||||
// New returns a new Engine instance
|
||||
@@ -36,7 +67,7 @@ func New(options *types.Options) *Engine {
|
||||
func (e *Engine) GetWorkPoolConfig() WorkPoolConfig {
|
||||
config := WorkPoolConfig{
|
||||
InputConcurrency: e.options.BulkSize,
|
||||
TypeConcurrency: e.options.TemplateThreads,
|
||||
TypeConcurrency: e.options.CurrentTemplateThreads(),
|
||||
HeadlessInputConcurrency: e.options.HeadlessBulkSize,
|
||||
HeadlessTypeConcurrency: e.options.HeadlessTemplateThreads,
|
||||
}
|
||||
@@ -54,6 +85,29 @@ func (e *Engine) SetExecuterOptions(options *protocols.ExecutorOptions) {
|
||||
e.executerOpts = options
|
||||
}
|
||||
|
||||
// SetTemplateExecutionCallback registers a callback for template execution lifecycle events.
|
||||
func (e *Engine) SetTemplateExecutionCallback(callback TemplateExecutionCallback) {
|
||||
e.templateExecutionCallback = callback
|
||||
}
|
||||
|
||||
func (e *Engine) templateExecutionStarted(template *templates.Template, target string) func(error) {
|
||||
if e.templateExecutionCallback == nil {
|
||||
return func(error) {}
|
||||
}
|
||||
event := TemplateExecutionEvent{
|
||||
TemplateID: template.ID,
|
||||
TemplatePath: template.Path,
|
||||
Target: target,
|
||||
State: TemplateExecutionStarted,
|
||||
}
|
||||
e.templateExecutionCallback(event)
|
||||
return func(contextErr error) {
|
||||
event.State = TemplateExecutionFinished
|
||||
event.ContextErr = contextErr
|
||||
e.templateExecutionCallback(event)
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuterOptions returns protocols.ExecutorOptions for nuclei engine.
|
||||
func (e *Engine) ExecuterOptions() *protocols.ExecutorOptions {
|
||||
return e.executerOpts
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -67,7 +68,7 @@ func (e *Engine) ExecuteScanWithOpts(ctx context.Context, templatesList []*templ
|
||||
// workflow requests are not counted as they can be conditional
|
||||
// templateList count is user requested templates count (before clustering)
|
||||
// totalReqAfterClustering is total requests count after clustering
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), int64(totalReqAfterClustering))
|
||||
e.executerOpts.Progress.Init(target.Count(), len(templatesList), e.expectedRequests(finalTemplates, target, totalReqAfterClustering))
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(e.options.ScanStrategy, scanstrategy.Auto.String(), "") {
|
||||
@@ -132,14 +133,26 @@ func (e *Engine) executeTemplateSpray(ctx context.Context, templatesList []*temp
|
||||
wg = wp.Default
|
||||
}
|
||||
|
||||
wg.Add()
|
||||
go func(tpl *templates.Template) {
|
||||
if err := wg.AddWithContext(ctx); err != nil {
|
||||
return results
|
||||
}
|
||||
usesSharedTemplateBudget := templateType != types.HeadlessProtocol
|
||||
if usesSharedTemplateBudget {
|
||||
if err := e.options.AcquireTemplateThread(ctx); err != nil {
|
||||
wg.Done()
|
||||
return results
|
||||
}
|
||||
}
|
||||
go func(tpl *templates.Template, sharedBudget bool) {
|
||||
defer wg.Done()
|
||||
if sharedBudget {
|
||||
defer e.options.ReleaseTemplateThread()
|
||||
}
|
||||
// All other request types are executed here
|
||||
// Note: executeTemplateWithTargets creates goroutines and blocks
|
||||
// given template is executed on all targets
|
||||
e.executeTemplateWithTargets(ctx, tpl, target, results)
|
||||
}(template)
|
||||
}(template, usesSharedTemplateBudget)
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -167,8 +180,33 @@ func (e *Engine) executeHostSpray(ctx context.Context, templatesList []*template
|
||||
return results
|
||||
}
|
||||
|
||||
// expectedRequests counts only the template and target pairs that per-target
|
||||
// profiles select, so progress reflects the requests that will be sent.
|
||||
func (e *Engine) expectedRequests(templatesList []*templates.Template, target provider.InputProvider, unscoped int) int64 {
|
||||
scope := e.executerOpts.TargetScope
|
||||
if scope == nil {
|
||||
return int64(unscoped)
|
||||
}
|
||||
targets := make(map[protocols.TemplateSelection]int64)
|
||||
target.Iterate(func(value *contextargs.MetaInput) bool {
|
||||
targets[scope.For(value)]++
|
||||
return true
|
||||
})
|
||||
var total int64
|
||||
for selection, count := range targets {
|
||||
total += count * int64(getSelectedRequestCount(templatesList, selection))
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// returns total requests count
|
||||
func getRequestCount(templates []*templates.Template) int {
|
||||
return getSelectedRequestCount(templates, nil)
|
||||
}
|
||||
|
||||
// getSelectedRequestCount returns the requests of the templates selection
|
||||
// selects; a nil selection selects every template.
|
||||
func getSelectedRequestCount(templates []*templates.Template, selection protocols.TemplateSelection) int {
|
||||
count := 0
|
||||
for _, template := range templates {
|
||||
// ignore requests in workflows as total requests in workflow
|
||||
@@ -176,6 +214,9 @@ func getRequestCount(templates []*templates.Template) int {
|
||||
if len(template.Workflows) > 0 {
|
||||
continue
|
||||
}
|
||||
if selection != nil && !template.SelectedBy(selection) {
|
||||
continue
|
||||
}
|
||||
count += template.TotalRequests
|
||||
}
|
||||
return count
|
||||
|
||||
+122
-10
@@ -6,6 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/normalize"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
@@ -21,9 +22,20 @@ import (
|
||||
// executeAllSelfContained executes all self contained templates that do not use `target`
|
||||
func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*templates.Template, results *atomic.Bool, sg *sync.WaitGroup) {
|
||||
for _, v := range alltemplates {
|
||||
usesSharedTemplateBudget := v.Type() != types.HeadlessProtocol
|
||||
if usesSharedTemplateBudget {
|
||||
if err := e.options.AcquireTemplateThread(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
sg.Add(1)
|
||||
go func(template *templates.Template) {
|
||||
go func(template *templates.Template, sharedBudget bool) {
|
||||
defer sg.Done()
|
||||
if sharedBudget {
|
||||
defer e.options.ReleaseTemplateThread()
|
||||
}
|
||||
finished := e.templateExecutionStarted(template, "")
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
var err error
|
||||
var match bool
|
||||
ctx := scan.NewScanContext(ctx, contextargs.New(ctx))
|
||||
@@ -42,7 +54,7 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
|
||||
e.options.Logger.Warning().Msgf("[%s] Could not execute step (self-contained): %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), err)
|
||||
}
|
||||
results.CompareAndSwap(false, match)
|
||||
}(v)
|
||||
}(v, usesSharedTemplateBudget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,12 +155,18 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
skip = false
|
||||
}
|
||||
|
||||
// out of scope pairs keep their index so resume positions stay stable
|
||||
inScope := e.inScope(template, scannedValue)
|
||||
if !inScope {
|
||||
skip = true
|
||||
}
|
||||
|
||||
currentInfo.Lock()
|
||||
currentInfo.InFlight[index] = struct{}{}
|
||||
currentInfo.Unlock()
|
||||
|
||||
// Skip if the host has had errors
|
||||
if e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
if inScope && e.executerOpts.HostErrorsCache != nil && e.executerOpts.HostErrorsCache.Check(e.executerOpts.ProtocolType.String(), contextargs.NewWithMetaInput(ctx, scannedValue)) {
|
||||
skipEvent := &output.ResultEvent{
|
||||
TemplateID: template.ID,
|
||||
TemplatePath: template.Path,
|
||||
@@ -199,6 +217,10 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
default:
|
||||
}
|
||||
|
||||
if !e.inScope(tpl, target) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check whether the target has already been marked as permanently
|
||||
// unresponsive by HostErrorsCache before spawning another goroutine.
|
||||
if e.executerOpts.HostErrorsCache != nil &&
|
||||
@@ -230,21 +252,54 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
} else {
|
||||
sg = wp.Default
|
||||
}
|
||||
sg.Add()
|
||||
go func(template *templates.Template, value *contextargs.MetaInput, wg *syncutil.AdaptiveWaitGroup) {
|
||||
if err := sg.AddWithContext(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
usesSharedTemplateBudget := tpl.Type() != types.HeadlessProtocol
|
||||
if usesSharedTemplateBudget {
|
||||
if err := e.options.AcquireTemplateThread(ctx); err != nil {
|
||||
sg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
go func(template *templates.Template, value *contextargs.MetaInput, wg *syncutil.AdaptiveWaitGroup, sharedBudget bool) {
|
||||
defer wg.Done()
|
||||
if sharedBudget {
|
||||
defer e.options.ReleaseTemplateThread()
|
||||
}
|
||||
|
||||
match, err := e.executeTemplateOnInput(ctx, template, value)
|
||||
if err != nil {
|
||||
e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", template.ID, value.Input, err)
|
||||
}
|
||||
results.CompareAndSwap(false, match)
|
||||
}(tpl, target, sg)
|
||||
}(tpl, target, sg, usesSharedTemplateBudget)
|
||||
}
|
||||
}
|
||||
|
||||
// inScope reports whether per-target profiles select template for input.
|
||||
func (e *Engine) inScope(template *templates.Template, input *contextargs.MetaInput) bool {
|
||||
if e.executerOpts.TargetScope == nil {
|
||||
return true
|
||||
}
|
||||
selection := e.executerOpts.TargetScope.For(input)
|
||||
return selection == nil || template.SelectedBy(selection)
|
||||
}
|
||||
|
||||
// executeTemplateOnInput performs template execution for a single input and returns match status and error
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (bool, error) {
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (matched bool, err error) {
|
||||
// A template fixed to the target's origin makes the same requests for every
|
||||
// target that shares one, which a crawled list produces by the hundred.
|
||||
// Running it once per origin sends exactly the same traffic, minus the
|
||||
// repeats. The reservation is kept only when execution succeeds, so a
|
||||
// failed attempt leaves the origin free for a later target.
|
||||
finish, run := e.claimOriginScoped(template, value)
|
||||
if !run {
|
||||
return false, nil
|
||||
}
|
||||
defer func() { finish(err == nil) }()
|
||||
finished := e.templateExecutionStarted(template, value.Input)
|
||||
defer func() { finished(ctx.Err()) }()
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
ctxArgs.MetaInput = value
|
||||
scanCtx := scan.NewScanContext(ctx, ctxArgs)
|
||||
@@ -254,9 +309,9 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
results, err := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
results, execErr := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if execErr != nil {
|
||||
return false, execErr
|
||||
}
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
@@ -266,3 +321,60 @@ func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates
|
||||
return template.Executer.Execute(scanCtx)
|
||||
}
|
||||
}
|
||||
|
||||
// originAttempt is one origin-scoped execution. done is set only after a
|
||||
// successful run. While inflight is set, other callers wait, then retry if
|
||||
// that attempt failed.
|
||||
type originAttempt struct {
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
inflight bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newOriginAttempt() *originAttempt {
|
||||
attempt := &originAttempt{}
|
||||
attempt.cond = sync.NewCond(&attempt.mu)
|
||||
return attempt
|
||||
}
|
||||
|
||||
// claimOriginScoped reports whether this caller should execute an origin-scoped
|
||||
// template. finish(true) records a successful run. finish(false) releases the
|
||||
// claim so a later target, including one that waited, can try again. Templates
|
||||
// whose requests depend on the target path always run. CustomIP is part of the
|
||||
// key so each address selected for one URL is tested on its own.
|
||||
func (e *Engine) claimOriginScoped(template *templates.Template, value *contextargs.MetaInput) (finish func(bool), run bool) {
|
||||
noop := func(bool) {}
|
||||
if template == nil || value == nil || !template.IsOriginScoped() {
|
||||
return noop, true
|
||||
}
|
||||
origin := normalize.Origin(value.Input)
|
||||
if origin == "" {
|
||||
return noop, true
|
||||
}
|
||||
key := template.ID + "\x00" + origin + "\x00" + value.CustomIP
|
||||
actual, _ := e.originScoped.LoadOrStore(key, newOriginAttempt())
|
||||
slot := actual.(*originAttempt)
|
||||
|
||||
slot.mu.Lock()
|
||||
for {
|
||||
if slot.done {
|
||||
slot.mu.Unlock()
|
||||
return noop, false
|
||||
}
|
||||
if !slot.inflight {
|
||||
slot.inflight = true
|
||||
slot.mu.Unlock()
|
||||
return func(success bool) {
|
||||
slot.mu.Lock()
|
||||
slot.inflight = false
|
||||
if success {
|
||||
slot.done = true
|
||||
}
|
||||
slot.cond.Broadcast()
|
||||
slot.mu.Unlock()
|
||||
}, true
|
||||
}
|
||||
slot.cond.Wait()
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -11,6 +14,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
httpprotocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
@@ -95,6 +99,332 @@ func Test_executeTemplateOnInput_CallbackErrorPropagates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTemplateOnInputReportsLifecycle(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
var events []TemplateExecutionEvent
|
||||
e.SetTemplateExecutionCallback(func(event TemplateExecutionEvent) {
|
||||
events = append(events, event)
|
||||
})
|
||||
|
||||
tpl := &templates.Template{ID: "template-id", Path: "http/example.yaml"}
|
||||
tpl.Executer = &fakeExecuter{withResults: false}
|
||||
|
||||
_, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "https://example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want := []TemplateExecutionEvent{
|
||||
{TemplateID: "template-id", TemplatePath: "http/example.yaml", Target: "https://example.com", State: TemplateExecutionStarted},
|
||||
{TemplateID: "template-id", TemplatePath: "http/example.yaml", Target: "https://example.com", State: TemplateExecutionFinished},
|
||||
}
|
||||
if !reflect.DeepEqual(events, want) {
|
||||
t.Fatalf("unexpected lifecycle events: got %#v want %#v", events, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTemplateOnInputReportsCancellationOnFinish(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
var finished TemplateExecutionEvent
|
||||
e.SetTemplateExecutionCallback(func(event TemplateExecutionEvent) {
|
||||
if event.State == TemplateExecutionFinished {
|
||||
finished = event
|
||||
}
|
||||
})
|
||||
tpl := &templates.Template{ID: "interrupted"}
|
||||
tpl.Executer = &fakeExecuter{withResults: false}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, _ = e.executeTemplateOnInput(ctx, tpl, &contextargs.MetaInput{Input: "target"})
|
||||
if finished.ContextErr != context.Canceled {
|
||||
t.Fatalf("got finish context error %v, want context canceled", finished.ContextErr)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -127,6 +457,185 @@ func (s *slowExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
type adaptiveConcurrencyExecuter struct {
|
||||
delay time.Duration
|
||||
started atomic.Int32
|
||||
completed atomic.Int32
|
||||
inFlight atomic.Int32
|
||||
maxInFlight atomic.Int32
|
||||
onStart func(int32)
|
||||
}
|
||||
|
||||
func (s *adaptiveConcurrencyExecuter) Compile() error { return nil }
|
||||
func (s *adaptiveConcurrencyExecuter) Requests() int { return 1 }
|
||||
func (s *adaptiveConcurrencyExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
started := s.started.Add(1)
|
||||
if s.onStart != nil {
|
||||
s.onStart(started)
|
||||
}
|
||||
inFlight := s.inFlight.Add(1)
|
||||
for {
|
||||
maximum := s.maxInFlight.Load()
|
||||
if inFlight <= maximum || s.maxInFlight.CompareAndSwap(maximum, inFlight) {
|
||||
break
|
||||
}
|
||||
}
|
||||
defer s.inFlight.Add(-1)
|
||||
|
||||
select {
|
||||
case <-ctx.Context().Done():
|
||||
return false, ctx.Context().Err()
|
||||
case <-time.After(s.delay):
|
||||
s.completed.Add(1)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
func (s *adaptiveConcurrencyExecuter) ExecuteWithResults(*scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestExecuteTemplateSprayRefreshesDynamicTemplateConcurrency(t *testing.T) {
|
||||
const (
|
||||
templateCount = 8
|
||||
initialConcurrency = 2
|
||||
finalConcurrency = 8
|
||||
)
|
||||
|
||||
var currentConcurrency atomic.Int32
|
||||
currentConcurrency.Store(initialConcurrency)
|
||||
var expandOnce sync.Once
|
||||
executer := &adaptiveConcurrencyExecuter{
|
||||
delay: 100 * time.Millisecond,
|
||||
onStart: func(started int32) {
|
||||
if started == initialConcurrency {
|
||||
expandOnce.Do(func() { currentConcurrency.Store(finalConcurrency) })
|
||||
}
|
||||
},
|
||||
}
|
||||
options := &types.Options{
|
||||
BulkSize: 1,
|
||||
TemplateThreads: initialConcurrency,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
}
|
||||
options.SetTemplateThreadsProvider(func() int { return int(currentConcurrency.Load()) })
|
||||
if got := options.Copy().CurrentTemplateThreads(); got != initialConcurrency {
|
||||
t.Fatalf("copied options returned concurrency %d, expected %d", got, initialConcurrency)
|
||||
}
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.NetworkProtocol,
|
||||
})
|
||||
templatesList := make([]*templates.Template, 0, templateCount)
|
||||
for index := range templateCount {
|
||||
templatesList = append(templatesList, &templates.Template{ID: fmt.Sprintf("adaptive-%d", index), Executer: executer})
|
||||
}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "slow-target"}}}
|
||||
|
||||
started := time.Now()
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targets, true)
|
||||
elapsed := time.Since(started)
|
||||
|
||||
if got := executer.completed.Load(); got != templateCount {
|
||||
t.Fatalf("completed %d templates, expected %d", got, templateCount)
|
||||
}
|
||||
if got := executer.maxInFlight.Load(); got <= initialConcurrency {
|
||||
t.Fatalf("maximum concurrency stayed at %d; expected it to expand beyond %d", got, initialConcurrency)
|
||||
}
|
||||
if elapsed >= 350*time.Millisecond {
|
||||
t.Fatalf("dynamic execution took %s; fixed concurrency would take about 400ms", elapsed)
|
||||
}
|
||||
t.Logf("dynamic execution completed all %d templates in %s with peak concurrency %d", templateCount, elapsed, executer.maxInFlight.Load())
|
||||
}
|
||||
|
||||
func TestExecuteTemplateSprayHonorsSharedConcurrencyLimiter(t *testing.T) {
|
||||
const (
|
||||
templateCount = 8
|
||||
sharedBudget = 2
|
||||
)
|
||||
|
||||
tokens := make(chan struct{}, sharedBudget)
|
||||
executer := &adaptiveConcurrencyExecuter{delay: 50 * time.Millisecond}
|
||||
options := &types.Options{
|
||||
BulkSize: 1,
|
||||
TemplateThreads: templateCount,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
}
|
||||
options.SetTemplateThreadsLimiter(func(ctx context.Context) error {
|
||||
select {
|
||||
case tokens <- struct{}{}:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}, func() {
|
||||
<-tokens
|
||||
})
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.NetworkProtocol,
|
||||
})
|
||||
templatesList := make([]*templates.Template, 0, templateCount)
|
||||
for index := range templateCount {
|
||||
templatesList = append(templatesList, &templates.Template{ID: fmt.Sprintf("shared-limit-%d", index), Executer: executer})
|
||||
}
|
||||
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList,
|
||||
&fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "slow-target"}}}, true)
|
||||
|
||||
if got := executer.completed.Load(); got != templateCount {
|
||||
t.Fatalf("completed %d templates, expected %d", got, templateCount)
|
||||
}
|
||||
if got := executer.maxInFlight.Load(); got > sharedBudget {
|
||||
t.Fatalf("maximum concurrency was %d, shared budget is %d", got, sharedBudget)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteAllSelfContainedHonorsSharedConcurrencyLimiter(t *testing.T) {
|
||||
const (
|
||||
templateCount = 8
|
||||
sharedBudget = 2
|
||||
)
|
||||
|
||||
tokens := make(chan struct{}, sharedBudget)
|
||||
executer := &adaptiveConcurrencyExecuter{delay: 20 * time.Millisecond}
|
||||
options := &types.Options{}
|
||||
options.SetTemplateThreadsLimiter(func(ctx context.Context) error {
|
||||
select {
|
||||
case tokens <- struct{}{}:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}, func() {
|
||||
<-tokens
|
||||
})
|
||||
engine := New(options)
|
||||
templatesList := make([]*templates.Template, 0, templateCount)
|
||||
for index := range templateCount {
|
||||
templatesList = append(templatesList, &templates.Template{ID: fmt.Sprintf("self-contained-%d", index), Executer: executer})
|
||||
}
|
||||
|
||||
var results atomic.Bool
|
||||
var waitGroup sync.WaitGroup
|
||||
engine.executeAllSelfContained(context.Background(), templatesList, &results, &waitGroup)
|
||||
waitGroup.Wait()
|
||||
|
||||
if got := executer.completed.Load(); got != templateCount {
|
||||
t.Fatalf("completed %d templates, expected %d", got, templateCount)
|
||||
}
|
||||
if got := executer.maxInFlight.Load(); got > sharedBudget {
|
||||
t.Fatalf("maximum concurrency was %d, shared budget is %d", got, sharedBudget)
|
||||
}
|
||||
}
|
||||
func (s *slowExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types/scanstrategy"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// allowList selects the templates it lists.
|
||||
type allowList struct{ paths []string }
|
||||
|
||||
func (a *allowList) Allows(templatePath string) bool { return slices.Contains(a.paths, templatePath) }
|
||||
|
||||
// scopeByInput maps inputs to selections; unlisted inputs are unrestricted.
|
||||
type scopeByInput map[string]*allowList
|
||||
|
||||
func (s scopeByInput) For(input *contextargs.MetaInput) protocols.TemplateSelection {
|
||||
if selection, ok := s[input.Input]; ok {
|
||||
return selection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordingExecuter records the inputs it runs on.
|
||||
type recordingExecuter struct {
|
||||
path string
|
||||
mu *sync.Mutex
|
||||
runs *[]string
|
||||
}
|
||||
|
||||
func (r *recordingExecuter) Compile() error { return nil }
|
||||
func (r *recordingExecuter) Requests() int { return 1 }
|
||||
func (r *recordingExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
*r.runs = append(*r.runs, r.path+"@"+ctx.Input.MetaInput.Input)
|
||||
return false, nil
|
||||
}
|
||||
func (r *recordingExecuter) ExecuteWithResults(*scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// progressRecorder captures the expected request count; the engine calls no
|
||||
// other progress method on this path.
|
||||
type progressRecorder struct {
|
||||
progress.Progress
|
||||
requests int64
|
||||
}
|
||||
|
||||
func (p *progressRecorder) Init(_ int64, _ int, requests int64) { p.requests = requests }
|
||||
|
||||
func TestExecuteScanRunsOnlyScopedPairs(t *testing.T) {
|
||||
for _, strategy := range []scanstrategy.ScanStrategy{scanstrategy.TemplateSpray, scanstrategy.HostSpray} {
|
||||
t.Run(strategy.String(), func(t *testing.T) {
|
||||
options := &types.Options{
|
||||
BulkSize: 2,
|
||||
TemplateThreads: 2,
|
||||
HeadlessBulkSize: 1,
|
||||
HeadlessTemplateThreads: 1,
|
||||
ScanStrategy: strategy.String(),
|
||||
}
|
||||
progress := &progressRecorder{}
|
||||
engine := New(options)
|
||||
engine.SetExecuterOptions(&protocols.ExecutorOptions{
|
||||
Logger: engine.Logger,
|
||||
Options: options,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
ProtocolType: tmpltypes.HTTPProtocol,
|
||||
Progress: progress,
|
||||
TargetScope: scopeByInput{
|
||||
"php": {paths: []string{"thinkphp.yaml"}},
|
||||
"tomcat": {paths: []string{"tomcat.yaml", "generic.yaml"}},
|
||||
"none": {},
|
||||
},
|
||||
})
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
runs []string
|
||||
)
|
||||
var templatesList []*templates.Template
|
||||
for _, path := range []string{"thinkphp.yaml", "tomcat.yaml", "generic.yaml"} {
|
||||
templatesList = append(templatesList, &templates.Template{
|
||||
ID: path,
|
||||
Path: path,
|
||||
TotalRequests: 1,
|
||||
Executer: &recordingExecuter{path: path, mu: &mu, runs: &runs},
|
||||
})
|
||||
}
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{
|
||||
{Input: "php"}, {Input: "tomcat"}, {Input: "none"}, {Input: "plain"},
|
||||
}}
|
||||
|
||||
engine.ExecuteScanWithOpts(context.Background(), templatesList, targets, true)
|
||||
|
||||
require.ElementsMatch(t, []string{
|
||||
"thinkphp.yaml@php",
|
||||
"tomcat.yaml@tomcat", "generic.yaml@tomcat",
|
||||
"thinkphp.yaml@plain", "tomcat.yaml@plain", "generic.yaml@plain",
|
||||
}, runs)
|
||||
require.EqualValues(t, len(runs), progress.requests, "progress counts only scoped pairs")
|
||||
})
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -2,13 +2,12 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"context"
|
||||
httpclient "net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/google/go-github/v30/github"
|
||||
"github.com/google/go-github/v92/github"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
@@ -89,7 +88,10 @@ func (customTemplate *customTemplateGitHubRepo) logPullError(err error) {
|
||||
// NewGitHubProviders returns new instance of GitHub providers for downloading custom templates
|
||||
func NewGitHubProviders(options *types.Options) ([]*customTemplateGitHubRepo, error) {
|
||||
providers := []*customTemplateGitHubRepo{}
|
||||
gitHubClient := getGHClientIncognito()
|
||||
gitHubClient, err := github.NewClient()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create github client")
|
||||
}
|
||||
|
||||
if options.GitHubTemplateDisableDownload {
|
||||
return providers, nil
|
||||
@@ -150,9 +152,16 @@ getRepo:
|
||||
repo, _, err := gitHubClient.Repositories.Get(context.Background(), repoOwner, repoName)
|
||||
if err != nil {
|
||||
// retry with authentication
|
||||
if gitHubClient = getGHClientWithToken(githubToken); gitHubClient != nil && !retried {
|
||||
retried = true
|
||||
goto getRepo
|
||||
if !retried {
|
||||
authClient, authErr := getGHClientWithToken(githubToken)
|
||||
if authErr != nil {
|
||||
return nil, errors.Wrap(authErr, "could not create authenticated github client")
|
||||
}
|
||||
if authClient != nil {
|
||||
gitHubClient = authClient
|
||||
retried = true
|
||||
goto getRepo
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -233,20 +242,15 @@ func getAuth(username, password string) *http.BasicAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGHClientWithToken(token string) *github.Client {
|
||||
func getGHClientWithToken(token string) (*github.Client, error) {
|
||||
if token != "" {
|
||||
ctx := context.Background()
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
oauthClient := oauth2.NewClient(ctx, ts)
|
||||
return github.NewClient(oauthClient)
|
||||
return github.NewClient(github.WithHTTPClient(oauthClient))
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getGHClientIncognito() *github.Client {
|
||||
var tc *httpclient.Client
|
||||
return github.NewClient(tc)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -55,18 +55,28 @@ func (b *Body) Parse(req *retryablehttp.Request) (bool, error) {
|
||||
|
||||
b.value = NewValue(dataStr)
|
||||
tmp := b.value.Parsed()
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(contentType, "application/json") && tmp.IsNIL():
|
||||
case strings.Contains(contentType, "application/json"):
|
||||
// GraphQL-over-HTTP uses application/json; prefer the GraphQL decoder
|
||||
// so variables / inline args become fuzz points instead of raw JSON.
|
||||
if graphql := dataformat.Get(dataformat.GraphqlDataFormat); graphql != nil && graphql.IsType(dataStr) {
|
||||
return b.parseBody(dataformat.GraphqlDataFormat, req)
|
||||
}
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
return b.parseBody(dataformat.JSONDataFormat, req)
|
||||
case strings.Contains(contentType, "application/xml") && tmp.IsNIL():
|
||||
return b.parseBody(dataformat.XMLDataFormat, req)
|
||||
case strings.Contains(contentType, "multipart/form-data") && tmp.IsNIL():
|
||||
return b.parseBody(dataformat.MultiPartFormDataFormat, req)
|
||||
}
|
||||
|
||||
if !tmp.IsNIL() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
parsed, err := b.parseBody(dataformat.FormDataFormat, req)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("Could not parse body as form data: %s\n", err)
|
||||
|
||||
@@ -244,3 +244,109 @@ func TestMultiPartFormConcurrentParse(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyGraphqlVariablesComponent(t *testing.T) {
|
||||
raw := `{
|
||||
"query": "mutation ImportPaste($host: String!, $path: String!) { importPaste(host: $host, path: $path) { result } }",
|
||||
"variables": {
|
||||
"host": "example.com",
|
||||
"path": "/robots.txt"
|
||||
}
|
||||
}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
keys := map[string]struct{}{}
|
||||
_ = body.Iterate(func(key string, value interface{}) error {
|
||||
keys[key] = struct{}{}
|
||||
return nil
|
||||
})
|
||||
require.Contains(t, keys, "host")
|
||||
require.Contains(t, keys, "path")
|
||||
require.NotContains(t, keys, "query")
|
||||
|
||||
require.NoError(t, body.SetValue("path", "/robots.txt; id"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), `/robots.txt; id`)
|
||||
require.Contains(t, string(newBody), `"host":"example.com"`)
|
||||
require.Contains(t, string(newBody), "mutation ImportPaste")
|
||||
}
|
||||
|
||||
func TestBodyGraphqlInlineArgsComponent(t *testing.T) {
|
||||
raw := `{"query":"query { jobs(jobType: \"front-end\") { id } }"}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("jobType", "canary"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), "canary")
|
||||
require.NotContains(t, string(newBody), "front-end")
|
||||
}
|
||||
|
||||
func TestBodyGraphqlNestedVariablesComponent(t *testing.T) {
|
||||
raw := `{
|
||||
"query": "mutation ($input: ImportInput!) { importPaste(input: $input) { result } }",
|
||||
"variables": {
|
||||
"input": {
|
||||
"host": "example.com",
|
||||
"path": "/robots.txt"
|
||||
}
|
||||
}
|
||||
}`
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com/graphql", strings.NewReader(raw))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("input~path", "/etc/passwd"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(newBody), `/etc/passwd`)
|
||||
require.Contains(t, string(newBody), `"host":"example.com"`)
|
||||
}
|
||||
|
||||
func TestBodyRegularJSONNotForcedToGraphql(t *testing.T) {
|
||||
req, err := retryablehttp.NewRequest("POST", "https://example.com", strings.NewReader(`{"foo":"bar"}`))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
body := New(RequestBodyComponent)
|
||||
parsed, err := body.Parse(req)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsed)
|
||||
|
||||
require.NoError(t, body.SetValue("foo", "baz"))
|
||||
rebuilt, err := body.Rebuild()
|
||||
require.NoError(t, err)
|
||||
newBody, err := io.ReadAll(rebuilt.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"foo":"baz"}`, string(newBody))
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package component
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat"
|
||||
httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
mapsutil "github.com/projectdiscovery/utils/maps"
|
||||
)
|
||||
@@ -85,14 +86,17 @@ func (c *Cookie) Rebuild() (*retryablehttp.Request, error) {
|
||||
cloned := c.req.Clone(context.Background())
|
||||
|
||||
cloned.Header.Del("Cookie")
|
||||
var fields []string
|
||||
c.value.parsed.Iterate(func(key string, value any) bool {
|
||||
cookie := &http.Cookie{
|
||||
Name: key,
|
||||
Value: fmt.Sprint(value), // Assume the value is always a string for cookies
|
||||
}
|
||||
cloned.AddCookie(cookie)
|
||||
fields = append(fields, key+"="+fmt.Sprint(value)) // Assume the value is always a string for cookies
|
||||
return true
|
||||
})
|
||||
// Cookie.String sanitizes bytes that can be intentional fuzzing payloads.
|
||||
// Header validation still applies when the HTTP transport sends the request.
|
||||
if len(fields) > 0 {
|
||||
cloned.Header.Set("Cookie", strings.Join(fields, "; "))
|
||||
}
|
||||
httputil.SetCookieFields(cloned.Request, fields)
|
||||
return cloned, nil
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 328 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user