mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2b23a8f1 | ||
|
|
2aeaf3ab8c | ||
|
|
c32d04b610 | ||
|
|
88ca259c93 | ||
|
|
1468f465c9 | ||
|
|
ada6c30520 | ||
|
|
bfc126ea64 | ||
|
|
db50a225f4 | ||
|
|
5f5a01d6a5 | ||
|
|
a73fc2ee27 | ||
|
|
1b851dcc73 | ||
|
|
9455f7ebd9 | ||
|
|
2f29c9b998 | ||
|
|
a31c52abe0 | ||
|
|
f580d8a2c4 | ||
|
|
bd4cbbef1a | ||
|
|
3e8e7435cb | ||
|
|
3998a168e4 | ||
|
|
0afa888f30 | ||
|
|
20ccda7510 | ||
|
|
e4c3595272 | ||
|
|
bb7a257063 | ||
|
|
90261e1e79 | ||
|
|
af55e53ff3 | ||
|
|
a168a58f17 | ||
|
|
5342743791 | ||
|
|
3d500f415a | ||
|
|
d637391787 | ||
|
|
c4ae5f49b6 | ||
|
|
7a75d5bbb1 | ||
|
|
453b36973f | ||
|
|
fbc1cfe355 | ||
|
|
50788612c5 | ||
|
|
464375986b | ||
|
|
2c355c5d8b | ||
|
|
0ce70fc9c2 | ||
|
|
1faf2c44bc | ||
|
|
56055b54de | ||
|
|
3727bd5c05 | ||
|
|
46ab3f251f | ||
|
|
a1298943a1 | ||
|
|
d7a2cde48c | ||
|
|
4ca992b476 | ||
|
|
80c78fbabe | ||
|
|
181b5fe703 | ||
|
|
0369cd06e6 | ||
|
|
da9ffc3493 | ||
|
|
ad9ed48a2d | ||
|
|
0e03294287 | ||
|
|
a7184480c7 | ||
|
|
bcd7329793 | ||
|
|
9075235681 |
No files matched your search
@@ -13,7 +13,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v8
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
name: Maintain security context
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "17 4 * * 1"
|
||||
push:
|
||||
paths: [.github/workflows/security-context.yml]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: security-context
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
# Analyze this commit on securitycontext.dev, wait for the result, and
|
||||
# download it. The run's own token authorizes private repositories —
|
||||
# no secrets to configure.
|
||||
- name: Generate SECURITY_CONTEXT.md
|
||||
id: context
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
sc() { curl --fail-with-body -sS -H "X-GitHub-Token: $GITHUB_TOKEN" "$@"; }
|
||||
base="https://securitycontext.dev"
|
||||
head="$(git rev-parse HEAD)"
|
||||
|
||||
sc -X POST "$base/api/v1/context?wait=60" -H "Content-Type: application/json" \
|
||||
-H "X-Secc-Client: github-action" --data "{\"repo\":\"$GITHUB_REPOSITORY\",\"head\":\"$head\"}" >/dev/null
|
||||
|
||||
for _ in {1..120}; do
|
||||
state="$(sc "$base/api/v1/context/$GITHUB_REPOSITORY")"
|
||||
status="$(jq -r .status <<<"$state")"
|
||||
if [ "$status" = "error" ]; then
|
||||
jq -r '.error // "analysis failed"' <<<"$state" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$status" = "ready" ] && [ "$(jq -r '.summary.head_sha // empty' <<<"$state")" = "$head" ]; then
|
||||
sc -L "$base/r/$GITHUB_REPOSITORY.md" -o SECURITY_CONTEXT.md
|
||||
# A private report is access-gated on the web, so the pull request
|
||||
# links the rendered file on this branch instead.
|
||||
jq -r '"visibility=" + (.visibility // "public")' <<<"$state" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
echo "Timed out waiting for analysis of $head" >&2
|
||||
exit 1
|
||||
|
||||
# A pull request should mean the analysis changed. When only the volatile
|
||||
# header moved (timestamps, analyzed commit, commit counts), restore the
|
||||
# committed file so no PR is opened and any header-only PR closes.
|
||||
- name: Skip the update unless the analysis changed
|
||||
id: report
|
||||
run: |
|
||||
changed=true
|
||||
if [ -n "$(git ls-files SECURITY_CONTEXT.md)" ] && git diff --quiet -I '^<!-- Generated' -I '^_Generated' -I '^_Analyzed commit' -I '^- Commits analyzed' -I '^- Commits flagged' -- SECURITY_CONTEXT.md; then
|
||||
git checkout -- SECURITY_CONTEXT.md
|
||||
changed=false
|
||||
fi
|
||||
echo "changed=$changed" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Point the repository's agent instructions at the file, when it has any.
|
||||
# Never create one: adding an agent instructions file uninvited is a
|
||||
# bigger footprint than this warrants.
|
||||
- name: Reference the context from agent instructions
|
||||
id: agents
|
||||
env:
|
||||
REPORT_CHANGED: ${{ steps.report.outputs.changed }}
|
||||
run: |
|
||||
line="See SECURITY_CONTEXT.md for this repo's known vulnerabilities and recurring weak spots. Check it before writing or reviewing security-sensitive code."
|
||||
touched=""
|
||||
paths="SECURITY_CONTEXT.md"
|
||||
for f in CLAUDE.md AGENTS.md; do
|
||||
if [ ! -f "$f" ]; then continue; fi
|
||||
paths="$(printf '%s\n%s' "$paths" "$f")"
|
||||
if grep -q "SECURITY_CONTEXT.md" "$f"; then continue; fi
|
||||
printf '\n%s\n' "$line" >> "$f"
|
||||
if [ -n "$touched" ]; then touched="$touched and $f"; else touched="$f"; fi
|
||||
done
|
||||
echo "touched=$touched" >> "$GITHUB_OUTPUT"
|
||||
# Title the pull request for what it actually carries: when the
|
||||
# analysis is unchanged, the only edit is the agent-instructions line.
|
||||
if [ "$REPORT_CHANGED" = "false" ]; then
|
||||
echo "subject=chore: reference security context from agent instructions" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "subject=chore: refresh security context" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
{
|
||||
echo "paths<<SC_EOF"
|
||||
echo "$paths"
|
||||
echo "SC_EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Open an update pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
branch: security-context/refresh
|
||||
commit-message: ${{ steps.agents.outputs.subject }}
|
||||
title: ${{ steps.agents.outputs.subject }}
|
||||
body: |
|
||||
Refreshes `SECURITY_CONTEXT.md`, this repository's security-fix history as working context, so agents and reviewers don't reintroduce fixes that already shipped.
|
||||
|
||||
${{ steps.context.outputs.visibility == 'private' && format('[Read the updated file]({0}/{1}/blob/security-context/refresh/SECURITY_CONTEXT.md)', github.server_url, github.repository) || format('[Full report for this repository](https://securitycontext.dev/r/{0})', github.repository) }}
|
||||
|
||||
${{ steps.agents.outputs.touched != '' && format('Also adds a one-line reference to {0} so your coding agent picks this up automatically.', steps.agents.outputs.touched) || 'Tip: reference `SECURITY_CONTEXT.md` from your agent instructions file (`CLAUDE.md` or `AGENTS.md`) so your coding agent reads it automatically.' }}
|
||||
|
||||
---
|
||||
_Your agent doesn't know this repo's security history. [Security Context](https://securitycontext.dev) keeps it where your agent can see it, so old vulnerabilities and their variants don't come back in new code._
|
||||
add-paths: ${{ steps.agents.outputs.paths }}
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v11
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
|
||||
@@ -17,14 +17,12 @@ 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.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- uses: projectdiscovery/actions/golangci-lint/v2@v1
|
||||
@@ -44,9 +42,19 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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)"
|
||||
@@ -77,12 +85,12 @@ jobs:
|
||||
|
||||
sdk:
|
||||
name: "Run example SDK"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- name: "Simple"
|
||||
@@ -98,7 +106,7 @@ jobs:
|
||||
|
||||
integration:
|
||||
name: "Integration tests"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
strategy:
|
||||
@@ -107,22 +115,10 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- 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 }}"
|
||||
@@ -130,7 +126,7 @@ jobs:
|
||||
|
||||
functional:
|
||||
name: "Functional tests"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
strategy:
|
||||
@@ -139,7 +135,7 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: projectdiscovery/actions/setup/python@v1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
@@ -151,26 +147,26 @@ jobs:
|
||||
|
||||
validate:
|
||||
name: "Template validate"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ github.token }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/nuclei-action/cache@v3
|
||||
- run: make template-validate
|
||||
|
||||
codeql:
|
||||
name: "CodeQL analysis"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: 'go'
|
||||
@@ -205,16 +201,18 @@ jobs:
|
||||
|
||||
release:
|
||||
name: "Release test"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@v7
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
with:
|
||||
go-version: "stable"
|
||||
- uses: projectdiscovery/actions/goreleaser@v1
|
||||
|
||||
flamegraph:
|
||||
name: "Flamegraph"
|
||||
needs: ["lint"]
|
||||
needs: ["tests"]
|
||||
uses: ./.github/workflows/flamegraph.yaml
|
||||
|
||||
# perf-regression:
|
||||
|
||||
@@ -17,4 +17,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: crate-ci/typos@v1.51.1
|
||||
- uses: crate-ci/typos@v1.47.2
|
||||
@@ -33,8 +33,6 @@
|
||||
|
||||
# Templates
|
||||
/*.yaml
|
||||
!/.sops.yaml
|
||||
!/.golangci.yaml
|
||||
|
||||
# Generated docs
|
||||
/dsl.md
|
||||
|
||||
@@ -12,7 +12,6 @@ builds:
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOEXPERIMENT=greenteagc
|
||||
- GODEBUG=tlssha1=1
|
||||
goos: [windows, linux, darwin]
|
||||
goarch: [amd64, '386', arm, arm64]
|
||||
ignore:
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# 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)$
|
||||
@@ -81,5 +81,4 @@ Each protocol (HTTP, DNS, Network, etc.) implements:
|
||||
- **internal/tests/functional/** - CI-only native functional comparison suite and testcase assets
|
||||
- **pkg/fuzz/** - Fuzzing engine and DAST capabilities
|
||||
- **pkg/input/** - Input processing for various formats (Burp, OpenAPI, etc.)
|
||||
- **pkg/reporting/** - Result export and issue tracking integrations
|
||||
See SECURITY_CONTEXT.md for this repo's known vulnerabilities and recurring weak spots. Check it before writing or reviewing security-sensitive code.
|
||||
- **pkg/reporting/** - Result export and issue tracking integrations
|
||||
@@ -36,7 +36,7 @@ It also specifies a `WorkflowLoader` interface that is used during workflow load
|
||||
|
||||
```go
|
||||
type WorkflowLoader interface {
|
||||
GetTemplatePathsByTags(tags []string, noValidate bool) []string
|
||||
GetTemplatePathsByTags(tags []string) []string
|
||||
GetTemplatePaths(templatesList []string, noValidate bool) []string
|
||||
}
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Build
|
||||
FROM golang:1.27-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
RUN apk add build-base
|
||||
WORKDIR /app
|
||||
|
||||
@@ -33,9 +33,8 @@ clean:
|
||||
|
||||
go-build: clean
|
||||
go-build:
|
||||
CGO_ENABLED=0 GOEXPERIMENT=greenteagc GODEBUG=tlssha1=1 $(GOBUILD) \
|
||||
-trimpath $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
|
||||
-o '${GOBUILD_OUTPUT}${GOBUILD_OUTPUT_EXT}' $(GOBUILD_PACKAGES)
|
||||
CGO_ENABLED=0 $(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
|
||||
@@ -91,10 +90,8 @@ 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 40m -v ./internal/tests/integration
|
||||
$(GOTEST) -tags=integration -timeout 1h ./internal/tests/integration
|
||||
|
||||
integration-debug:
|
||||
$(GOTEST) -tags=integration ./internal/tests/integration -v $(GO_TEST_ARGS) -args $(INTEGRATION_ARGS)
|
||||
@@ -112,7 +109,7 @@ functional: build
|
||||
exit 1; \
|
||||
fi; \
|
||||
RELEASE_BINARY="$$release_binary" DEV_BINARY="$(PWD)/bin/nuclei" \
|
||||
$(GOTEST) -tags=functional -timeout 1h -v ./internal/tests/functional
|
||||
$(GOTEST) -tags=functional -timeout 1h ./internal/tests/functional
|
||||
|
||||
tidy:
|
||||
$(GOMOD) tidy
|
||||
@@ -300,4 +297,4 @@ template-validate:
|
||||
./bin/nuclei -validate \
|
||||
-w workflows \
|
||||
-et http/technologies \
|
||||
-ept code
|
||||
-ept code
|
||||
@@ -1,7 +1,7 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
@@ -9,9 +9,8 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
@@ -65,11 +64,11 @@
|
||||
|
||||
### **1. Nuclei CLI**
|
||||
|
||||
_Install Nuclei on your machine. Get started by following the installation guide [**`here`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme). Additionally, we provide [**`a free cloud tier`**](https://cloud.projectdiscovery.io/sign-up) that comes with generous monthly free limits:_
|
||||
_Install Nuclei on your machine. Get started by following the installation guide [**`here`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme). Additionally, We provide [**`a free cloud tier`**](https://cloud.projectdiscovery.io/sign-up) and comes with a generous monthly free limits:_
|
||||
|
||||
- Store and visualize your vulnerability findings
|
||||
- Write and manage your Nuclei templates
|
||||
- Access the latest Nuclei templates
|
||||
- Write and manage your nuclei templates
|
||||
- Access latest nuclei templates
|
||||
- Discover and store your targets
|
||||
|
||||
> [!Important]
|
||||
@@ -92,7 +91,7 @@ _For security teams and enterprises, we provide a cloud-hosted service built on
|
||||
- We're constantly [**`adding new features`**](https://feedback.projectdiscovery.io/changelog)!
|
||||
- **Ideal for:** Pentesters, security teams, and enterprises
|
||||
|
||||
[**`Sign up to Pro`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) or [**`Talk to our team`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) if you have a large organization and complex requirements.
|
||||
[**`Sign up to Pro`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) or [**`Talk to our team`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) if you have large organization and complex requirements.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@@ -111,7 +110,7 @@ Browse the full Nuclei [**`documentation here`**](https://docs.projectdiscovery.
|
||||
|
||||
### Installation
|
||||
|
||||
`nuclei` requires **go >= 1.27.3** to install successfully. Run the following command to get the repo:
|
||||
`nuclei` requires **go >= 1.24.2** to install successfully. Run the following command to get the repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,20 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6); default uses dialer fallback, -sa scans all addresses
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -176,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -219,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -247,15 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -278,9 +260,6 @@ 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")
|
||||
@@ -298,9 +277,6 @@ 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)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -325,9 +301,7 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
-ac, -auto-calibrate drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)
|
||||
|
||||
HEADLESS:
|
||||
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
|
||||
@@ -344,7 +318,6 @@ 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
|
||||
@@ -360,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
@@ -686,7 +658,7 @@ Thanks to all the amazing [**`community contributors for sending PRs`**](https:/
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
<sub>**`nuclei`** is distributed under [**MIT License**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md)</sub>
|
||||
|
||||
</div>
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ _针对安全团队和企业,我们提供了构建在 Nuclei OSS 之上的云
|
||||
|
||||
### 安装
|
||||
|
||||
`nuclei` 需要 **go >= 1.27.3** 才能成功安装。运行以下命令获取仓库:
|
||||
`nuclei` 需要 **go >= 1.24.2** 才能成功安装。运行以下命令获取仓库:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Consulta la [**`documentación completa de Nuclei aquí`**](https://docs.project
|
||||
|
||||
### Instalación
|
||||
|
||||
`nuclei` requiere **go >= 1.27.3** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
`nuclei` requiere **go >= 1.24.2** para instalarse correctamente. Ejecuta el siguiente comando para obtener el repositorio:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
-726
@@ -1,726 +0,0 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README.md">`English`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_CN.md">`中文`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_KR.md">`Korean`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ID.md">`Indonesia`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://docs.projectdiscovery.io/tools/nuclei/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Documentation-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1ib29rLW9wZW4iPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjwvc3ZnPg==&logoColor=white" alt="Documentation Badge"></a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei-templates"><img src="https://img.shields.io/badge/Templates Library-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoaWVsZCI+PHBhdGggZD0iTTIwIDEzYzAgNS0zLjUgNy41LTcuNjYgOC45NWExIDEgMCAwIDEtLjY3LS4wMUM3LjUgMjAuNSA0IDE4IDQgMTNWNmExIDEgMCAwIDEgMS0xYzIgMCA0LjUtMS4yIDYuMjQtMi43MmExLjE3IDEuMTcgMCAwIDEgMS41MiAwQzE0LjUxIDMuODEgMTcgNSAxOSA1YTEgMSAwIDAgMSAxIDF6Ii8+PC9zdmc+&logoColor=white"></a>
|
||||
|
||||
<a href="https://discord.gg/projectdiscovery?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Discord Badge"></a>
|
||||
|
||||
|
||||
<hr>
|
||||
</p>
|
||||
|
||||
<div dir="rtl">
|
||||
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
**Nuclei یک اسکنر آسیبپذیری مدرن و با کارایی بالاست که از قالبهای ساده بر پایه YAML استفاده میکند. این ابزار به شما امکان میدهد سناریوهای سفارشی برای شناسایی آسیبپذیریها طراحی کنید که شرایط واقعی را شبیهسازی میکنند و منجر به صفر شدن خطاهای مثبت کاذب (گزارشهای اشتباه) میشوند.**
|
||||
|
||||
- فرمت ساده YAML برای ساخت و سفارشیسازی قالبهای آسیبپذیری.
|
||||
- مشارکت توسط هزاران متخصص امنیت جهت مقابله با آسیبپذیریهای نوظهور و رایج.
|
||||
- کاهش خطاهای مثبت کاذب با شبیهسازی مراحل واقعی جهت تایید یک آسیبپذیری.
|
||||
- پردازش موازی فوقسریع اسکنها و خوشهبندی درخواستها.
|
||||
- قابلیت ادغام در خطوط لوله CI/CD برای شناسایی آسیبپذیریها و تستهای رگرسیون.
|
||||
- پشتیبانی از پروتکلهای متعدد مانند TCP، DNS، HTTP، SSL، WHOIS، جاوااسکریپت، کد و موارد دیگر.
|
||||
- قابلیت یکپارچهسازی با Jira، Splunk، GitHub، Elastic و GitLab.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## فهرست مطالب
|
||||
|
||||
- [**`شروع کار`**](#شروع-کار)
|
||||
- [_`۱. ابزار خط فرمان Nuclei`_](#۱-ابزار-خط-فرمان-nuclei)
|
||||
- [_`۲. نسخههای حرفهای و سازمانی`_](#۲-نسخههای-حرفهای-و-سازمانی)
|
||||
- [**`مستندات`**](#مستندات)
|
||||
- [_`دستورات خط فرمان`_](#دستورات-خط-فرمان)
|
||||
- [_`اسکن یک هدف واحد`_](#اسکن-یک-هدف)
|
||||
- [_`اسکن اهداف متعدد`_](#اسکن-چندین-هدف)
|
||||
- [_`اسکن شبکه`_](#اسکن-شبکه)
|
||||
- [_`اسکن با قالب سفارشی شما`_](#اسکن-با-استفاده-از-قالب-سفارشی-خودتان)
|
||||
- [_`اتصال Nuclei به ProjectDiscovery`_](#اتصال-nuclei-به-projectdiscovery)
|
||||
- [**`قالبهای Nuclei، جامعه کاربری و پاداشها`**](#قالبهای-nuclei-جامعه-و-پاداشها-) 💎
|
||||
- [**`ماموریت ما`**](#ماموریت-ما)
|
||||
- [**`مشارکتکنندگان`**](#مشارکتکنندگان-) ❤
|
||||
- [**`مجوز استفاده`**](#license)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## شروع کار
|
||||
|
||||
### ۱. ابزار خط فرمان Nuclei
|
||||
|
||||
_ابزار Nuclei را روی سیستم خود نصب کنید. کار خود را با دنبال کردن راهنمای نصب در [**`اینجا`**](https://docs.projectdiscovery.io/tools/nuclei/install?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) آغاز کنید. علاوه بر این، ما [**`یک سطح ابری رایگان`**](https://cloud.projectdiscovery.io/sign-up) ارائه میدهیم که همراه با محدودیتهای رایگان ماهانه سخاوتمندانهای عرضه میشود:_
|
||||
|
||||
- ذخیرهسازی و بصریسازی یافتههای آسیبپذیری شما
|
||||
- نوشتن و مدیریت قالبهای Nuclei شما
|
||||
- دسترسی به آخرین قالبهای Nuclei
|
||||
- شناسایی و ذخیرهسازی اهداف شما
|
||||
|
||||
> [!Important]
|
||||
> |**این پروژه در حال توسعه فعال است**. انتظار تغییرات اساسی در نسخههای جدید را داشته باشید. لطفاً قبل از بهروزرسانی، لیست تغییرات نسخه (Changelog) را بررسی کنید.|
|
||||
> |:--------------------------------|
|
||||
> | این پروژه در درجه اول برای استفاده به عنوان یک ابزار خط فرمان (CLI) مستقل ساخته شده است. **اجرای Nuclei به عنوان یک سرویس ممکن است خطرات امنیتی به همراه داشته باشد.** توصیه میشود با احتیاط و با در نظر گرفتن تدابیر امنیتی اضافی از آن استفاده کنید. |
|
||||
|
||||
<br>
|
||||
|
||||
### ۲. نسخههای حرفهای و سازمانی
|
||||
|
||||
_برای تیمهای امنیتی و سازمانها، ما یک سرویس میزبانیشده در ابر بر پایه نسخه متنباز (OSS) ابزار Nuclei ارائه میدهیم؛ این سرویس بهطور دقیق بهینهسازی شده است تا به شما کمک کند اسکنهای آسیبپذیری را بهصورت مداوم، در مقیاس بزرگ و همراه با تیم و گردشهای کاری فعلی خود اجرا کنید:_
|
||||
|
||||
- اسکنهای ۵۰ برابر سریعتر
|
||||
- اسکن در مقیاس بزرگ با دقت بسیار بالا
|
||||
- یکپارچهسازی با سرویسهای ابری (AWS، GCP، Azure، Cloudflare، Fastly، Terraform، Kubernetes)
|
||||
- اتصال به Jira، Slack، Linear، رابطهای برنامهنویسی (APIs) و وبهوکها (Webhooks)
|
||||
- گزارشدهیهای مدیریتی و سازگاری با استانداردهای امنیتی (Compliance)
|
||||
- بهعلاوه: اسکن بیدرنگ (Real-time)، پشتیبانی از SAML SSO، پلتفرم منطبق با استاندارد SOC 2 (همراه با گزینههای میزبانی در اتحادیه اروپا و آمریکا)، فضاهای کاری مشترک تیمی و موارد دیگر
|
||||
- ما بهطور مداوم در حال [**`افزودن قابلیتهای جدید`**](https://feedback.projectdiscovery.io/changelog) هستیم!
|
||||
- **ایدهآل برای:** نفوذگران قانونمند (Pentesters)، تیمهای امنیتی و سازمانها
|
||||
|
||||
در صورتی که سازمان بزرگی دارید و نیازمندیهای شما پیچیده است، میتوانید در [**`نسخه پرو ثبتنام کنید`**](https://projectdiscovery.io/pricing?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) یا [**`با تیم ما گفتگو کنید`**](https://projectdiscovery.io/request-demo?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme).
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مستندات
|
||||
|
||||
[**`مستندات کامل`**](https://docs.projectdiscovery.io/tools/nuclei/running) ابزار Nuclei را در اینجا مرور کنید. اگر بهتازگی کار با Nuclei را شروع کردهاید، [**`مجموعه ویدیوهای آموزشی پایهای ما در یوتیوب`**](https://www.youtube.com/playlist?list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl) را بررسی کنید.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=b5qMyQvL1ZA&list=PLZRbR9aMzTTpItEdeNSulo8bYsvil80Rl&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-getting-started.png" width="350px" alt="Nuclei Getting Started Video Tutorial"></a> <a href="https://www.youtube.com/watch?v=nFXygQdtjyw&utm_source=github&utm_medium=web&utm_campaign=nuclei_readme" target="_blank"><img src="/static/nuclei-write-your-first-template.png" width="350px" alt="Writing Your First Nuclei Template Video Tutorial"></a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
### نصب
|
||||
|
||||
برای نصب موفقیتآمیز `nuclei` به نسخه **go >= 1.24.2** نیاز است. دستور زیر را برای دریافت مخزن اجرا کنید:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
```
|
||||
</div>
|
||||
|
||||
برای اطلاعات بیشتر درباره نحوه نصب nuclei، به آدرس `https://docs.projectdiscovery.io/tools/nuclei/install` مراجعه کنید.
|
||||
|
||||
### دستورات خط فرمان
|
||||
|
||||
برای نمایش تمامی پارامترها و سوییچهای این ابزار:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -h
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>گسترش تمام فلگهای راهنما</summary>
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```yaml
|
||||
Nuclei is a fast, template based vulnerability scanner focusing
|
||||
on extensive configurability, massive extensibility and ease of use.
|
||||
|
||||
Usage:
|
||||
./nuclei [flags]
|
||||
|
||||
Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
|
||||
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
-ntv, -new-templates-version string[] run new templates added in specific version
|
||||
-as, -automatic-scan automatic web scan using wappalyzer technology detection to tags mapping
|
||||
-t, -templates string[] list of template or template directory to run (comma-separated, file)
|
||||
-turl, -template-url string[] template url or list containing template urls to run (comma-separated, file)
|
||||
-ai, -prompt string generate and run template using ai prompt
|
||||
-w, -workflows string[] list of workflow or workflow directory to run (comma-separated, file)
|
||||
-wurl, -workflow-url string[] workflow url or list containing workflow urls to run (comma-separated, file)
|
||||
-validate validate the passed templates to nuclei
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
-tgl list all available tags
|
||||
-sign signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable
|
||||
-code enable loading code protocol-based templates
|
||||
-dut, -disable-unsigned-templates disable running unsigned templates or templates with mismatched signature
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
-tags string[] templates to run based on tags (comma-separated, file)
|
||||
-etags, -exclude-tags string[] templates to exclude based on tags (comma-separated, file)
|
||||
-itags, -include-tags string[] tags to be executed even if they are excluded either by default or configuration
|
||||
-id, -template-id string[] templates to run based on template ids (comma-separated, file, allow-wildcard)
|
||||
-eid, -exclude-id string[] templates to exclude based on template ids (comma-separated, file)
|
||||
-it, -include-templates string[] path to template file or directory to be executed even if they are excluded either by default or configuration
|
||||
-et, -exclude-templates string[] path to template file or directory to exclude (comma-separated, file)
|
||||
-em, -exclude-matchers string[] template matchers to exclude in result
|
||||
-s, -severity value[] templates to run based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-es, -exclude-severity value[] templates to exclude based on severity. Possible values: info, low, medium, high, critical, unknown
|
||||
-pt, -type value[] templates to run based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-ept, -exclude-type value[] templates to exclude based on protocol type. Possible values: dns, file, http, headless, tcp, workflow, ssl, websocket, whois, code, javascript
|
||||
-tc, -template-condition string[] templates to run based on expression condition
|
||||
|
||||
OUTPUT:
|
||||
-o, -output string output file to write found issues/vulnerabilities
|
||||
-sresp, -store-resp store all request/response passed through nuclei to output directory
|
||||
-srd, -store-resp-dir string store all request/response passed through nuclei to custom directory (default "output")
|
||||
-silent display findings only
|
||||
-nc, -no-color disable output content coloring (ANSI escape codes)
|
||||
-j, -jsonl write output in JSONL(ines) format
|
||||
-irr, -include-rr -omit-raw include request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only) [DEPRECATED use -omit-raw] (default true)
|
||||
-or, -omit-raw omit request/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)
|
||||
-ot, -omit-template omit encoded template in the JSON, JSONL output
|
||||
-nm, -no-meta disable printing result metadata in cli output
|
||||
-ts, -timestamp enables printing timestamp in cli output
|
||||
-rdb, -report-db string nuclei reporting database (always use this to persist report data)
|
||||
-ms, -matcher-status display match failure status
|
||||
-me, -markdown-export string directory to export results in markdown format
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
-tp, -profile string template profile config file to run
|
||||
-tpl, -profile-list list community template profiles
|
||||
-fr, -follow-redirects enable following redirects for http templates
|
||||
-fhr, -follow-host-redirects follow redirects on the same host
|
||||
-mr, -max-redirects int max number of redirects to follow for http templates (default 10)
|
||||
-dr, -disable-redirects disable redirects for http templates
|
||||
-rc, -report-config string nuclei reporting module configuration file
|
||||
-H, -header string[] custom header/cookie to include in all http request in header:value format (cli, file)
|
||||
-V, -var value custom vars in key=value format
|
||||
-r, -resolvers string file containing resolver list for nuclei
|
||||
-sr, -system-resolvers use system DNS resolving as error fallback
|
||||
-dc, -disable-clustering disable clustering of requests
|
||||
-passive enable passive HTTP response processing mode
|
||||
-fh2, -force-http2 force http2 connection on requests
|
||||
-ev, -env-vars enable environment variables to be used in template
|
||||
-cc, -client-cert string client certificate file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ck, -client-key string client key file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-ca, -client-ca string client certificate authority file (PEM-encoded) used for authenticating against scanned hosts
|
||||
-sml, -show-match-line show match lines for file templates, works with extractors only
|
||||
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
|
||||
-sni string tls sni hostname to use (default: input domain name)
|
||||
-dka, -dialer-keep-alive value keep-alive duration for network requests.
|
||||
-lfa, -allow-local-file-access allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lna, -restrict-local-network-access blocks connections to the local / private network
|
||||
-i, -interface string network interface to use for network scan
|
||||
-at, -attack-type string type of payload combinations to perform (batteringram,pitchfork,clusterbomb)
|
||||
-pro, -payload-random-order iterate payload combinations in a random order instead of sequentially
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
|
||||
INTERACTSH:
|
||||
-iserver, -interactsh-server string interactsh server url for self-hosted instance (default: oast.pro,oast.live,oast.site,oast.online,oast.fun,oast.me)
|
||||
-itoken, -interactsh-token string authentication token for self-hosted interactsh server
|
||||
-interactions-cache-size int number of requests to keep in the interactions cache (default 5000)
|
||||
-interactions-eviction int number of seconds to wait before evicting requests from cache (default 60)
|
||||
-interactions-poll-duration int number of seconds to wait before each interaction poll request (default 5)
|
||||
-interactions-cooldown-period int extra time for interaction polling before exiting (default 5)
|
||||
-ni, -no-interactsh disable interactsh server for OAST testing, exclude OAST based templates
|
||||
|
||||
FUZZING:
|
||||
-ft, -fuzzing-type string overrides fuzzing type set in template (replace, prefix, postfix, infix)
|
||||
-fm, -fuzzing-mode string overrides fuzzing mode set in template (multiple, single)
|
||||
-fuzz enable loading fuzzing templates (Deprecated: use -dast instead)
|
||||
-dast enable / run dast (fuzz) nuclei templates
|
||||
-dts, -dast-server enable dast server mode (live fuzzing)
|
||||
-dtr, -dast-report write dast scan report to file
|
||||
-dtst, -dast-server-token string dast server token (optional)
|
||||
-dtsa, -dast-server-address string dast server address (default "localhost:9055")
|
||||
-dtp, -dast-proxy enable dast proxy mode (fuzz live traffic proxied through nuclei)
|
||||
-dtpa, -dast-proxy-address string dast proxy listen address (default "127.0.0.1:9056")
|
||||
-dtpau, -dast-proxy-auth string dast proxy basic auth in user:pass format (required for non-loopback address)
|
||||
-dfp, -display-fuzz-points display fuzz points in the output for debugging
|
||||
-fuzz-param-frequency int frequency of uninteresting parameters for fuzzing before skipping (default 10)
|
||||
-fa, -fuzz-aggression string fuzzing aggression level controls payload count for fuzz (low, medium, high) (default "low")
|
||||
-cs, -fuzz-scope string[] in scope url regex to be followed by fuzzer
|
||||
-cos, -fuzz-out-scope string[] out of scope url regex to be excluded by fuzzer
|
||||
|
||||
UNCOVER:
|
||||
-uc, -uncover enable uncover engine
|
||||
-uq, -uncover-query string[] uncover search query
|
||||
-ue, -uncover-engine string[] uncover search engine (shodan,censys,fofa,shodan-idb,quake,hunter,zoomeye,netlas,criminalip,publicwww,hunterhow,google) (default shodan)
|
||||
-uf, -uncover-field string uncover fields to return (ip,port,host) (default "ip:port")
|
||||
-ul, -uncover-limit int uncover results to return (default 100)
|
||||
-ur, -uncover-ratelimit int override ratelimit of engines with unknown ratelimit (default 60 req/min) (default 60)
|
||||
|
||||
RATE-LIMIT:
|
||||
-rl, -rate-limit int maximum number of requests to send per second (default 150)
|
||||
-rld, -rate-limit-duration value maximum number of requests to send per second (default 1s)
|
||||
-host-backoff slow down a host that starts blocking (429, repeated 403s, connection failures)
|
||||
-host-backoff-max value maximum delay applied to a host that is blocking (default 30s)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
-hbs, -headless-bulk-size int maximum number of headless hosts to be analyzed in parallel per template (default 10)
|
||||
-headc, -headless-concurrency int maximum number of headless templates to be executed in parallel (default 10)
|
||||
-jsc, -js-concurrency int maximum number of javascript runtimes to be executed in parallel (default 120)
|
||||
-pc, -payload-concurrency int max payload concurrency for each template (default 25)
|
||||
-prc, -probe-concurrency int http probe concurrency with httpx (default 50)
|
||||
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int time to wait in seconds before timeout (default 10)
|
||||
-retries int number of times to retry a failed request (default 1)
|
||||
-ldp, -leave-default-ports leave default HTTP/HTTPS ports (eg. host:80,host:443)
|
||||
-mhe, -max-host-error int max errors for a host before skipping from scan (default 30)
|
||||
-te, -track-error string[] adds given error to max-host-error watchlist (standard, file)
|
||||
-nmhe, -no-mhe disable skipping host from scan based on errors
|
||||
-project use a project folder to avoid sending same request multiple times
|
||||
-project-path string set a specific project path (default "/tmp")
|
||||
-spm, -stop-at-first-match stop processing HTTP requests after the first match (may break template/workflow logic)
|
||||
-stream stream mode - start elaborating without sorting the input
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
-headless enable templates that require headless browser support (root user on Linux will disable sandbox)
|
||||
-page-timeout int seconds to wait for each page in headless mode (default 20)
|
||||
-sb, -show-browser show the browser on the screen when running templates with headless mode
|
||||
-ho, -headless-options string[] start headless chrome with additional options
|
||||
-sc, -system-chrome use local installed Chrome browser instead of nuclei installed
|
||||
-cdpe, -cdp-endpoint string use remote browser via Chrome DevTools Protocol (CDP) endpoint
|
||||
-lha, -list-headless-action list available headless actions
|
||||
|
||||
DEBUG:
|
||||
-debug show all requests and responses
|
||||
-dreq, -debug-req show all sent requests
|
||||
-dresp, -debug-resp show all received responses
|
||||
-p, -proxy string[] list of http/socks5 proxy to use (comma separated or file input)
|
||||
-pi, -proxy-internal proxy all internal requests
|
||||
-rp, -replay-proxy string http/socks5 proxy to re-send requests that produced a finding through (e.g. burp)
|
||||
-ldf, -list-dsl-function list all supported DSL function signatures
|
||||
-tlog, -trace-log string file to write sent requests trace log
|
||||
-elog, -error-log string file to write sent requests error log
|
||||
-version show nuclei version
|
||||
-hm, -hang-monitor enable nuclei hang monitoring
|
||||
-v, -verbose show verbose output
|
||||
-profile-mem string generate memory (heap) profile & trace files
|
||||
-vv display templates loaded for scan
|
||||
-svd, -show-var-dump show variables dump for debugging
|
||||
-vdl, -var-dump-limit int limit the number of characters displayed in var dump (default 255)
|
||||
-ep, -enable-pprof enable pprof debugging server
|
||||
-tv, -templates-version shows the version of the installed nuclei-templates
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
-hpt, -honeypot-threshold int number of distinct template IDs required to flag a honeypot host (default 15)
|
||||
-shp, -suppress-honeypot suppress output for flagged honeypot hosts
|
||||
|
||||
STATISTICS:
|
||||
-stats display statistics about the running scan
|
||||
-sj, -stats-json display statistics in JSONL(ines) format
|
||||
-si, -stats-interval int number of seconds to wait between showing a statistics update (default 5)
|
||||
-mp, -metrics-port int port to expose nuclei metrics on (default 9092)
|
||||
-hps, -http-stats enable http status capturing (experimental)
|
||||
|
||||
CLOUD:
|
||||
-auth configure projectdiscovery cloud (pdcp) api key (default true)
|
||||
-tid, -team-id string upload scan results to given team id (optional) (default "none")
|
||||
-cup, -cloud-upload upload scan results to pdcp dashboard [DEPRECATED use -dashboard]
|
||||
-sid, -scan-id string upload scan results to existing scan id (optional)
|
||||
-sname, -scan-name string scan name to set (optional)
|
||||
-pd, -dashboard upload / view nuclei results in projectdiscovery cloud (pdcp) UI dashboard
|
||||
-pdu, -dashboard-upload string upload / view nuclei results file (jsonl) in projectdiscovery cloud (pdcp) UI dashboard
|
||||
|
||||
AUTHENTICATION:
|
||||
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
|
||||
-ps, -prefetch-secrets prefetch secrets from the secrets file
|
||||
# NOTE: Headers in secrets files preserve exact casing (useful for case-sensitive APIs)
|
||||
|
||||
|
||||
EXAMPLES:
|
||||
Run nuclei on single host:
|
||||
$ nuclei -target example.com
|
||||
|
||||
Run nuclei with specific template directories:
|
||||
$ nuclei -target example.com -t http/cves/ -t ssl
|
||||
|
||||
Run nuclei against a list of hosts:
|
||||
$ nuclei -list hosts.txt
|
||||
|
||||
Run nuclei with a JSON output:
|
||||
$ nuclei -target example.com -json-export output.json
|
||||
|
||||
Run nuclei with sorted Markdown outputs (with environment variables):
|
||||
$ MARKDOWN_EXPORT_SORT_MODE=template nuclei -target example.com -markdown-export nuclei_report/
|
||||
|
||||
Additional documentation is available at: https://docs.projectdiscovery.io/getting-started/running
|
||||
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
مستندات تکمیلی در این آدرس در دسترس است: [**`docs.projectdiscovery.io/getting-started/running`**](https://docs.projectdiscovery.io/getting-started/running?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme)
|
||||
|
||||
</details>
|
||||
|
||||
### اسکن یک هدف
|
||||
|
||||
برای انجام یک اسکن سریع روی یک وباپلیکیشن:
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
### اسکن چندین هدف
|
||||
|
||||
Nuclei میتواند با ارائهٔ فهرستی از اهداف، اسکن گروهی را انجام دهد. میتوانید از فایلی استفاده کنید که شامل چندین URL است.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -list urls.txt
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن شبکه
|
||||
|
||||
این کار کل زیرشبکه را برای مشکلات مرتبط با شبکه، مانند پورتهای باز یا سرویسهای پیکربندی نادرست، اسکن میکند.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target 192.168.1.0/24
|
||||
```
|
||||
</div>
|
||||
|
||||
### اسکن با استفاده از قالب سفارشی خودتان
|
||||
|
||||
برای نوشتن و استفاده از قالب خودتان، یک فایل `.yaml` با قوانین مشخص ایجاد کنید، سپس آن را بهصورت زیر استفاده کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -u https://example.com -t /path/to/your-template.yaml
|
||||
```
|
||||
</div>
|
||||
|
||||
### اتصال Nuclei به ProjectDiscovery
|
||||
|
||||
میتوانید اسکنها را روی دستگاه خود اجرا کرده و نتایج را برای تحلیل و رفع مشکلات بیشتر، در پلتفرم ابری بارگذاری کنید.
|
||||
|
||||
<div dir="ltr">
|
||||
|
||||
```sh
|
||||
nuclei -target https://example.com -dashboard
|
||||
```
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> این قابلیت کاملاً رایگان است و به هیچگونه اشتراکی نیاز ندارد. برای راهنمایی دقیقتر، به [**`مستندات`**](https://docs.projectdiscovery.io/cloud/scanning/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme) مراجعه کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## قالبهای Nuclei، جامعه و پاداشها 💎
|
||||
|
||||
[**قالبهای Nuclei**](https://github.com/projectdiscovery/nuclei-templates) بر اساس مفهوم فایلهای قالب مبتنی بر YAML طراحی شدهاند که نحوه ارسال و پردازش درخواستها را تعریف میکنند. این موضوع امکان توسعهپذیری آسان Nuclei را فراهم میکند. قالبها با استفاده از YAML نوشته میشوند که قالبی ساده و قابلخواندن برای انسان ارائه میدهد تا بتوان فرآیند اجرا را بهسرعت تعریف کرد.
|
||||
|
||||
**همین حالا بهصورت آنلاین با ویرایشگر رایگان قالبهای Nuclei مجهز به هوش مصنوعی ما امتحان کنید؛** [**`اینجا کلیک کنید`**](https://cloud.projectdiscovery.io/templates).
|
||||
|
||||
قالبهای Nuclei روشی ساده و یکپارچه برای شناسایی و گزارش آسیبپذیریها ارائه میدهند و جزئیات ضروری مانند سطح شدت و روشهای شناسایی را در بر میگیرند. این ابزار متنباز که توسط جامعه توسعه داده میشود، واکنش به تهدیدات را سرعت میبخشد و در دنیای امنیت سایبری بهطور گسترده شناخته شده است. قالبهای Nuclei بهطور فعال توسط هزاران پژوهشگر امنیتی در سراسر جهان توسعه داده میشوند. ما دو برنامه برای مشارکتکنندگان خود داریم: [**`Pioneers`**](https://projectdiscovery.io/pioneers) و [**`💎 پاداشها`**](https://github.com/projectdiscovery/nuclei-templates/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%92%8E%20Bounty%22).
|
||||
|
||||
<p align="right">
|
||||
<a href="/static/nuclei-templates-teamcity.png" target="_blank"><img src="/static/nuclei-templates-teamcity.png" width="1200px" alt="نمونه قالب Nuclei برای شناسایی پیکربندی نادرست TeamCity" /></a>
|
||||
</p>
|
||||
|
||||
### مثالها
|
||||
|
||||
برای مشاهده موارد استفاده و ایدهها، به [**مستندات ما**](https://docs.projectdiscovery.io/templates/introduction) مراجعه کنید.
|
||||
|
||||
| قالب Nuclei | مورد استفاده |
|
||||
| ---: | ---: |
|
||||
| **[CVE-2021-44228 (Log4Shell)](https://cloud.projectdiscovery.io/public/CVE-2021-45046)** | شناسایی CVEهای شناختهشده |
|
||||
| **[Blind SQL Injection via OOB](https://cloud.projectdiscovery.io/public/CVE-2024-22120)** | شناسایی آسیبپذیریهای Out-of-Band |
|
||||
| **[Generic SQL Injection](https://cloud.projectdiscovery.io/public/CVE-2022-34265)** | شناسایی SQL Injection |
|
||||
| **[Reflected XSS Detection](https://cloud.projectdiscovery.io/public/CVE-2023-4173)** | Cross-Site Scripting (XSS) |
|
||||
| **[Default Credentials Check](https://cloud.projectdiscovery.io/public/airflow-default-login)** | رمزهای عبور پیشفرض یا ضعیف |
|
||||
| **[Sensitive File Disclosure](https://cloud.projectdiscovery.io/public/airflow-configuration-exposure)** | فایلهای محرمانه یا افشای دادهها |
|
||||
| **[Open Redirect Detection](https://cloud.projectdiscovery.io/public/open-redirect)** | شناسایی Open Redirect |
|
||||
| **[Subdomain Takeover Templates](https://cloud.projectdiscovery.io/public/azure-takeover-detection)** | شناسایی Subdomain Takeover |
|
||||
| **[Unprotected Jenkins Console](https://cloud.projectdiscovery.io/public/unauthenticated-jenkins)** | پیکربندیهای نادرست امنیتی |
|
||||
| **[SSL Certificate Expiry](https://cloud.projectdiscovery.io/public/expired-ssl)** | پیکربندیهای ضعیف SSL/TLS |
|
||||
| **[Open S3 Bucket Detection](https://cloud.projectdiscovery.io/public/s3-public-read-acp)** | سرویسهای ابری با پیکربندی نادرست |
|
||||
| **[RCE Detection Templates](https://cloud.projectdiscovery.io/public/CVE-2024-29824)** | آسیبپذیریهای اجرای کد از راه دور |
|
||||
| **[Path Traversal Detection](https://cloud.projectdiscovery.io/public/oracle-fatwire-lfi)** | حملات Directory Traversal |
|
||||
| **[Local/Remote File Inclusion](https://cloud.projectdiscovery.io/public/CVE-2023-6977)** | آسیبپذیریهای File Inclusion |
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## ماموریت ما
|
||||
|
||||
اسکنرهای سنتی آسیبپذیری دههها پیش ساخته شدهاند. آنها متنبسته، فوقالعاده کند و وابسته به فروشندگان هستند. مهاجمان امروزی، برخلاف گذشته که بهرهبرداری از آسیبپذیریها سالها طول میکشید، ظرف چند روز پس از انتشار CVEهای جدید، آنها را در سراسر اینترنت بهصورت گسترده مورد سوءاستفاده قرار میدهند. این تغییر، نیازمند رویکردی کاملاً متفاوت برای مقابله با اکسپلویتهای پرطرفدار در اینترنت است.
|
||||
|
||||
ما Nuclei را برای حل این چالش ساختیم. ما کل چارچوب موتور اسکن را متنباز و قابل سفارشیسازی کردیم تا جامعه امنیتی جهانی بتواند با یکدیگر همکاری کرده و با بردارهای حمله و آسیبپذیریهای پرطرفدار در اینترنت مقابله کند. امروزه Nuclei توسط شرکتهای Fortune 500، سازمانهای دولتی و دانشگاهها استفاده و توسعه داده میشود.
|
||||
|
||||
شما نیز میتوانید با مشارکت در کد ما، [**`کتابخانه قالبها`**](https://github.com/projectdiscovery/nuclei-templates)، یا [**`پیوستن به تیم ما`**](https://projectdiscovery.io/) مشارکت کنید.
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
## مشارکتکنندگان :heart:
|
||||
|
||||
از تمام مشارکتکنندگان فوقالعاده [**`جامعه که با ارسال PRها`**](https://github.com/projectdiscovery/nuclei/graphs/contributors) و بهروز نگهداشتن این پروژه از آن حمایت میکنند، سپاسگزاریم. :heart:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/Ice3man543"><img src="https://avatars.githubusercontent.com/u/22318055?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehsandeep"><img src="https://avatars.githubusercontent.com/u/8293321?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Mzack9999"><img src="https://avatars.githubusercontent.com/u/13421144?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/forgedhallpass"><img src="https://avatars.githubusercontent.com/u/13679401?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tarunKoyalwar"><img src="https://avatars.githubusercontent.com/u/45962551?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/manuelbua"><img src="https://avatars.githubusercontent.com/u/819314?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/actions-user"><img src="https://avatars.githubusercontent.com/u/65916846?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dogancanbakir"><img src="https://avatars.githubusercontent.com/u/65292895?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parrasajad"><img src="https://avatars.githubusercontent.com/u/16835787?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vzamanillo"><img src="https://avatars.githubusercontent.com/u/10209695?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShubhamRasal"><img src="https://avatars.githubusercontent.com/u/45902122?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/RamanaReddy0M"><img src="https://avatars.githubusercontent.com/u/90540245?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LuitelSamikshya"><img src="https://avatars.githubusercontent.com/u/85764322?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kchason"><img src="https://avatars.githubusercontent.com/u/1111099?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/pmareke"><img src="https://avatars.githubusercontent.com/u/3502075?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dwisiswant0"><img src="https://avatars.githubusercontent.com/u/25837540?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xm1k3"><img src="https://avatars.githubusercontent.com/u/73166077?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/5amu"><img src="https://avatars.githubusercontent.com/u/39925709?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ehrishirajsharma"><img src="https://avatars.githubusercontent.com/u/35542790?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zerodivisi0n"><img src="https://avatars.githubusercontent.com/u/687694?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/geeknik"><img src="https://avatars.githubusercontent.com/u/466878?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/TerminalFi"><img src="https://avatars.githubusercontent.com/u/32599364?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KaulSe"><img src="https://avatars.githubusercontent.com/u/45340011?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sullo"><img src="https://avatars.githubusercontent.com/u/1474884?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wdahlenburg"><img src="https://avatars.githubusercontent.com/u/4451504?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ghost"><img src="https://avatars.githubusercontent.com/u/10137?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Nishan8583"><img src="https://avatars.githubusercontent.com/u/20457968?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jdk2588"><img src="https://avatars.githubusercontent.com/u/985054?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nothinux"><img src="https://avatars.githubusercontent.com/u/17433202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CodFrm"><img src="https://avatars.githubusercontent.com/u/22783163?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/CasperGN"><img src="https://avatars.githubusercontent.com/u/5549643?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ankh2054"><img src="https://avatars.githubusercontent.com/u/6784287?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revblock"><img src="https://avatars.githubusercontent.com/u/72813848?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/cn-kali-team"><img src="https://avatars.githubusercontent.com/u/30642514?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/EndPositive"><img src="https://avatars.githubusercontent.com/u/25148195?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jimen0"><img src="https://avatars.githubusercontent.com/u/6826244?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xstevens"><img src="https://avatars.githubusercontent.com/u/69216?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mjkim610"><img src="https://avatars.githubusercontent.com/u/17107206?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/organiccrap"><img src="https://avatars.githubusercontent.com/u/376317?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lu4nx"><img src="https://avatars.githubusercontent.com/u/3006875?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/souvikhazra1"><img src="https://avatars.githubusercontent.com/u/13842393?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tovask"><img src="https://avatars.githubusercontent.com/u/22732484?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Marmelatze"><img src="https://avatars.githubusercontent.com/u/199681?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/doug-threatmate"><img src="https://avatars.githubusercontent.com/u/127235272?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/yabeow"><img src="https://avatars.githubusercontent.com/u/21117771?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/olearycrew"><img src="https://avatars.githubusercontent.com/u/6044920?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gano3s"><img src="https://avatars.githubusercontent.com/u/2551605?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alizmhdi"><img src="https://avatars.githubusercontent.com/u/79321261?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hackerpain"><img src="https://avatars.githubusercontent.com/u/61242234?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lc"><img src="https://avatars.githubusercontent.com/u/19563282?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/savushkin-yauheni"><img src="https://avatars.githubusercontent.com/u/5173352?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/74616e696d"><img src="https://avatars.githubusercontent.com/u/97121933?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/edoardottt"><img src="https://avatars.githubusercontent.com/u/35783570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zt2"><img src="https://avatars.githubusercontent.com/u/7644862?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/moonD4rk"><img src="https://avatars.githubusercontent.com/u/24284231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/wk8"><img src="https://avatars.githubusercontent.com/u/2536231?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mikerott"><img src="https://avatars.githubusercontent.com/u/857712?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/toufik-airane"><img src="https://avatars.githubusercontent.com/u/5610269?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hktalent"><img src="https://avatars.githubusercontent.com/u/18223385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jturner"><img src="https://avatars.githubusercontent.com/u/1825202?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/gaby"><img src="https://avatars.githubusercontent.com/u/835733?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vavkamil"><img src="https://avatars.githubusercontent.com/u/47953210?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leonjza"><img src="https://avatars.githubusercontent.com/u/1148127?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mionskowski-form3"><img src="https://avatars.githubusercontent.com/u/91873652?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chenrui333"><img src="https://avatars.githubusercontent.com/u/1580956?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamargus95"><img src="https://avatars.githubusercontent.com/u/77744293?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shashikarsiddharth"><img src="https://avatars.githubusercontent.com/u/60960197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/trypa11"><img src="https://avatars.githubusercontent.com/u/67585616?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Zeokat"><img src="https://avatars.githubusercontent.com/u/1313154?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/alban-stourbe-wmx"><img src="https://avatars.githubusercontent.com/u/159776828?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/anykno"><img src="https://avatars.githubusercontent.com/u/2528207?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ronaudinho"><img src="https://avatars.githubusercontent.com/u/10264710?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/boy-hack"><img src="https://avatars.githubusercontent.com/u/18695984?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iuliu8899"><img src="https://avatars.githubusercontent.com/u/31680027?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/debasishbsws"><img src="https://avatars.githubusercontent.com/u/65381620?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denysvitali-niantic"><img src="https://avatars.githubusercontent.com/u/157139422?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fail-open"><img src="https://avatars.githubusercontent.com/u/72417455?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Xc1Ym"><img src="https://avatars.githubusercontent.com/u/29765332?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/XTeam-Wing"><img src="https://avatars.githubusercontent.com/u/25416365?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Weltolk"><img src="https://avatars.githubusercontent.com/u/40228052?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tonghuaroot"><img src="https://avatars.githubusercontent.com/u/23011166?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/praetorian-thendrickson"><img src="https://avatars.githubusercontent.com/u/69640071?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/S0obi"><img src="https://avatars.githubusercontent.com/u/4180104?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skahn007gl"><img src="https://avatars.githubusercontent.com/u/144735608?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/shouichi"><img src="https://avatars.githubusercontent.com/u/99586?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seb-elttam"><img src="https://avatars.githubusercontent.com/u/111818823?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AdallomRoy"><img src="https://avatars.githubusercontent.com/u/4046118?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rotemreiss"><img src="https://avatars.githubusercontent.com/u/9288082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/oscarintherocks"><img src="https://avatars.githubusercontent.com/u/1785821?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/xxcdd"><img src="https://avatars.githubusercontent.com/u/42600601?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/chen2aaron"><img src="https://avatars.githubusercontent.com/u/9978183?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/voidz0r"><img src="https://avatars.githubusercontent.com/u/1032286?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vince-isec"><img src="https://avatars.githubusercontent.com/u/149686094?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/true13"><img src="https://avatars.githubusercontent.com/u/18207552?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/skhalsa-sigsci"><img src="https://avatars.githubusercontent.com/u/68570441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ShuBo6"><img src="https://avatars.githubusercontent.com/u/41125338?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/seeyarh"><img src="https://avatars.githubusercontent.com/u/16869800?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/securibee"><img src="https://avatars.githubusercontent.com/u/51520913?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sduc"><img src="https://avatars.githubusercontent.com/u/2879617?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/scottdharvey"><img src="https://avatars.githubusercontent.com/u/25498254?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rykkard"><img src="https://avatars.githubusercontent.com/u/51889048?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/monitor403"><img src="https://avatars.githubusercontent.com/u/45124775?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mlec1"><img src="https://avatars.githubusercontent.com/u/42201667?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/meme-lord"><img src="https://avatars.githubusercontent.com/u/17912559?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/LazyMaple"><img src="https://avatars.githubusercontent.com/u/12314941?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/llussy"><img src="https://avatars.githubusercontent.com/u/18432966?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/linchizhen"><img src="https://avatars.githubusercontent.com/u/170242051?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kiokuless"><img src="https://avatars.githubusercontent.com/u/110003596?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Jarnpher553"><img src="https://avatars.githubusercontent.com/u/10233873?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/c-f"><img src="https://avatars.githubusercontent.com/u/35263248?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/hanghuge"><img src="https://avatars.githubusercontent.com/u/166206050?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/galoget"><img src="https://avatars.githubusercontent.com/u/8353133?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/fudancoder"><img src="https://avatars.githubusercontent.com/u/171416994?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/revolunet"><img src="https://avatars.githubusercontent.com/u/124937?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jsoref"><img src="https://avatars.githubusercontent.com/u/2119212?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MachadoOtto"><img src="https://avatars.githubusercontent.com/u/93268441?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jonathanwalker"><img src="https://avatars.githubusercontent.com/u/14978093?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/nHurD"><img src="https://avatars.githubusercontent.com/u/233374?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/jessekelly881"><img src="https://avatars.githubusercontent.com/u/22938931?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/JaneX8"><img src="https://avatars.githubusercontent.com/u/5116641?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/atomiczsec"><img src="https://avatars.githubusercontent.com/u/75549184?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/M-Faheem-Khan"><img src="https://avatars.githubusercontent.com/u/17150767?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/denandz"><img src="https://avatars.githubusercontent.com/u/5291556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/tibbon"><img src="https://avatars.githubusercontent.com/u/82880?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/dany74q"><img src="https://avatars.githubusercontent.com/u/2129762?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0x123456789"><img src="https://avatars.githubusercontent.com/u/36066426?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/danigoland"><img src="https://avatars.githubusercontent.com/u/15079567?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/ChrisMandich"><img src="https://avatars.githubusercontent.com/u/14286797?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/austintraver"><img src="https://avatars.githubusercontent.com/u/25112463?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/socialsister"><img src="https://avatars.githubusercontent.com/u/155628741?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Anemys"><img src="https://avatars.githubusercontent.com/u/51196227?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/andreangelucci"><img src="https://avatars.githubusercontent.com/u/18552197?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/AlexS778"><img src="https://avatars.githubusercontent.com/u/98418121?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/akkuman"><img src="https://avatars.githubusercontent.com/u/7813511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/zrquan"><img src="https://avatars.githubusercontent.com/u/33086594?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1efty"><img src="https://avatars.githubusercontent.com/u/18194777?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/rsrdesarrollo"><img src="https://avatars.githubusercontent.com/u/5142014?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/razin99"><img src="https://avatars.githubusercontent.com/u/44442082?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MetzinAround"><img src="https://avatars.githubusercontent.com/u/65838556?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/vil02"><img src="https://avatars.githubusercontent.com/u/65706193?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mrschyte"><img src="https://avatars.githubusercontent.com/u/8571?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/PeterDaveHello"><img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/parthmalhotra"><img src="https://avatars.githubusercontent.com/u/28601533?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/owenrumney"><img src="https://avatars.githubusercontent.com/u/3049157?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Ovi3"><img src="https://avatars.githubusercontent.com/u/29408109?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/Bisstocuz"><img src="https://avatars.githubusercontent.com/u/42398278?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/daffainfo"><img src="https://avatars.githubusercontent.com/u/36522826?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/mhmdiaa"><img src="https://avatars.githubusercontent.com/u/19687798?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/MiryangJung"><img src="https://avatars.githubusercontent.com/u/48237511?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/0xmin"><img src="https://avatars.githubusercontent.com/u/44919834?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/maikthulhu"><img src="https://avatars.githubusercontent.com/u/680830?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/sttlr"><img src="https://avatars.githubusercontent.com/u/40246850?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/iamRjarpan"><img src="https://avatars.githubusercontent.com/u/45498226?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/leoloobeek"><img src="https://avatars.githubusercontent.com/u/8801754?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KristinnVikar"><img src="https://avatars.githubusercontent.com/u/93918469?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/kant01ne"><img src="https://avatars.githubusercontent.com/u/5072452?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/KeisukeYamashita"><img src="https://avatars.githubusercontent.com/u/23056537?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
<a href="https://github.com/1hehaq"><img src="https://avatars.githubusercontent.com/u/162917546?v=4" width="50" height="50" alt="" style="max-width: 100%;"></a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub>**`nuclei`** تحت [**مجوز MIT**](https://github.com/projectdiscovery/nuclei/blob/main/LICENSE.md) منتشر شده است.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Telusuri [**`dokumentasi Nuclei lengkap di sini`**](https://docs.projectdiscover
|
||||
|
||||
### Instalasi
|
||||
|
||||
`nuclei` memerlukan **go >= 1.27.3** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
`nuclei` memerlukan **go >= 1.24.2** agar dapat diinstal dengan sukses. Jalankan perintah berikut untuk mengambil repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Nuclei の完全な[**`ドキュメントはこちら`**](https://docs.projectdi
|
||||
|
||||
### インストール
|
||||
|
||||
`nuclei` のインストールには **go >= 1.27.3** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
`nuclei` のインストールには **go >= 1.24.2** が必要です。以下のコマンドを実行してリポジトリを取得してください:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Nuclei 전체 [**`문서를 여기에서`**](https://docs.projectdiscovery.io/to
|
||||
|
||||
### 설치
|
||||
|
||||
`nuclei` 설치에는 **go >= 1.27.3** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
`nuclei` 설치에는 **go >= 1.24.2** 가 필요합니다. 다음 명령어를 실행하여 저장소를 가져옵니다:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Acesse a [**`documentação completa do Nuclei aqui`**](https://docs.projectdisc
|
||||
|
||||
### Instalação
|
||||
|
||||
O `nuclei` requer **go >= 1.27.3** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
O `nuclei` requer **go >= 1.24.2** para instalar com sucesso. Execute o seguinte comando para obter o repositório:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
+8
-29
@@ -9,8 +9,7 @@
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_ES.md">`Spanish`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_JP.md">`日本語`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_PT-BR.md">`Portuguese`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a> •
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_FA.md">`فارسی`</a>
|
||||
<a href="https://github.com/projectdiscovery/nuclei/blob/main/README_TR.md">`Türkçe`</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -111,7 +110,7 @@ Nuclei'nin tam [**`dokümantasyonuna buradan`**](https://docs.projectdiscovery.i
|
||||
|
||||
### Kurulum
|
||||
|
||||
`nuclei` yüklemek için **go >= 1.27.3** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
`nuclei` yüklemek için **go >= 1.24.2** gerektirir. Repoyu almak için aşağıdaki komutu çalıştırın:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -141,18 +140,15 @@ Flags:
|
||||
TARGET:
|
||||
-u, -target string[] target URLs/hosts to scan
|
||||
-l, -list string path to file containing a list of target URLs/hosts to scan (one per line)
|
||||
-targets-inline string inline multiline target list (for use in template profiles)
|
||||
-eh, -exclude-hosts string[] hosts to exclude to scan from the input list (ip, cidr, hostname)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-sa, -scan-all-ips scan all the IP's associated with dns record
|
||||
-iv, -ip-version string[] IP version to scan of hostname (4,6) - (default 4)
|
||||
|
||||
TARGET-FORMAT:
|
||||
-im, -input-mode string mode of input file (list, burp, har, jsonl, yaml, openapi, swagger, http) (default "list")
|
||||
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger) (default "list")
|
||||
-ro, -required-only use only required fields in input format when generating requests
|
||||
-sfv, -skip-format-validation skip format validation (like missing vars) when parsing input file
|
||||
-vtt, -vars-text-templating enable text templating for vars in input file (only for yaml input mode)
|
||||
-vfp, -var-file-paths string[] list of yaml file contained vars to inject into yaml input
|
||||
|
||||
TEMPLATES:
|
||||
-nt, -new-templates run only new templates added in latest nuclei-templates release
|
||||
@@ -174,14 +170,6 @@ TEMPLATES:
|
||||
-esc, -enable-self-contained enable loading self-contained templates
|
||||
-egm, -enable-global-matchers enable loading global matchers templates
|
||||
-file enable loading file templates
|
||||
-llm enable llm matchers and extractors (semantic matching)
|
||||
-llm-provider string llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev) (default "openai")
|
||||
-llm-base-url string openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)
|
||||
-llm-model string model used for semantic matching
|
||||
-llm-timeout int time in seconds to wait for a single llm call (default 30)
|
||||
-llm-max-calls int maximum llm calls per scan (0 for unlimited) (default 5000)
|
||||
-llm-concurrency int maximum concurrent llm calls (default 4)
|
||||
-llm-cache cache llm responses within a scan (default true)
|
||||
|
||||
FILTERING:
|
||||
-a, -author string[] templates to run based on authors (comma-separated, file)
|
||||
@@ -217,9 +205,7 @@ OUTPUT:
|
||||
-se, -sarif-export string file to export results in SARIF format
|
||||
-je, -json-export string file to export results in JSON format
|
||||
-jle, -jsonl-export string file to export results in JSONL(ine) format
|
||||
-pe, -pdf-export string file to export results in PDF format
|
||||
-rd, -redact string[] redact given list of keys from query parameter, request header and body
|
||||
-nrd, -no-redact disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted
|
||||
|
||||
CONFIGURATIONS:
|
||||
-config string path to the nuclei configuration file
|
||||
@@ -245,14 +231,13 @@ 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 allow all templates in the run to read any file on the system (not just payloads)
|
||||
-lfa, -allow-local-file-access allows file (payload) access anywhere on the system
|
||||
-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)
|
||||
-sip, -source-ip string source ip address to use for network scan
|
||||
-rsr, -response-size-read int max response size to read in bytes
|
||||
-rss, -response-size-save int max response size to read in bytes (default 1048576)
|
||||
-rqss, -request-size-save int max request body size to save in bytes (0 to save the whole body) (default 32768)
|
||||
-reset reset removes all nuclei configuration and data files (including nuclei-templates)
|
||||
-tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization
|
||||
-hae, -http-api-endpoint string experimental http api endpoint
|
||||
@@ -275,9 +260,6 @@ 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")
|
||||
@@ -295,7 +277,6 @@ 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)
|
||||
-per-host-rate-limit enable per-host rate limiting (global rate limit becomes unlimited when enabled)
|
||||
-rlm, -rate-limit-minute int maximum number of requests to send per minute (DEPRECATED)
|
||||
-bs, -bulk-size int maximum number of hosts to be analyzed in parallel per template (default 25)
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
@@ -320,7 +301,6 @@ OPTIMIZATIONS:
|
||||
-ss, -scan-strategy value strategy to use while scanning(auto/host-spray/template-spray) (default auto)
|
||||
-irt, -input-read-timeout value timeout on input read (default 3m0s)
|
||||
-nh, -no-httpx disable httpx probing for non-url input
|
||||
-preflight-portscan run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)
|
||||
-no-stdin disable stdin processing
|
||||
|
||||
HEADLESS:
|
||||
@@ -353,11 +333,10 @@ DEBUG:
|
||||
-hc, -health-check run diagnostic check up
|
||||
|
||||
UPDATE:
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-utc, -update-templates-channel string nuclei-templates update channel (stable, latest) (default "stable")
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
-up, -update update nuclei engine to the latest released version
|
||||
-ut, -update-templates update nuclei-templates to latest released version
|
||||
-ud, -update-template-dir string custom directory to install / update nuclei-templates
|
||||
-duc, -disable-update-check disable automatic nuclei/templates update check
|
||||
|
||||
HONEYPOT:
|
||||
-hpd, -honeypot-detect detect potential honeypot hosts based on match concentration
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
<!-- Generated by securitycontext.dev. Source HEAD: 8cda90655970270b215749b42ab59ecc38669293. -->
|
||||
# Security Context: projectdiscovery/nuclei @ dev
|
||||
|
||||
> Reference context for editing projectdiscovery/nuclei. Before changing any component below, **preserve the listed guards and do not reintroduce these fixed issues**. Each fingerprint is a regression check, and each shared attack surface is a guard that must keep holding on every path.
|
||||
|
||||
_Analyzed [8cda906559][c-8cda906559] on `dev` · 5234 commits, 574 security-relevant examined · generated 2026-11-07_
|
||||
|
||||
## Recurring Security Patterns
|
||||
|
||||
**12.5% of classified fixes repeat a previously fixed pattern** (2 recurring, 14 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
|
||||
|
||||
## Top Risks
|
||||
|
||||
Components with the strongest concentration of past fixes for one class.
|
||||
|
||||
| # | Class | Component | Severity | Fixes | Evidence |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Code Execution | `pkg/fuzz/execute.go` | high | 2 | [4fed40f75a][c-4fed40f75a], [ccbfb12bd0][c-ccbfb12bd0] |
|
||||
| 2 | SSRF | `pkg/protocols/common/protocolstate/headless.go` | high | 2 | [d43c1f547f][c-d43c1f547f], [d9dabbb4a1][c-d9dabbb4a1] |
|
||||
| 3 | Arbitrary File Access | `v2/pkg/protocols/common/generators/load.go` | high | 0 (1 component gone, not counted) | [e5154d362a][c-e5154d362a] |
|
||||
| 4 | Arbitrary File Access | `pkg/js/libs/mysql/mysql.go` | high | 1 | [634cecbb28][c-634cecbb28] |
|
||||
| 5 | Certificate Validation Bypass | `pkg/reporting/exporters/es/elasticsearch.go` | high | 1 | [82d5a8acc2][c-82d5a8acc2] |
|
||||
| 6 | Code Execution | `pkg/protocols/common/expressions/expressions.go` | high | 1 | [d221732016][c-d221732016] |
|
||||
|
||||
## Fixed-Issue Fingerprints
|
||||
|
||||
Real security fixes mined from git history; use them as regression checks. `verified` fixes are linked to a published advisory, `inferred` fixes were confirmed from the diff evidence alone. Each is identified by its short commit SHA, which the variant-leads report uses to reference it back to this history.
|
||||
|
||||
**By component** (directories with the most fixes):
|
||||
|
||||
- `pkg/fuzz` — 2 fixes: Code Execution (2)
|
||||
- `pkg/protocols/common/protocolstate` — 2 fixes: SSRF (2)
|
||||
|
||||
- [4fed40f75a][c-4fed40f75a] **fix(fuzz): stop re-rendering evaluated template variables (#8057)** — Code Execution, high [CWE-94] · inferred · 2026-11-05
|
||||
`pkg/fuzz/execute.go` · sink `rule.evaluateVarsWithInteractsh` · fix: safe parser configuration
|
||||
- [82d5a8acc2][c-82d5a8acc2] **exporters: verify TLS certificates (#8042)** — Certificate Validation Bypass, high [CWE-295] · inferred · 2026-11-04
|
||||
`pkg/reporting/exporters/es/elasticsearch.go` · sink `DialTLS` · fix: cryptographic verification
|
||||
- [1c0f77d28e][c-1c0f77d28e] **fix(js/net): enforce network policy on proxied target in JS net dial** — SSRF, high [CWE-918] · inferred · 2026-10-18
|
||||
`pkg/js/libs/net/net.go` · sink `dialHTTPProxy` · fix: input validation
|
||||
- [d43c1f547f][c-d43c1f547f] **fix(headless): canonicalize IPv4-mapped IPv6 literals and re-validate dial targets** — SSRF, high [CWE-918] · inferred · 2026-10-13
|
||||
`pkg/protocols/common/protocolstate/headless.go` · sink `net.DialTimeout` · fix: canonicalization before validation
|
||||
- [d9dabbb4a1][c-d9dabbb4a1] **fix(headless): enforce network policy on ws and wss schemes (#7867)** — SSRF, high [CWE-918] · inferred · 2026-10-12
|
||||
`pkg/protocols/common/protocolstate/headless.go` · sink `isValidHost` · fix: canonicalization before validation
|
||||
- [a20aa26024][c-a20aa26024] **Fix pprof/auth exposure on http-api-endpoint and harden file permissions** — Information Disclosure, high [CWE-200] · inferred · 2026-08-03
|
||||
`internal/httpapi/apiendpoint.go` · sink `http.ListenAndServe` · fix: authentication verification
|
||||
- [fac0b7196d][c-fac0b7196d] **feat(templates): require signatures for javascript templates (#7514)** — Signature Verification Bypass, high [CWE-347] · verified (CVE-2026-76819, GHSA-vxg7-f2jj-jmqm) · 2026-07-03
|
||||
`pkg/catalog/loader/loader.go` · sink `t.HasJavascriptRequest` · fix: cryptographic verification
|
||||
- [ccbfb12bd0][c-ccbfb12bd0] **refactor(protocols): template rendering boundary (#7499)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-06-30
|
||||
`pkg/fuzz/execute.go` · sink `expressions.Evaluate` · fix: sandbox isolation
|
||||
- [3a8b65cb45][c-3a8b65cb45] **fix(js): reject unsandboxed ccache writes in krbforge (#7482)** — Sandbox Escape, high [CWE-693] · inferred · 2026-06-24
|
||||
`pkg/js/libs/krbforge/krbforge.go` · sink `OutputFile` · fix: sandbox isolation
|
||||
- [68daef44a7][c-68daef44a7] **fix(js): escape dbname in lib/pq URL (#7479)** — SQL Injection, high [CWE-89] · inferred · 2026-06-23
|
||||
`pkg/js/libs/postgres/postgres.go` · sink `fmt.Sprintf` (library call — the operation around it is the sink) · fix: context-aware output escaping
|
||||
- [634cecbb28][c-634cecbb28] **gate mysql allowAllFiles behind -lfa** — Arbitrary File Access, high [CWE-22] · verified (CVE-2026-76803, GHSA-xhmx-w2j4-rw3q) · 2026-06-19
|
||||
`pkg/js/libs/mysql/mysql.go` · sink `sql.Open` · fix: sandbox isolation
|
||||
- [20cbf0bd73][c-20cbf0bd73] **refactor: use path-aware filesystem containment checks (#7420)** — Path Traversal, high [CWE-22] · inferred · 2026-06-04
|
||||
`pkg/installer/template.go` · sink `filepath.Join` · fix: canonicalization before validation
|
||||
- [6f2ade6a9b][c-6f2ade6a9b] **fix(js): respect \`allow-local-file-access\` in \`require\` (#7332)** — Sandbox Escape, high [CWE-693] · inferred · 2026-04-10
|
||||
`pkg/js/compiler/pool.go` · sink `require.DefaultSourceLoader` · fix: sandbox isolation
|
||||
- [d221732016][c-d221732016] **fix(expressions): only eval template-authored expressions (#7221)** — Code Execution, high [CWE-94] · verified (CVE-2026-76805, GHSA-jpvm-9frm-hjcq) · 2026-03-16
|
||||
`pkg/protocols/common/expressions/expressions.go` · sink `FindExpressions` · fix: allowlist enforcement
|
||||
- [0da993afe6][c-0da993afe6] **Merge commit from fork** — Signature Verification Bypass, medium [CWE-347] · verified (CVE-2024-43405, GHSA-7h5p-mmpp-hgmm) · 2024-08-19
|
||||
`pkg/templates/signer/tmpl_signer.go` · sink `os.ReadFile` · fix: cryptographic verification
|
||||
- [e5154d362a][c-e5154d362a] **fixing payload load (#3927)** — Arbitrary File Access, high [CWE-22] · verified (CVE-2023-37896, GHSA-2xx4-jj5v-6mff) · 2023-07-14
|
||||
`v2/pkg/protocols/common/generators/load.go` · sink `loadPayloads` · fix: sandbox isolation
|
||||
|
||||
## Historical Vulnerability Classes
|
||||
|
||||
| Class | Fixes | Verified | Most recent fix |
|
||||
|---|---|---|---|
|
||||
| Code Execution | 3 | 2 | [2026-11-05][c-4fed40f75a] 4fed40f75a |
|
||||
| SSRF | 3 | 0 | [2026-10-18][c-1c0f77d28e] 1c0f77d28e |
|
||||
| Arbitrary File Access | 1 (1 component gone, not counted) | 2 | [2026-06-19][c-634cecbb28] 634cecbb28 |
|
||||
| Sandbox Escape | 2 | 0 | [2026-06-24][c-3a8b65cb45] 3a8b65cb45 |
|
||||
| Signature Verification Bypass | 2 | 2 | [2026-07-03][c-fac0b7196d] fac0b7196d |
|
||||
| Certificate Validation Bypass | 1 | 0 | [2026-11-04][c-82d5a8acc2] 82d5a8acc2 |
|
||||
| Information Disclosure | 1 | 0 | [2026-08-03][c-a20aa26024] a20aa26024 |
|
||||
| Path Traversal | 1 | 0 | [2026-06-04][c-20cbf0bd73] 20cbf0bd73 |
|
||||
| SQL Injection | 1 | 0 | [2026-06-23][c-68daef44a7] 68daef44a7 |
|
||||
|
||||
## Known Advisories
|
||||
|
||||
### Linked to a fix commit in this history (5)
|
||||
|
||||
- **CVE-2026-76805** / GHSA-jpvm-9frm-hjcq (medium 5.3) CWE-200 — fixed in [ccbfb12bd0][c-ccbfb12bd0], [d221732016][c-d221732016]
|
||||
- Environment Variable Disclosure via Response-Derived Data in DAST/Fuzz Mode. A vulnerability in Nuclei's DAST/fuzz expression evaluation path allows a malicious target server to trigger disclosure of ...
|
||||
- **CVE-2026-76803** / GHSA-xhmx-w2j4-rw3q (medium 5.3) CWE-284 — fixed in [634cecbb28][c-634cecbb28]
|
||||
- Local File Read via MySQL Client Sandbox Bypass. A vulnerability in Nuclei's JavaScript MySQL client library allows arbitrary local file reads that bypass the `-allow-local-file-access` (`-lfa`) sandb...
|
||||
- **CVE-2026-76819** / GHSA-vxg7-f2jj-jmqm (high 8.6) CWE-94,CWE-787 — fixed in [fac0b7196d][c-fac0b7196d]
|
||||
- Arbitrary Code Execution via Goja JavaScript Engine Vulnerability. A vulnerability in the Goja JavaScript engine used by Nuclei's `javascript:` protocol allows arbitrary native code execution on the s...
|
||||
- **CVE-2024-43405** / GHSA-7h5p-mmpp-hgmm (high 7.4) CWE-78 — fixed in [0da993afe6][c-0da993afe6]
|
||||
- Nuclei Template Signature Verification Bypass. ## Summary A vulnerability has been identified in Nuclei's template signature verification system that could allow an attacker to bypass the signature ch...
|
||||
- **CVE-2023-37896** / GHSA-2xx4-jj5v-6mff (medium 5.9) CWE-22 — fixed in [e5154d362a][c-e5154d362a]
|
||||
- Nuclei < 2.9.9 - Sandbox restriction bypass. ## Overview Affected: < 2.9.9 (patched 2.9.9).
|
||||
|
||||
### Without a linked fix commit (6)
|
||||
|
||||
Published elsewhere, fixed before the history window, or not yet linked; full descriptions are in the JSON context.
|
||||
|
||||
| Advisory | Severity | Published | CWE | Summary |
|
||||
|---|---|---|---|---|
|
||||
| CVE-2026-76804 / GHSA-qgw5-7j4f-fg97 | medium 5.5 | 2026-07-16 | CWE-284 | Local File Read via Workflow File-Protocol Gate Bypass. A vulnerability in Nuclei's workfl... |
|
||||
| CVE-2026-76802 / GHSA-jpf4-98qj-qr67 | medium 4.7 | 2026-07-16 | CWE-78 | Arbitrary Command Execution via DAST Code Signature Bypass. A vulnerability in Nuclei's DA... |
|
||||
| CVE-2026-41645 / GHSA-jm34-66cf-qpvr | medium 5.3 | 2026-04-18 | CWE-94 | Environment variable disclosure via Response-Derived DSL Expressions. A vulnerability in N... |
|
||||
| CVE-2026-41646 / GHSA-29rg-wmcw-hpf4 | medium 5.5 | 2026-04-18 | CWE-284 | Local File Read via require() Module Loader Bypass. A vulnerability in Nuclei's JavaScript... |
|
||||
| CVE-2024-40641 / GHSA-c3q9-c27p-cw9h | high 7.4 | 2024-07-17 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Summary... |
|
||||
| CVE-2024-27920 / GHSA-w5wx-6g2r-r78q | high 7.4 | 2024-03-15 | CWE-78 | Unsigned code template execution through workflows in projectdiscovery/nuclei. ### Overvie... |
|
||||
|
||||
[c-8cda906559]: https://github.com/projectdiscovery/nuclei/commit/8cda90655970270b215749b42ab59ecc38669293
|
||||
[c-4fed40f75a]: https://github.com/projectdiscovery/nuclei/commit/4fed40f75a57c7c414aac62d213d772cc3f430d3
|
||||
[c-ccbfb12bd0]: https://github.com/projectdiscovery/nuclei/commit/ccbfb12bd01447ba25f6e755dfb2bee677736da6
|
||||
[c-d43c1f547f]: https://github.com/projectdiscovery/nuclei/commit/d43c1f547f98ac56654f4335ad20af0cca4c229c
|
||||
[c-d9dabbb4a1]: https://github.com/projectdiscovery/nuclei/commit/d9dabbb4a19e897a6bc12334a030a43035b27b73
|
||||
[c-e5154d362a]: https://github.com/projectdiscovery/nuclei/commit/e5154d362af900e83b30fb79028bfab1a7f0c648
|
||||
[c-634cecbb28]: https://github.com/projectdiscovery/nuclei/commit/634cecbb289db494247c04a9c4ec6a113aef3cc0
|
||||
[c-82d5a8acc2]: https://github.com/projectdiscovery/nuclei/commit/82d5a8acc2c756394c7fa92eae08b41330e89d69
|
||||
[c-d221732016]: https://github.com/projectdiscovery/nuclei/commit/d2217320162d5782ca7cb95bef9dda17063818f3
|
||||
[c-1c0f77d28e]: https://github.com/projectdiscovery/nuclei/commit/1c0f77d28ed639a34c2036fa23c7503ffac0bf27
|
||||
[c-a20aa26024]: https://github.com/projectdiscovery/nuclei/commit/a20aa260245e5db889c5a421d4a65aeb2a64bf46
|
||||
[c-fac0b7196d]: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37
|
||||
[c-3a8b65cb45]: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122
|
||||
[c-68daef44a7]: https://github.com/projectdiscovery/nuclei/commit/68daef44a7eafd41c39690924620e418762909ef
|
||||
[c-20cbf0bd73]: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206
|
||||
[c-6f2ade6a9b]: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d
|
||||
[c-0da993afe6]: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a
|
||||
|
||||
---
|
||||
_Fixed-issue fingerprints and advisory matches are derived from git history and published advisories; the risk tables and pattern descriptions above are derived from them and should be verified against the cited commits._
|
||||
@@ -530,16 +530,6 @@ Description of the template.
|
||||
|
||||
You can go in-depth here on what the template actually does.
|
||||
|
||||
Description, impact and remediation may hold {{placeholders}} filled for
|
||||
each finding: named extractors, payloads, variables and DSL helpers
|
||||
(network helpers excluded), and the result fields host, port, ip, url,
|
||||
scheme, path, matched-at, matcher-name and extractor-name, which win over
|
||||
values of the same name. Use join(", ", name) for an extractor with
|
||||
several values, and brackets for a hyphenated name inside a helper:
|
||||
to_upper([matcher-name]). Unknown placeholders stay as written, and so
|
||||
does a field whose expression fails. Global matcher templates match on
|
||||
other templates' responses, so only the result fields fill their text.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -553,10 +543,6 @@ description: Bower is a package manager which stores package information in the
|
||||
description: Subversion ALM for the enterprise before 8.8.2 allows reflected XSS at multiple locations
|
||||
```
|
||||
|
||||
```yaml
|
||||
description: Redis {{version}} on {{host}}:{{port}} accepts the default password
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -573,8 +559,6 @@ Impact of the template.
|
||||
|
||||
You can go in-depth here on impact of the template.
|
||||
|
||||
Supports the same placeholders as description.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -681,8 +665,6 @@ Remediation steps for the template.
|
||||
|
||||
You can go in-depth here on how to mitigate the problem found by this template.
|
||||
|
||||
Supports the same placeholders as description.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
@@ -1027,27 +1009,6 @@ Part Definitions:
|
||||
- <code>all</code> - HTTP response body + headers
|
||||
- <code>cookies_from_response</code> - HTTP response cookies in name:value format
|
||||
- <code>headers_from_response</code> - HTTP response headers in name:value format
|
||||
- <code>tls_version</code> - TLS version negotiated for the HTTP connection
|
||||
- <code>cipher</code> - TLS cipher suite negotiated for the HTTP connection
|
||||
- <code>key_exchange</code> - TLS key exchange group negotiated for the HTTP connection
|
||||
- <code>sni</code> - SNI value used in the TLS handshake
|
||||
- <code>subject_cn</code> - Leaf certificate subject common name
|
||||
- <code>subject_dn</code> - Leaf certificate subject distinguished name
|
||||
- <code>subject_an</code> - Leaf certificate subject alternative names
|
||||
- <code>subject_org</code> - Leaf certificate subject organization
|
||||
- <code>issuer_cn</code> - Leaf certificate issuer common name
|
||||
- <code>issuer_dn</code> - Leaf certificate issuer distinguished name
|
||||
- <code>issuer_org</code> - Leaf certificate issuer organization
|
||||
- <code>serial</code> - Leaf certificate serial number
|
||||
- <code>fingerprint_hash</code> - Leaf certificate fingerprint hashes (md5/sha1/sha256)
|
||||
- <code>not_before</code> - Leaf certificate not-before timestamp
|
||||
- <code>not_after</code> - Leaf certificate not-after timestamp
|
||||
- <code>expired</code> - Whether the leaf certificate has expired
|
||||
- <code>self_signed</code> - Whether the leaf certificate is self-signed
|
||||
- <code>mismatched</code> - Whether the leaf certificate does not match the SNI hostname
|
||||
- <code>domains</code> - Deduplicated domains from subject CN and SANs
|
||||
- <code>wildcard_certificate</code> - Whether the leaf certificate is a wildcard certificate
|
||||
- <code>emails</code> - Email addresses embedded in the leaf certificate
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -1496,19 +1457,6 @@ 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>
|
||||
@@ -2494,136 +2442,6 @@ 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 />
|
||||
@@ -2749,8 +2567,6 @@ Enum Values:
|
||||
- <code>dsl</code>
|
||||
|
||||
- <code>xpath</code>
|
||||
|
||||
- <code>llm</code>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -2790,7 +2606,6 @@ Part Definitions:
|
||||
- <code>host</code> - Host is the input to the template
|
||||
- <code>matched</code> - Matched is the input which was matched upon
|
||||
- <code>request</code> - Request contains the DNS request in text format
|
||||
- <code>duration</code> - Protocol operation duration in seconds
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>rcode</code> - Rcode field returned for the DNS request
|
||||
- <code>question</code> - Question contains the DNS question field
|
||||
@@ -3083,16 +2898,6 @@ Enum Values:
|
||||
- <code>ANY</code>
|
||||
|
||||
- <code>SRV</code>
|
||||
|
||||
- <code>RRSIG</code>
|
||||
|
||||
- <code>NSEC</code>
|
||||
|
||||
- <code>DNSKEY</code>
|
||||
|
||||
- <code>NSEC3</code>
|
||||
|
||||
- <code>NSEC3PARAM</code>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -3266,82 +3071,6 @@ NoRecursive specifies whether to not do recursive checks if folders are provided
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>smb-user</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
SMBUser authenticates to remote SMB shares when the file input is a UNC
|
||||
or smb:// path (issue #6142). Guest/anon: empty password.
|
||||
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
```yaml
|
||||
smb-user: auditor
|
||||
```
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>smb-password</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
SMBPassword is the password for SMB file targets.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>smb-domain</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
SMBDomain is the optional NTLM domain / workgroup.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>smb-hash</code> <i>string</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
SMBHash enables pass-the-hash (overrides smb-password when set).
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
<code>smb-port</code> <i>int</i>
|
||||
|
||||
</div>
|
||||
<div class="dt">
|
||||
|
||||
SMBPort overrides the default SMB port (445) for UNC targets.
|
||||
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3380,7 +3109,6 @@ Part Definitions:
|
||||
- <code>matched</code> - Matched is the input which was matched upon
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>request</code> - Network request made from the client
|
||||
- <code>duration</code> - Latest measured operation duration in seconds
|
||||
- <code>body,all,data</code> - Network response received from server (default)
|
||||
- <code>raw</code> - Full Network protocol data
|
||||
|
||||
@@ -3773,10 +3501,7 @@ Part Definitions:
|
||||
- <code>matched</code> - Matched is the input which was matched upon
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>req</code> - Headless request made from the client
|
||||
- <code>duration</code> - Latest measured operation duration in seconds
|
||||
- <code>resp,body,data</code> - Headless response received from client (default)
|
||||
- <code>header,all_headers</code> - Headless response headers
|
||||
- <code>status_code</code> - Status code received from the server
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -4156,7 +3881,6 @@ Part Definitions:
|
||||
- <code>matched</code> - Matched is the input which was matched upon
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>timestamp</code> - Timestamp is the time when the request was made
|
||||
- <code>duration</code> - Protocol operation duration in seconds
|
||||
- <code>response</code> - JSON SSL protocol handshake details
|
||||
- <code>cipher</code> - Cipher is the encryption algorithm used
|
||||
- <code>domains</code> - Domains are the list of domain names in the certificate
|
||||
@@ -4165,7 +3889,6 @@ Part Definitions:
|
||||
- <code>issuer_cn</code> - Issuer CN is the common name of the certificate issuer
|
||||
- <code>issuer_dn</code> - Issuer DN is the distinguished name of the certificate issuer
|
||||
- <code>issuer_org</code> - Issuer organization is the organization of the certificate issuer
|
||||
- <code>key_exchange</code> - Key exchange is the TLS key exchange group negotiated in the handshake
|
||||
- <code>not_after</code> - Timestamp after which the remote cert expires
|
||||
- <code>not_before</code> - Timestamp before which the certificate is not valid
|
||||
- <code>probe_status</code> - Probe status indicates if the probe was successful
|
||||
@@ -4357,7 +4080,6 @@ Part Definitions:
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>success</code> - Success specifies whether websocket connection was successful
|
||||
- <code>request</code> - Websocket request made to the server
|
||||
- <code>duration</code> - Latest measured operation duration in seconds
|
||||
- <code>response</code> - Websocket response received from the server
|
||||
- <code>host</code> - Host is the input to the template
|
||||
- <code>matched</code> - Matched is the input which was matched upon
|
||||
@@ -4532,14 +4254,8 @@ Appears in:
|
||||
|
||||
|
||||
|
||||
Part Definitions:
|
||||
|
||||
|
||||
- <code>type</code> - Type is the type of request made
|
||||
- <code>host</code> - Host is the input to the template
|
||||
- <code>response</code> - WHOIS response data
|
||||
- <code>duration</code> - Protocol operation duration in seconds
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="dd">
|
||||
|
||||
@@ -20,22 +20,17 @@ extend-exclude = [
|
||||
"vendor/",
|
||||
# Go checksum file
|
||||
"go.sum",
|
||||
# Generated history. Commit SHAs are not words.
|
||||
"SECURITY_CONTEXT.md",
|
||||
]
|
||||
|
||||
[default.extend-identifiers]
|
||||
# Go identifiers that cannot be renamed without breaking API
|
||||
MisMatched = "MisMatched"
|
||||
IsMisMatchedCert = "IsMisMatchedCert" # tlsx clients API
|
||||
NoopWriter = "NoopWriter"
|
||||
AllowdTypes = "AllowdTypes"
|
||||
# Deprecated alias kept for backward compatibility
|
||||
ExludedDastTmplStats = "ExludedDastTmplStats"
|
||||
|
||||
[default.extend-words]
|
||||
# SMTP greeting command
|
||||
HELO = "HELO"
|
||||
# External package API (fileutil.IsWriteable)
|
||||
Writeable = "Writeable" # TODO(dwisiswant0): Fix in upstream.
|
||||
# Variable name used for flow annotations
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
func TestAuthValueIsCLIOnly(t *testing.T) {
|
||||
oldConfigHome := xdg.ConfigHome
|
||||
oldConfigDirs := xdg.ConfigDirs
|
||||
t.Cleanup(func() {
|
||||
xdg.ConfigHome = oldConfigHome
|
||||
xdg.ConfigDirs = oldConfigDirs
|
||||
})
|
||||
|
||||
const apiKey = "12345678-1234-1234-1234-123456789012"
|
||||
tests := []struct {
|
||||
name string
|
||||
args func(string) []string
|
||||
file string
|
||||
want string
|
||||
}{
|
||||
{name: "explicit config", args: func(path string) []string { return []string{"-config=" + path} }, file: "auth: true\n"},
|
||||
{name: "profile", args: func(path string) []string { return []string{"-profile=" + path} }, file: "auth: " + apiKey + "\n"},
|
||||
{name: "CLI", args: func(string) []string { return []string{"-auth=" + apiKey} }, want: apiKey},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
xdg.ConfigHome = filepath.Join(dir, "config-home")
|
||||
xdg.ConfigDirs = nil
|
||||
|
||||
selectedPath := filepath.Join(dir, "selected.yaml")
|
||||
if test.file != "" {
|
||||
if err := os.WriteFile(selectedPath, []byte(test.file), 0o600); err != nil {
|
||||
t.Fatalf("write selected configuration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var auth string
|
||||
var selected configuration.Selection
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.DynamicVar(&auth, "auth", "true", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
|
||||
cfg := &catalogconfig.Config{Logger: gologger.DefaultLogger}
|
||||
cfg.SetConfigDir(filepath.Join(xdg.ConfigHome, catalogconfig.BinaryName))
|
||||
warnings, err := configuration.Load(cfg, flagSet, &selected, func(path string) (string, error) {
|
||||
return path, nil
|
||||
}, test.args(selectedPath)...)
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration: %v", err)
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Fatalf("configuration warnings: %v", warnings)
|
||||
}
|
||||
|
||||
if got := cliOnlyDynamicValue(flagSet, "auth", auth); got != test.want {
|
||||
t.Fatalf("auth value = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const headerChildEnv = "NUCLEI_HEADER_CHILD_ARGS"
|
||||
|
||||
// -H values can contain "#", on the command line, in a header file and in the
|
||||
// config file, while lines of a header file that start with "#" are skipped as
|
||||
// comments.
|
||||
func TestHeaderValuesKeepHash(t *testing.T) {
|
||||
if args := os.Getenv(headerChildEnv); args != "" {
|
||||
options = &types.Options{Logger: gologger.DefaultLogger}
|
||||
os.Args = append([]string{os.Args[0]}, strings.Split(args, "\n")...)
|
||||
readConfig()
|
||||
fmt.Printf("headers=%s\n", strings.Join(options.CustomHeaders, "|"))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
headerFile := filepath.Join(root, "headers.txt")
|
||||
require.NoError(t, os.WriteFile(headerFile, []byte("# auth headers\nX-Token: abc#def\nReferer: https://app.example.com/#/dashboard\n"), 0o600))
|
||||
configFile := filepath.Join(root, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(configFile, []byte("header:\n - \"X-Config: cfg#value\"\n"), 0o600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{"cli and header file", []string{"-H", "Cookie: sid=1; color=#ff0000", "-H", headerFile}, "Cookie: sid=1; color=#ff0000|X-Token: abc#def|Referer: https://app.example.com/#/dashboard"},
|
||||
{"config file", []string{"-config", configFile}, "X-Config: cfg#value"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempRoot := filepath.Join(t.TempDir(), "tmp")
|
||||
require.NoError(t, os.Mkdir(tempRoot, 0o700))
|
||||
|
||||
args := append(tt.args, "-disable-update-check")
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestHeaderValuesKeepHash$", "-test.count=1")
|
||||
cmd.Env = isolatedNucleiStorageEnv(root, tempRoot, headerChildEnv+"="+strings.Join(args, "\n"))
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, string(output))
|
||||
require.Contains(t, string(output), "headers="+tt.want+"\n")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,7 @@
|
||||
# severity: low
|
||||
# # duplicate-issue-check (optional) flag to enable duplicate tracking issue check
|
||||
# duplicate-issue-check: false
|
||||
# # duplicate-issue-page-size (optional) sets the number of issues to request per page when searching for duplicates
|
||||
# # (GitLab returns at most 100 issues per page)
|
||||
# # duplicate-issue-page-size (optional) controls how many issues to fetch per page when searching for duplicates
|
||||
# duplicate-issue-page-size: 100
|
||||
# # duplicate-issue-max-pages (optional) limits how many pages to fetch when searching for duplicates (0 = no limit)
|
||||
# duplicate-issue-max-pages: 0
|
||||
|
||||
+291
-233
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -12,11 +11,11 @@ import (
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
_pdcp "github.com/projectdiscovery/nuclei/v3/internal/pdcp"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
"github.com/projectdiscovery/utils/auth/pdcp"
|
||||
"github.com/projectdiscovery/utils/env"
|
||||
_ "github.com/projectdiscovery/utils/pprof"
|
||||
@@ -26,7 +25,6 @@ import (
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/interactsh/pkg/client"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/configuration"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/runner"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
@@ -49,33 +47,27 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
memProfile string // optional profile file path
|
||||
options = &types.Options{}
|
||||
inlineSecretsTempDir string
|
||||
inlineSecretsTempMu sync.Mutex
|
||||
cfgFile string
|
||||
templateProfile string
|
||||
memProfile string // optional profile file path
|
||||
options = &types.Options{}
|
||||
inlineSecretsTempFiles []string
|
||||
)
|
||||
|
||||
// normalizeHeaderValue trims a -H value and drops it when the whole line is a
|
||||
// "#" comment. The goflags default strips inline comments, which cut values
|
||||
// such as "Cookie: color=#fff" at the "#".
|
||||
func normalizeHeaderValue(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "#") {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
options.Logger = gologger.DefaultLogger
|
||||
|
||||
defer cleanupInlineSecretsDirs()
|
||||
defer func() {
|
||||
for _, f := range inlineSecretsTempFiles {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
}()
|
||||
|
||||
// enables CLI specific configs mostly interactive behavior
|
||||
config.CurrentAppMode = config.AppModeCLI
|
||||
|
||||
if err := runner.ConfigureOptions(); err != nil {
|
||||
fatalf("Could not initialize options: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("Could not initialize options: %s\n", err)
|
||||
}
|
||||
_ = readConfig()
|
||||
|
||||
@@ -91,7 +83,7 @@ func main() {
|
||||
templates.UseOptionsForSigner(options)
|
||||
tsigner, err := signer.NewTemplateSigner(nil, nil) // will read from env , config or generate new keys
|
||||
if err != nil {
|
||||
fatalf("Could not initialize signer crypto engine: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("couldn't initialize signer crypto engine: %s\n", err)
|
||||
}
|
||||
|
||||
successCounter := 0
|
||||
@@ -130,7 +122,7 @@ func main() {
|
||||
createProfileFile := func(ext, profileType string) *os.File {
|
||||
f, err := os.Create(memProfile + ext)
|
||||
if err != nil {
|
||||
fatalf("profile: could not create %s profile %q file: %v", profileType, memProfile+ext, err)
|
||||
options.Logger.Fatal().Msgf("profile: could not create %s profile %q file: %v", profileType, f.Name(), err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -144,18 +136,18 @@ func main() {
|
||||
|
||||
// Start tracing
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fatalf("profile: could not start trace: %v", err)
|
||||
options.Logger.Fatal().Msgf("profile: could not start trace: %v", err)
|
||||
}
|
||||
|
||||
// Start CPU profiling
|
||||
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
|
||||
fatalf("profile: could not start CPU profile: %v", err)
|
||||
options.Logger.Fatal().Msgf("profile: could not start CPU profile: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Start heap memory snapshot
|
||||
if err := pprof.WriteHeapProfile(memProfileFile); err != nil {
|
||||
fatalf("profile: could not write memory profile: %v", err)
|
||||
options.Logger.Fatal().Msgf("profile: could not write memory profile: %v", err)
|
||||
}
|
||||
|
||||
pprof.StopCPUProfile()
|
||||
@@ -177,14 +169,14 @@ func main() {
|
||||
|
||||
if options.ScanUploadFile != "" {
|
||||
if err := runner.UploadResultsToCloud(options); err != nil {
|
||||
fatalf("Could not upload scan results to cloud dashboard: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("could not upload scan results to cloud dashboard: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
nucleiRunner, err := runner.New(options)
|
||||
if err != nil {
|
||||
fatalf("Could not create runner: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("Could not create runner: %s\n", err)
|
||||
}
|
||||
if nucleiRunner == nil {
|
||||
return
|
||||
@@ -195,7 +187,7 @@ func main() {
|
||||
cancel := stackMonitor.Start(10 * time.Second)
|
||||
defer cancel()
|
||||
stackMonitor.RegisterCallback(func(dumpID string) error {
|
||||
resumeFileName := types.DefaultCrashResumeFilePath(dumpID)
|
||||
resumeFileName := fmt.Sprintf("crash-resume-file-%s.dump", dumpID)
|
||||
if options.EnableCloudUpload {
|
||||
options.Logger.Info().Msgf("Uploading scan results to cloud...")
|
||||
}
|
||||
@@ -221,7 +213,6 @@ func main() {
|
||||
options.Logger.Info().Msgf("CTRL+C pressed: Exiting\n")
|
||||
if options.DASTServer {
|
||||
nucleiRunner.Close()
|
||||
cleanupInlineSecretsDirs()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -237,15 +228,17 @@ func main() {
|
||||
options.Logger.Error().Msgf("Couldn't create resume file: %s\n", err)
|
||||
}
|
||||
}
|
||||
cleanupInlineSecretsDirs()
|
||||
for _, f := range inlineSecretsTempFiles {
|
||||
_ = os.Remove(f)
|
||||
}
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
if err := nucleiRunner.RunEnumeration(); err != nil {
|
||||
if options.Validate {
|
||||
fatalf("Could not validate templates: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("Could not validate templates: %s\n", err)
|
||||
} else {
|
||||
fatalf("Could not run nuclei: %s\n", err)
|
||||
options.Logger.Fatal().Msgf("Could not run nuclei: %s\n", err)
|
||||
}
|
||||
}
|
||||
nucleiRunner.Close()
|
||||
@@ -255,30 +248,18 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// readConfig defines the cli flags and loads them from the command line and
|
||||
// config files.
|
||||
func readConfig() *goflags.FlagSet {
|
||||
var selectedFiles configuration.Selection
|
||||
|
||||
// when true updates nuclei binary to latest version
|
||||
var updateNucleiBinary bool
|
||||
var disableUpdateCheck bool
|
||||
var resetRequested bool
|
||||
var pdcpAuth string
|
||||
var pdcpauth string
|
||||
var fuzzFlag bool
|
||||
|
||||
// FileStringSliceOptions as set up by runner.ConfigureOptions, but keeping
|
||||
// "#" inside -H values
|
||||
headerSliceOptions := goflags.FileStringSliceOptions
|
||||
headerSliceOptions.Normalize = normalizeHeaderValue
|
||||
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.CaseSensitive = true
|
||||
flagSet.SetDescription(`Nuclei is a fast, template based vulnerability scanner focusing
|
||||
on extensive configurability, massive extensibility and ease of use.`)
|
||||
|
||||
// Shared PD flags (e.g. -max-time soft-kill via interrupt).
|
||||
flagSet.AddCommonFlags()
|
||||
|
||||
/* TODO Important: The defined default values, especially for slice/array types are NOT DEFAULT VALUES, but rather implicit values to which the user input is appended.
|
||||
This can be very confusing and should be addressed
|
||||
*/
|
||||
@@ -287,12 +268,10 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringSliceVarP(&options.Targets, "target", "u", nil, "target URLs/hosts to scan", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVarP(&options.TargetsFilePath, "list", "l", "", "path to file containing a list of target URLs/hosts to scan (one per line)"),
|
||||
flagSet.StringVarP(&options.InlineTargetsList, "targets-inline", "", "", "inline multiline target list (for use in template profiles)"),
|
||||
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileNormalizedOriginalStringSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
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 uses dialer fallback, -sa scans all addresses", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
|
||||
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
|
||||
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("target-format", "Target-Format",
|
||||
@@ -324,14 +303,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVarP(&options.EnableSelfContainedTemplates, "enable-self-contained", "esc", false, "enable loading self-contained templates"),
|
||||
flagSet.BoolVarP(&options.EnableGlobalMatchersTemplates, "enable-global-matchers", "egm", false, "enable loading global matchers templates"),
|
||||
flagSet.BoolVar(&options.EnableFileTemplates, "file", false, "enable loading file templates"),
|
||||
flagSet.BoolVar(&options.EnableLLM, "llm", false, "enable llm matchers and extractors (semantic matching)"),
|
||||
flagSet.StringVar(&options.LLMProvider, "llm-provider", "openai", "llm provider for semantic matching (openai, ollama, llamacpp, vllm, lmstudio, groq, openrouter, together, orcarouter, jev)"),
|
||||
flagSet.StringVar(&options.LLMBaseURL, "llm-base-url", "", "openai-compatible endpoint for semantic matching (overrides -llm-provider, e.g. a local model)"),
|
||||
flagSet.StringVar(&options.LLMModel, "llm-model", "", "model used for semantic matching"),
|
||||
flagSet.IntVar(&options.LLMTimeout, "llm-timeout", 30, "time in seconds to wait for a single llm call"),
|
||||
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",
|
||||
@@ -371,19 +342,18 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVarP(&options.JSONLExport, "jsonl-export", "jle", "", "file to export results in JSONL(ine) format"),
|
||||
flagSet.StringVarP(&options.PDFExport, "pdf-export", "pe", "", "file to export results in PDF format"),
|
||||
flagSet.StringSliceVarP(&options.Redact, "redact", "rd", nil, "redact given list of keys from query parameter, request header and body", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.NoRedact, "no-redact", "nrd", false, "disable the built-in redaction of credential headers (Authorization, Cookie, Set-Cookie, ...) in output; keys given with -redact are still redacted"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("configs", "Configurations",
|
||||
flagSet.StringVar(&selectedFiles.Config, "config", "", "path to the nuclei configuration file"),
|
||||
flagSet.StringVarP(&selectedFiles.Profile, "profile", "tp", "", "template profile config file to run"),
|
||||
flagSet.StringVar(&cfgFile, "config", "", "path to the nuclei configuration file"),
|
||||
flagSet.StringVarP(&templateProfile, "profile", "tp", "", "template profile config file to run"),
|
||||
flagSet.BoolVarP(&options.ListTemplateProfiles, "profile-list", "tpl", false, "list community template profiles"),
|
||||
flagSet.BoolVarP(&options.FollowRedirects, "follow-redirects", "fr", false, "enable following redirects for http templates"),
|
||||
flagSet.BoolVarP(&options.FollowHostRedirects, "follow-host-redirects", "fhr", false, "follow redirects on the same host"),
|
||||
flagSet.IntVarP(&options.MaxRedirects, "max-redirects", "mr", 10, "max number of redirects to follow for http templates"),
|
||||
flagSet.BoolVarP(&options.DisableRedirects, "disable-redirects", "dr", false, "disable redirects for http templates"),
|
||||
flagSet.StringVarP(&options.ReportingConfig, "report-config", "rc", "", "nuclei reporting module configuration file"), // TODO merge into the config file or rename to issue-tracking
|
||||
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", headerSliceOptions),
|
||||
flagSet.StringSliceVarP(&options.CustomHeaders, "header", "H", nil, "custom header/cookie to include in all http request in header:value format (cli, file)", goflags.FileStringSliceOptions),
|
||||
flagSet.RuntimeMapVarP(&options.Vars, "var", "V", nil, "custom vars in key=value format"),
|
||||
flagSet.StringVarP(&options.ResolversFile, "resolvers", "r", "", "file containing resolver list for nuclei"),
|
||||
flagSet.BoolVarP(&options.SystemResolvers, "system-resolvers", "sr", false, "use system DNS resolving as error fallback"),
|
||||
@@ -398,16 +368,14 @@ 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, "allow all templates in the run to read any file on the system (not just payloads)"),
|
||||
flagSet.BoolVarP(&options.AllowLocalFileAccess, "allow-local-file-access", "lfa", false, "allows file (payload) access anywhere on the system"),
|
||||
flagSet.BoolVarP(&options.RestrictLocalNetworkAccess, "restrict-local-network-access", "lna", false, "blocks connections to the local / private network"),
|
||||
flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"),
|
||||
flagSet.StringVarP(&options.AttackType, "attack-type", "at", "", "type of payload combinations to perform (batteringram,pitchfork,clusterbomb)"),
|
||||
flagSet.BoolVarP(&options.PayloadRandomOrder, "payload-random-order", "pro", false, "iterate payload combinations in a random order instead of sequentially"),
|
||||
flagSet.StringVarP(&options.SourceIP, "source-ip", "sip", "", "source ip address to use for network scan"),
|
||||
flagSet.IntVarP(&options.ResponseReadSize, "response-size-read", "rsr", 0, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.ResponseSaveSize, "response-size-save", "rss", unitutils.Mega, "max response size to read in bytes"),
|
||||
flagSet.IntVarP(&options.RequestSaveSize, "request-size-save", "rqss", 32*unitutils.Kilo, "max request body size to save in bytes (0 to save the whole body)"),
|
||||
flagSet.CallbackVar(func() { resetRequested = true }, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.CallbackVar(resetCallback, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
|
||||
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
|
||||
)
|
||||
@@ -431,9 +399,6 @@ 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)"),
|
||||
@@ -453,8 +418,6 @@ 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"),
|
||||
@@ -486,8 +449,6 @@ 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.BoolVarP(&options.AutoCalibrate, "auto-calibrate", "ac", false, "drop HTTP matches on a host's catch-all page (answers missing paths with 2xx/3xx)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("headless", "Headless",
|
||||
@@ -506,7 +467,6 @@ 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"),
|
||||
@@ -525,9 +485,8 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("update", "Update",
|
||||
flagSet.BoolVarP(&updateNucleiBinary, "update", "up", false, "update nuclei engine to the latest released version"),
|
||||
flagSet.BoolVarP(&options.UpdateTemplates, "update-templates", "ut", false, "update nuclei-templates to latest released version"),
|
||||
flagSet.StringVarP(&options.UpdateTemplatesChannel, "update-templates-channel", "utc", installer.TemplatesChannelStable, "nuclei-templates update channel (stable, latest)"),
|
||||
flagSet.StringVarP(&options.NewTemplatesDirectory, "update-template-dir", "ud", "", "custom directory to install / update nuclei-templates"),
|
||||
flagSet.BoolVarP(&disableUpdateCheck, "disable-update-check", "duc", false, "disable automatic nuclei/templates update check"),
|
||||
flagSet.CallbackVarP(disableUpdatesCallback, "disable-update-check", "duc", "disable automatic nuclei/templates update check"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("Honeypot", "Honeypot",
|
||||
@@ -545,7 +504,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("cloud", "Cloud",
|
||||
flagSet.DynamicVar(&pdcpAuth, "auth", "true", "configure projectdiscovery cloud (pdcp) api key"),
|
||||
flagSet.DynamicVar(&pdcpauth, "auth", "true", "configure projectdiscovery cloud (pdcp) api key"),
|
||||
flagSet.StringVarP(&options.TeamID, "team-id", "tid", _pdcp.TeamIDEnv, "upload scan results to given team id (optional)"),
|
||||
flagSet.BoolVarP(&options.EnableCloudUpload, "cloud-upload", "cup", false, "upload scan results to pdcp dashboard [DEPRECATED use -dashboard]"),
|
||||
flagSet.StringVarP(&options.ScanID, "scan-id", "sid", "", "upload scan results to existing scan id (optional)"),
|
||||
@@ -557,6 +516,16 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CreateGroup("Authentication", "Authentication",
|
||||
flagSet.StringSliceVarP(&options.SecretsFile, "secret-file", "sf", nil, "path to config file containing secrets for nuclei authenticated scan", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.BoolVarP(&options.PreFetchSecrets, "prefetch-secrets", "ps", false, "prefetch secrets from the secrets file"),
|
||||
flagSet.StringVarP(&options.AuthLoginURL, "auth-login-url", "alu", "", "login page url for turnkey auto-login authenticated scan"),
|
||||
flagSet.StringVarP(&options.AuthUsername, "auth-username", "au", "", "username for auto-login (-auth-login-url)"),
|
||||
flagSet.StringVarP(&options.AuthPassword, "auth-password", "ap", "", "password for auto-login (-auth-login-url)"),
|
||||
flagSet.StringVarP(&options.AuthUsernameField, "auth-username-field", "auf", "", "override the detected username form field name for auto-login"),
|
||||
flagSet.StringVarP(&options.AuthPasswordField, "auth-password-field", "apf", "", "override the detected password form field name for auto-login"),
|
||||
flagSet.BoolVarP(&options.AuthHeadless, "auth-headless", "ah", false, "drive auto-login through a real browser (JS/SPA/SSO login pages)"),
|
||||
flagSet.StringVarP(&options.AuthRecording, "auth-recording", "ar", "", "path to a chrome devtools recorder json login flow to replay for auto-login"),
|
||||
flagSet.BoolVarP(&options.AuthCapture, "auth-capture", "ac", false, "open a browser to log in manually once, then capture the session for the scan"),
|
||||
flagSet.StringVarP(&options.AuthReauthStatusCodes, "auth-reauth-status-codes", "arsc", "", "comma-separated status codes (e.g. 401,403) that trigger an auto-login re-authentication during the scan"),
|
||||
flagSet.StringVarP(&options.AuthRefreshInterval, "auth-refresh-interval", "ari", "", "re-run the auto-login once the captured session is older than this duration (e.g. 15m)"),
|
||||
)
|
||||
|
||||
flagSet.SetCustomHelpText(`EXAMPLES:
|
||||
@@ -578,32 +547,12 @@ Run nuclei with sorted Markdown outputs (with environment variables):
|
||||
Additional documentation is available at: https://docs.nuclei.sh/getting-started/running
|
||||
`)
|
||||
|
||||
// Nuclei handles config-path compatibility before parsing its layered files.
|
||||
// nuclei has multiple migrations
|
||||
// ex: resume.cfg moved to platform standard cache dir from config dir
|
||||
// ex: config.yaml moved to platform standard config dir from linux specific config dir
|
||||
// and hence it will be attempted in config package during init
|
||||
goflags.DisableAutoConfigMigration = true
|
||||
warnings, err := configuration.Load(config.DefaultConfig, flagSet, &selectedFiles, func(path string) (string, error) {
|
||||
return configuration.ResolveProfilePath(path, configuration.TemplatesDirectory(options, config.DefaultConfig))
|
||||
})
|
||||
|
||||
for _, warning := range warnings {
|
||||
options.Logger.Warning().Msgf("Could not load configuration: %s\n", warning)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fatalf("Could not load configuration: %s\n", err)
|
||||
}
|
||||
pdcpAuth = cliOnlyDynamicValue(flagSet, "auth", pdcpAuth)
|
||||
|
||||
if disableUpdateCheck {
|
||||
config.DefaultConfig.DisableUpdateCheck()
|
||||
}
|
||||
|
||||
if templatesDir, overridden := configuration.TemplatesDirectoryOverride(options); overridden {
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
}
|
||||
|
||||
if resetRequested {
|
||||
resetCallback()
|
||||
}
|
||||
_ = flagSet.Parse()
|
||||
|
||||
// when fuzz flag is enabled, set the dast flag to true
|
||||
if fuzzFlag {
|
||||
@@ -617,13 +566,13 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
|
||||
}
|
||||
|
||||
// api key hierarchy: cli flag > env var > .pdcp/credential file
|
||||
if pdcpAuth == "true" {
|
||||
if pdcpauth == "true" {
|
||||
runner.AuthWithPDCP()
|
||||
} else if len(pdcpAuth) == 36 {
|
||||
} else if len(pdcpauth) == 36 {
|
||||
ph := pdcp.PDCPCredHandler{}
|
||||
if _, err := ph.GetCreds(); err == pdcp.ErrNoCreds {
|
||||
apiServer := env.GetEnvOrDefault("PDCP_API_SERVER", pdcp.DefaultApiServer)
|
||||
if validatedCreds, err := ph.ValidateAPIKey(pdcpAuth, apiServer, config.BinaryName); err == nil {
|
||||
if validatedCreds, err := ph.ValidateAPIKey(pdcpauth, apiServer, config.BinaryName); err == nil {
|
||||
_ = ph.SaveCreds(validatedCreds)
|
||||
}
|
||||
}
|
||||
@@ -634,7 +583,7 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
|
||||
h := &pdcp.PDCPCredHandler{}
|
||||
_, err := h.GetCreds()
|
||||
if err != nil {
|
||||
fatalf("To utilize the `-ai` flag, please configure your API key with the `-auth` flag or set the `PDCP_API_KEY` environment variable")
|
||||
options.Logger.Fatal().Msg("To utilize the `-ai` flag, please configure your API key with the `-auth` flag or set the `PDCP_API_KEY` environment variable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,34 +602,143 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
|
||||
runner.NucleiToolUpdateCallback()
|
||||
}
|
||||
|
||||
tempSecretsDir, err := configuration.ApplyProfile(selectedFiles.Profile, options)
|
||||
if err != nil {
|
||||
fatalf("Could not process template profile: %s\n", err)
|
||||
}
|
||||
if tempSecretsDir != "" {
|
||||
setInlineSecretsTempDir(tempSecretsDir)
|
||||
}
|
||||
|
||||
if options.LeaveDefaultPorts {
|
||||
http.LeaveDefaultPorts = true
|
||||
}
|
||||
if customConfigDir := os.Getenv(config.NucleiConfigDirEnv); customConfigDir != "" {
|
||||
config.DefaultConfig.SetConfigDir(customConfigDir)
|
||||
readFlagsConfig(flagSet)
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
if !fileutil.FileExists(cfgFile) {
|
||||
options.Logger.Fatal().Msgf("given config file '%s' does not exist", cfgFile)
|
||||
}
|
||||
// merge config file with flags
|
||||
if err := flagSet.MergeConfigFile(cfgFile); err != nil {
|
||||
options.Logger.Fatal().Msgf("Could not read config: %s\n", err)
|
||||
}
|
||||
|
||||
if !options.Vars.IsEmpty() {
|
||||
// Maybe we should add vars to the config file as well even if they are set via flags?
|
||||
file, err := os.Open(cfgFile)
|
||||
if err != nil {
|
||||
gologger.Fatal().Msgf("Could not open config file: %s\n", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
data := make(map[string]interface{})
|
||||
err = yaml.NewDecoder(file).Decode(&data)
|
||||
if err != nil {
|
||||
gologger.Fatal().Msgf("Could not decode config file: %s\n", err)
|
||||
}
|
||||
|
||||
variables := data["var"]
|
||||
if variables != nil {
|
||||
if varSlice, ok := variables.([]interface{}); ok {
|
||||
for _, value := range varSlice {
|
||||
if strVal, ok := value.(string); ok {
|
||||
err = options.Vars.Set(strVal)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("Could not set variable from config file: %s\n", err)
|
||||
}
|
||||
} else {
|
||||
gologger.Warning().Msgf("Skipping non-string variable in config: %#v", value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
gologger.Warning().Msgf("No 'var' section found in config file: %s", cfgFile)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
templatesDir := options.NewTemplatesDirectory
|
||||
if templatesDir == "" {
|
||||
templatesDir = os.Getenv(config.NucleiTemplatesDirEnv)
|
||||
}
|
||||
if templatesDir != "" {
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
}
|
||||
|
||||
defaultProfilesPath := filepath.Join(config.DefaultConfig.GetTemplateDir(), "profiles")
|
||||
if templateProfile != "" {
|
||||
if filepath.Ext(templateProfile) == "" {
|
||||
if tp := findProfilePathById(templateProfile, defaultProfilesPath); tp != "" {
|
||||
templateProfile = tp
|
||||
} else {
|
||||
options.Logger.Fatal().Msgf("'%s' is not a profile-id or profile path", templateProfile)
|
||||
}
|
||||
}
|
||||
if !filepath.IsAbs(templateProfile) {
|
||||
if filepath.Dir(templateProfile) == "profiles" {
|
||||
defaultProfilesPath = filepath.Join(config.DefaultConfig.GetTemplateDir())
|
||||
}
|
||||
currentDir, err := os.Getwd()
|
||||
if err == nil && fileutil.FileExists(filepath.Join(currentDir, templateProfile)) {
|
||||
templateProfile = filepath.Join(currentDir, templateProfile)
|
||||
} else {
|
||||
templateProfile = filepath.Join(defaultProfilesPath, templateProfile)
|
||||
}
|
||||
}
|
||||
if !fileutil.FileExists(templateProfile) {
|
||||
options.Logger.Fatal().Msgf("given template profile file '%s' does not exist", templateProfile)
|
||||
}
|
||||
if err := flagSet.MergeConfigFile(templateProfile); err != nil {
|
||||
options.Logger.Fatal().Msgf("Could not read template profile: %s\n", err)
|
||||
}
|
||||
|
||||
// Process inline target list from profile.
|
||||
// Supports both the dedicated targets-inline key and multiline
|
||||
// content in the list key (which normally holds a file path).
|
||||
if options.InlineTargetsList != "" {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(options.InlineTargetsList), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
options.Targets = append(options.Targets, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(options.TargetsFilePath, "\n") {
|
||||
// list key has multiline content, treat as inline targets
|
||||
inlineTargets := strings.Split(strings.TrimSpace(options.TargetsFilePath), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
options.Targets = append(options.Targets, target)
|
||||
}
|
||||
}
|
||||
options.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
// Process inline secrets from profile YAML
|
||||
tempSecretsFile, err := processInlineSecretsFromProfile(templateProfile, options)
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Msgf("Could not process inline secrets: %s\n", err)
|
||||
}
|
||||
if tempSecretsFile != "" {
|
||||
inlineSecretsTempFiles = append(inlineSecretsTempFiles, tempSecretsFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(options.SecretsFile) > 0 {
|
||||
for _, secretFile := range options.SecretsFile {
|
||||
if !fileutil.FileExists(secretFile) {
|
||||
fatalf("Secrets file '%s' does not exist", secretFile)
|
||||
options.Logger.Fatal().Msgf("given secrets file '%s' does not exist", secretFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanupOldResumeFiles()
|
||||
|
||||
return flagSet
|
||||
}
|
||||
|
||||
// cleanupOldResumeFiles cleans up resume files older than 10 days.
|
||||
func cleanupOldResumeFiles() {
|
||||
root := config.DefaultConfig.GetStateDir()
|
||||
root := config.DefaultConfig.GetCacheDir()
|
||||
filter := fileutil.FileFilters{
|
||||
OlderThan: 24 * time.Hour * 10, // cleanup on the 10th day
|
||||
Prefix: "resume-",
|
||||
@@ -688,10 +746,46 @@ func cleanupOldResumeFiles() {
|
||||
_ = fileutil.DeleteFilesOlderThan(root, filter)
|
||||
}
|
||||
|
||||
// readFlagsConfig reads the config file from the default config dir and copies it to the current config dir.
|
||||
func readFlagsConfig(flagset *goflags.FlagSet) {
|
||||
// check if config.yaml file exists
|
||||
defaultCfgFile, err := flagset.GetConfigFilePath()
|
||||
if err != nil {
|
||||
// something went wrong either dir is not readable or something else went wrong upstream in `goflags`
|
||||
// warn and exit in this case
|
||||
options.Logger.Warning().Msgf("Could not read config file: %s\n", err)
|
||||
return
|
||||
}
|
||||
cfgFile := config.DefaultConfig.GetFlagsConfigFilePath()
|
||||
if !fileutil.FileExists(cfgFile) {
|
||||
if !fileutil.FileExists(defaultCfgFile) {
|
||||
// if default config does not exist, warn and exit
|
||||
options.Logger.Warning().Msgf("missing default config file : %s", defaultCfgFile)
|
||||
return
|
||||
}
|
||||
// if does not exist copy it from the default config
|
||||
if err = fileutil.CopyFile(defaultCfgFile, cfgFile); err != nil {
|
||||
options.Logger.Warning().Msgf("Could not copy config file: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// if config file exists, merge it with the default config
|
||||
if err = flagset.MergeConfigFile(cfgFile); err != nil {
|
||||
options.Logger.Warning().Msgf("failed to merge configfile with flags got: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// disableUpdatesCallback disables the update check.
|
||||
func disableUpdatesCallback() {
|
||||
config.DefaultConfig.DisableUpdateCheck()
|
||||
}
|
||||
|
||||
// printVersion prints the nuclei version and exits.
|
||||
func printVersion() {
|
||||
options.Logger.Info().Msgf("Nuclei Engine Version: %s", config.Version)
|
||||
runner.LogDirectoryInfo(options.Logger)
|
||||
options.Logger.Info().Msgf("Nuclei Config Directory: %s", config.DefaultConfig.GetConfigDir())
|
||||
options.Logger.Info().Msgf("Nuclei Cache Directory: %s", config.DefaultConfig.GetCacheDir()) // cache dir contains resume files
|
||||
options.Logger.Info().Msgf("PDCP Directory: %s", pdcp.PDCPDir)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -715,155 +809,119 @@ func printTemplateVersion() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
type resetPath struct {
|
||||
name string
|
||||
path string
|
||||
}
|
||||
|
||||
func resetCallback() {
|
||||
cfg := config.DefaultConfig
|
||||
templateRoot := cfg.TemplatesDirectory
|
||||
templateAction := fmt.Sprintf("4. Active nuclei-templates at %v", templateRoot)
|
||||
warning := fmt.Sprintf(`
|
||||
Using '-reset' will delete Nuclei configuration, state, cache, and template files
|
||||
Using '-reset' will delete all nuclei configurations files and all nuclei-templates
|
||||
|
||||
Following files will be deleted:
|
||||
1. All config files at %v
|
||||
2. All state files (including generated resume state) at %v
|
||||
3. All cache files at %v
|
||||
%s
|
||||
2. All cache files (including resume state) at %v
|
||||
3. All nuclei-templates at %v
|
||||
|
||||
Note: Make sure you have backup of your custom nuclei-templates before proceeding
|
||||
|
||||
`, cfg.GetConfigDir(), cfg.GetStateDir(), cfg.GetCacheDir(), templateAction)
|
||||
`, config.DefaultConfig.GetConfigDir(), config.DefaultConfig.GetCacheDir(), config.DefaultConfig.TemplatesDirectory)
|
||||
options.Logger.Print().Msg(warning)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
fmt.Print("Are you sure you want to continue? [y/n]: ")
|
||||
|
||||
resp, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fatalf("Could not read response: %s", err)
|
||||
options.Logger.Fatal().Msgf("could not read response: %s", err)
|
||||
}
|
||||
|
||||
resp = strings.TrimSpace(resp)
|
||||
if stringsutil.EqualFoldAny(resp, "y", "yes") {
|
||||
break
|
||||
}
|
||||
|
||||
if stringsutil.EqualFoldAny(resp, "n", "no", "") {
|
||||
fmt.Println("Exiting...")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
resetPaths := []resetPath{
|
||||
{name: "config", path: cfg.GetConfigDir()},
|
||||
{name: "state", path: cfg.GetStateDir()},
|
||||
{name: "cache", path: cfg.GetCacheDir()},
|
||||
{name: "templates", path: templateRoot},
|
||||
err := os.RemoveAll(config.DefaultConfig.GetConfigDir())
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Msgf("could not delete config dir: %s", err)
|
||||
}
|
||||
|
||||
if err := removeResetPaths(resetPaths); err != nil {
|
||||
fatalf("Could not reset paths: %s", err)
|
||||
err = os.RemoveAll(config.DefaultConfig.GetCacheDir())
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Msgf("could not delete cache dir: %s", err)
|
||||
}
|
||||
|
||||
options.Logger.Info().Msgf("Successfully reset Nuclei files")
|
||||
err = os.RemoveAll(config.DefaultConfig.TemplatesDirectory)
|
||||
if err != nil {
|
||||
options.Logger.Fatal().Msgf("could not delete templates dir: %s", err)
|
||||
}
|
||||
options.Logger.Info().Msgf("Successfully deleted all nuclei configurations files and nuclei-templates")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func removeResetPaths(paths []resetPath) error {
|
||||
for _, path := range paths {
|
||||
if err := validateResetPath(path.path); err != nil {
|
||||
return fmt.Errorf("error validating %s path: %w", path.name, err)
|
||||
func findProfilePathById(profileId, templatesDir string) string {
|
||||
var profilePath string
|
||||
err := filepath.WalkDir(templatesDir, func(iterItem string, d fs.DirEntry, err error) error {
|
||||
ext := filepath.Ext(iterItem)
|
||||
isYaml := ext == extensions.YAML || ext == extensions.YML
|
||||
if err != nil || d.IsDir() || !isYaml {
|
||||
// skip non yaml files
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if err := os.RemoveAll(path.path); err != nil {
|
||||
return fmt.Errorf("error deleting %s path: %w", path.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResetPath(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("path is empty")
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve %q: %w", path, err)
|
||||
}
|
||||
|
||||
absPath = filepath.Clean(absPath)
|
||||
volume := filepath.VolumeName(absPath)
|
||||
if absPath == filepath.Clean(volume+string(filepath.Separator)) {
|
||||
return fmt.Errorf("path %q is a filesystem root", absPath)
|
||||
}
|
||||
|
||||
relativePath := strings.Trim(filepath.ToSlash(strings.TrimPrefix(absPath, volume)), "/")
|
||||
if relativePath == "" || len(strings.Split(relativePath, "/")) < 2 {
|
||||
return fmt.Errorf("path %q is too broad", absPath)
|
||||
}
|
||||
|
||||
if homeDir, err := os.UserHomeDir(); err == nil && absPath == filepath.Clean(homeDir) {
|
||||
return fmt.Errorf("path %q is the user home directory", absPath)
|
||||
}
|
||||
|
||||
if absPath == filepath.Clean(os.TempDir()) {
|
||||
return fmt.Errorf("path %q is the system temporary directory", absPath)
|
||||
}
|
||||
|
||||
if cwd, err := os.Getwd(); err == nil && absPath == filepath.Clean(cwd) {
|
||||
return fmt.Errorf("path %q is the current working directory", absPath)
|
||||
}
|
||||
|
||||
if resolvedPath, err := filepath.EvalSymlinks(absPath); err == nil && resolvedPath != absPath {
|
||||
if err := validateResetPath(resolvedPath); err != nil {
|
||||
return fmt.Errorf("resolved path %q is unsafe: %w", resolvedPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupInlineSecretsDirs() {
|
||||
inlineSecretsTempMu.Lock()
|
||||
dir := inlineSecretsTempDir
|
||||
inlineSecretsTempDir = ""
|
||||
inlineSecretsTempMu.Unlock()
|
||||
|
||||
if dir != "" {
|
||||
_ = os.RemoveAll(dir)
|
||||
}
|
||||
}
|
||||
|
||||
func setInlineSecretsTempDir(dir string) {
|
||||
inlineSecretsTempMu.Lock()
|
||||
inlineSecretsTempDir = dir
|
||||
inlineSecretsTempMu.Unlock()
|
||||
}
|
||||
|
||||
func cliOnlyDynamicValue(flagSet *goflags.FlagSet, name, value string) string {
|
||||
found := false
|
||||
|
||||
flagSet.CommandLine.Visit(func(current *flag.Flag) {
|
||||
if current.Name == name {
|
||||
found = true
|
||||
if strings.TrimSuffix(filepath.Base(iterItem), ext) == profileId {
|
||||
profilePath = iterItem
|
||||
return fmt.Errorf("FOUND")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil && err.Error() != "FOUND" {
|
||||
options.Logger.Error().Msgf("%s\n", err)
|
||||
}
|
||||
return profilePath
|
||||
}
|
||||
|
||||
if !found {
|
||||
return ""
|
||||
// profileSecrets is a helper struct to extract secrets section from a template profile YAML
|
||||
type profileSecrets struct {
|
||||
Secrets interface{} `yaml:"secrets"`
|
||||
}
|
||||
|
||||
// processInlineSecretsFromProfile parses the profile YAML file for inline secrets
|
||||
// and creates a temporary secrets file compatible with nuclei's auth provider.
|
||||
// Returns the path to the temp file or empty string if no secrets found.
|
||||
func processInlineSecretsFromProfile(profilePath string, options *types.Options) (string, error) {
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read profile file: %w", err)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
var profile profileSecrets
|
||||
if err := yaml.Unmarshal(data, &profile); err != nil {
|
||||
return "", fmt.Errorf("could not parse profile YAML: %w", err)
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
cleanupInlineSecretsDirs()
|
||||
options.Logger.Fatal().Msgf(format, args...)
|
||||
if profile.Secrets == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
secretsData, err := yaml.Marshal(profile.Secrets)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not marshal inline secrets: %w", err)
|
||||
}
|
||||
|
||||
tempDir := filepath.Join(os.TempDir(), "nuclei-secrets")
|
||||
if err := os.MkdirAll(tempDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("could not create temp directory: %w", err)
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp(tempDir, "inline-secrets-*.yaml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not create temp secrets file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tempFile.Close()
|
||||
}()
|
||||
|
||||
if _, err := tempFile.Write(secretsData); err != nil {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempFile.Name())
|
||||
return "", fmt.Errorf("could not write to temp secrets file: %w", err)
|
||||
}
|
||||
|
||||
options.SecretsFile = append(options.SecretsFile, tempFile.Name())
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
@@ -34,15 +34,10 @@ func TestMain(m *testing.M) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Only start the shared httptest for benchmarks so unit tests (e.g. goleak)
|
||||
// are not polluted by a long-lived accept loop.
|
||||
var dummyServer *httptest.Server
|
||||
if isBenchmarkRun() {
|
||||
dummyServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
targetURL = dummyServer.URL
|
||||
}
|
||||
dummyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
targetURL = dummyServer.URL
|
||||
|
||||
// Execute tests
|
||||
|
||||
@@ -50,24 +45,13 @@ func TestMain(m *testing.M) {
|
||||
|
||||
// Tear down
|
||||
|
||||
if dummyServer != nil {
|
||||
dummyServer.Close()
|
||||
}
|
||||
dummyServer.Close()
|
||||
_ = os.RemoveAll(projectPath)
|
||||
_ = os.Unsetenv("DISABLE_STDOUT")
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func isBenchmarkRun() bool {
|
||||
for _, arg := range os.Args[1:] {
|
||||
if strings.HasPrefix(arg, "-test.bench") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getUniqFilename generates a unique filename by appending .N if file exists
|
||||
// Similar to wget's behavior: file.cpu.prof, file.cpu.1.prof, file.cpu.2.prof, etc.
|
||||
func getUniqFilename(basePath string) string {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/runner"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tarunKoyalwar/goleak"
|
||||
)
|
||||
|
||||
const cliLeakTestChildEnv = "NUCLEI_CLI_LEAKTEST_CHILD"
|
||||
|
||||
var cliKnownLeaks = []goleak.Option{
|
||||
goleak.Pretty(),
|
||||
goleak.IgnoreAnyFunction("net/http.(*http2ClientConn).readLoop"),
|
||||
goleak.IgnoreAnyFunction("net/http.(*persistConn).readLoop"),
|
||||
goleak.IgnoreAnyFunction("net/http.(*persistConn).writeLoop"),
|
||||
goleak.IgnoreAnyContainingPkg("github.com/hashicorp/golang-lru/v2/expirable"),
|
||||
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"),
|
||||
}
|
||||
|
||||
// TestCLIRunnerGoroutineLeak runs a minimal CLI-path nuclei scan via internal/runner
|
||||
// and asserts no unexpected goroutine leaks remain after Close.
|
||||
//
|
||||
// The body runs in a child process so shared package state from other tests
|
||||
// does not pollute goleak (same approach as lib/tests SDK leak coverage).
|
||||
func TestCLIRunnerGoroutineLeak(t *testing.T) {
|
||||
if os.Getenv(cliLeakTestChildEnv) == "1" {
|
||||
runCLIRunnerLeakTest(t)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestCLIRunnerGoroutineLeak$", "-test.count=1", "-test.v")
|
||||
cmd.Env = append(os.Environ(), cliLeakTestChildEnv+"=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "CLI leak test child failed:\n%s", string(out))
|
||||
}
|
||||
|
||||
func runCLIRunnerLeakTest(t *testing.T) {
|
||||
gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent)
|
||||
_ = os.Setenv("DISABLE_STDOUT", "true")
|
||||
t.Cleanup(func() { _ = os.Unsetenv("DISABLE_STDOUT") })
|
||||
config.DefaultConfig.DisableUpdateCheck()
|
||||
|
||||
defer func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
goleak.VerifyNone(t, cliKnownLeaks...)
|
||||
}()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
require.True(t, ok)
|
||||
templatePath := filepath.Join(filepath.Dir(thisFile), "testdata", "leaktest", "basic-http.yaml")
|
||||
|
||||
options := getDefaultOptions()
|
||||
options.Targets = []string{server.URL}
|
||||
options.Templates = []string{templatePath}
|
||||
options.NoInteractsh = true
|
||||
options.DisableStdin = true
|
||||
options.BulkSize = 1
|
||||
options.TemplateThreads = 1
|
||||
options.PayloadConcurrency = 1
|
||||
options.ExecutionId = xid.New().String()
|
||||
|
||||
runner.ParseOptions(options)
|
||||
|
||||
nucleiRunner, err := runner.New(options)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, nucleiRunner)
|
||||
defer nucleiRunner.Close()
|
||||
|
||||
require.NoError(t, nucleiRunner.RunEnumeration())
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Ensures nuclei's goflags dependency still exposes AddCommonFlags / -max-time,
|
||||
// which is what cmd/nuclei wires via flagSet.AddCommonFlags().
|
||||
func TestMaxTimeCommonFlagAvailable(t *testing.T) {
|
||||
flagSet := goflags.NewFlagSet()
|
||||
common := flagSet.AddCommonFlags()
|
||||
require.NotNil(t, common)
|
||||
|
||||
require.NoError(t, flagSet.Parse("-max-time", "1h30m"))
|
||||
require.Equal(t, 90*time.Minute, common.MaxTime)
|
||||
|
||||
flagSet = goflags.NewFlagSet()
|
||||
common = flagSet.AddCommonFlags()
|
||||
require.NoError(t, flagSet.Parse("-mt", "45s"))
|
||||
require.Equal(t, 45*time.Second, common.MaxTime)
|
||||
}
|
||||
@@ -1,356 +1,254 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
func TestProcessInlineSecretsFromProfile(t *testing.T) {
|
||||
t.Run("profile with inline secrets", func(t *testing.T) {
|
||||
profileContent := `
|
||||
secrets:
|
||||
static:
|
||||
- type: header
|
||||
domains:
|
||||
- api.example.com
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: test-key-123
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
_ = tmpFile.Close()
|
||||
|
||||
opts := &types.Options{}
|
||||
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}()
|
||||
|
||||
if len(opts.SecretsFile) != 1 {
|
||||
t.Fatalf("expected 1 secrets file, got %d", len(opts.SecretsFile))
|
||||
}
|
||||
|
||||
secretsPath := opts.SecretsFile[0]
|
||||
if filepath.Base(secretsPath) != "inline-secrets.yaml" {
|
||||
t.Errorf("secrets file name = %q, want inline-secrets.yaml", filepath.Base(secretsPath))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(secretsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read generated secrets file: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "x-api-key") {
|
||||
t.Errorf("secrets file should contain header key, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "test-key-123") {
|
||||
t.Errorf("secrets file should contain header value, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "api.example.com") {
|
||||
t.Errorf("secrets file should contain domain, got:\n%s", content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("profile without secrets", func(t *testing.T) {
|
||||
profileContent := `
|
||||
severity:
|
||||
- critical
|
||||
- high
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("could not close temp file: %v", err)
|
||||
}
|
||||
|
||||
opts := &types.Options{}
|
||||
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile should not fail: %v", err)
|
||||
}
|
||||
|
||||
if tempDir != "" {
|
||||
t.Errorf("expected no temporary directory for profile without secrets, got %s", tempDir)
|
||||
}
|
||||
|
||||
if len(opts.SecretsFile) != 0 {
|
||||
t.Errorf("expected 0 secrets files for profile without secrets, got %d", len(opts.SecretsFile))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonexistent profile", func(t *testing.T) {
|
||||
opts := &types.Options{}
|
||||
_, err := processInlineSecretsFromProfile(filepath.Join(t.TempDir(), "nonexistent.yaml"), opts)
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not follow a shared temporary directory symlink", func(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
t.Setenv("TMPDIR", tempRoot)
|
||||
t.Setenv("TMP", tempRoot)
|
||||
t.Setenv("TEMP", tempRoot)
|
||||
|
||||
victimDir := filepath.Join(tempRoot, "victim")
|
||||
if err := os.Mkdir(victimDir, 0o755); err != nil {
|
||||
t.Fatalf("create victim directory: %v", err)
|
||||
}
|
||||
before, err := os.Stat(victimDir)
|
||||
if err != nil {
|
||||
t.Fatalf("stat victim directory before processing: %v", err)
|
||||
}
|
||||
|
||||
sharedPath := filepath.Join(tempRoot, "nuclei-secrets")
|
||||
if err := os.Symlink(victimDir, sharedPath); err != nil {
|
||||
t.Skipf("create directory symlink: %v", err)
|
||||
}
|
||||
|
||||
profilePath := filepath.Join(t.TempDir(), "profile.yaml")
|
||||
writeConfigFile(t, profilePath, "secrets:\n static: []\n")
|
||||
opts := &types.Options{}
|
||||
tempDir, err := processInlineSecretsFromProfile(profilePath, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("process inline secrets: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
|
||||
|
||||
if tempDir == sharedPath {
|
||||
t.Fatalf("inline secrets used shared path %q", sharedPath)
|
||||
}
|
||||
if got := filepath.Dir(opts.SecretsFile[0]); got != tempDir {
|
||||
t.Fatalf("inline secrets file directory = %q, want %q", got, tempDir)
|
||||
}
|
||||
after, err := os.Stat(victimDir)
|
||||
if err != nil {
|
||||
t.Fatalf("stat victim directory after processing: %v", err)
|
||||
}
|
||||
if before.Mode().Perm() != after.Mode().Perm() {
|
||||
t.Fatalf("victim directory mode changed from %o to %o", before.Mode().Perm(), after.Mode().Perm())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInlineTargetsParsing(t *testing.T) {
|
||||
t.Run("multiline list key treated as inline targets", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "example.com\ntest.com\nscanme.sh\n",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
opts.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 3 {
|
||||
t.Fatalf("expected 3 targets, got %d: %v", len(opts.Targets), opts.Targets)
|
||||
}
|
||||
if opts.Targets[0] != "example.com" {
|
||||
t.Errorf("expected first target 'example.com', got '%s'", opts.Targets[0])
|
||||
}
|
||||
if opts.Targets[1] != "test.com" {
|
||||
t.Errorf("expected second target 'test.com', got '%s'", opts.Targets[1])
|
||||
}
|
||||
if opts.Targets[2] != "scanme.sh" {
|
||||
t.Errorf("expected third target 'scanme.sh', got '%s'", opts.Targets[2])
|
||||
}
|
||||
if opts.TargetsFilePath != "" {
|
||||
t.Errorf("TargetsFilePath should be cleared, got '%s'", opts.TargetsFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single line list key remains as file path", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "/path/to/targets.txt",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
t.Error("single-line path should not be treated as inline targets")
|
||||
}
|
||||
|
||||
if opts.TargetsFilePath != "/path/to/targets.txt" {
|
||||
t.Errorf("file path should remain unchanged, got '%s'", opts.TargetsFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inline targets with comments and blank lines", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "example.com\n# this is a comment\n\ntest.com\n",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
opts.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 2 {
|
||||
t.Fatalf("expected 2 targets (comments/blanks filtered), got %d: %v", len(opts.Targets), opts.Targets)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("targets-inline key", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
InlineTargetsList: "host1.com\nhost2.com\nhost3.com",
|
||||
}
|
||||
|
||||
if opts.InlineTargetsList != "" {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.InlineTargetsList), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 3 {
|
||||
t.Fatalf("expected 3 targets from targets-inline, got %d", len(opts.Targets))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInlineSecretsFileFormat(t *testing.T) {
|
||||
// Verify the generated secrets file has the correct YAML structure
|
||||
// that matches what authx.GetAuthDataFromYAML expects
|
||||
profileContent := `
|
||||
secrets:
|
||||
static:
|
||||
- type: header
|
||||
domains:
|
||||
- api.example.com
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer test-token
|
||||
dynamic:
|
||||
- template: oauth-flow.yaml
|
||||
variables:
|
||||
- key: username
|
||||
value: testuser
|
||||
type: cookie
|
||||
domains:
|
||||
- auth.example.com
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-secrets-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("could not close temp file: %v", err)
|
||||
}
|
||||
|
||||
opts := &types.Options{}
|
||||
tempDir, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}()
|
||||
|
||||
data, err := os.ReadFile(opts.SecretsFile[0])
|
||||
if err != nil {
|
||||
t.Fatalf("could not read secrets file: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
// The secrets section should contain static and dynamic at root level,
|
||||
// matching the Authx struct yaml tags
|
||||
if !strings.Contains(content, "static:") {
|
||||
t.Errorf("secrets file should have 'static:' key for Authx compatibility, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "dynamic:") {
|
||||
t.Errorf("secrets file should have 'dynamic:' key for Authx compatibility, got:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyProfileParsesInlineTargets(t *testing.T) {
|
||||
profilePath := filepath.Join(t.TempDir(), "profile.yaml")
|
||||
writeConfigFile(t, profilePath, "severity: [high]\n")
|
||||
options := &types.Options{
|
||||
InlineTargetsList: "inline.example\n# comment\n",
|
||||
TargetsFilePath: "list.example\n\nsecond.example\n",
|
||||
}
|
||||
|
||||
tempSecretsDir, err := ApplyProfile(profilePath, options)
|
||||
if err != nil {
|
||||
t.Fatalf("apply profile: %v", err)
|
||||
}
|
||||
if tempSecretsDir != "" {
|
||||
t.Fatalf("temporary secrets directory = %q, want empty", tempSecretsDir)
|
||||
}
|
||||
want := []string{"inline.example", "list.example", "second.example"}
|
||||
if strings.Join(options.Targets, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("targets = %v, want %v", options.Targets, want)
|
||||
}
|
||||
if options.TargetsFilePath != "" {
|
||||
t.Fatalf("targets file path = %q, want empty", options.TargetsFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProfilePathByID(t *testing.T) {
|
||||
templatesDir := t.TempDir()
|
||||
profilePath := filepath.Join(templatesDir, "profiles", "nested", "recommended.yaml")
|
||||
writeConfigFile(t, profilePath, "severity: [high]\n")
|
||||
|
||||
got, err := ResolveProfilePath("recommended", templatesDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve profile ID: %v", err)
|
||||
}
|
||||
if got != profilePath {
|
||||
t.Fatalf("profile path = %q, want %q", got, profilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplatesDirectoryOverridePrecedence(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
envDir := filepath.Join(dir, "environment")
|
||||
flagDir := filepath.Join(dir, "flag")
|
||||
t.Setenv(catalogconfig.NucleiTemplatesDirEnv, envDir)
|
||||
|
||||
got, overridden := TemplatesDirectoryOverride(&types.Options{NewTemplatesDirectory: flagDir})
|
||||
if !overridden {
|
||||
t.Fatal("flag template directory was not reported as an override")
|
||||
}
|
||||
want, err := filepath.Abs(flagDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve expected path: %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("template directory = %q, want flag path %q", got, want)
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
func TestProcessInlineSecretsFromProfile(t *testing.T) {
|
||||
t.Run("profile with inline secrets", func(t *testing.T) {
|
||||
profileContent := `
|
||||
secrets:
|
||||
static:
|
||||
- type: header
|
||||
domains:
|
||||
- api.example.com
|
||||
headers:
|
||||
- key: x-api-key
|
||||
value: test-key-123
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
_ = tmpFile.Close()
|
||||
|
||||
opts := &types.Options{}
|
||||
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(tempPath)
|
||||
}()
|
||||
|
||||
if len(opts.SecretsFile) != 1 {
|
||||
t.Fatalf("expected 1 secrets file, got %d", len(opts.SecretsFile))
|
||||
}
|
||||
|
||||
secretsPath := opts.SecretsFile[0]
|
||||
if !strings.Contains(secretsPath, "inline-secrets-") {
|
||||
t.Errorf("secrets file path should contain 'inline-secrets-', got %s", secretsPath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(secretsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read generated secrets file: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "x-api-key") {
|
||||
t.Errorf("secrets file should contain header key, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "test-key-123") {
|
||||
t.Errorf("secrets file should contain header value, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "api.example.com") {
|
||||
t.Errorf("secrets file should contain domain, got:\n%s", content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("profile without secrets", func(t *testing.T) {
|
||||
profileContent := `
|
||||
severity:
|
||||
- critical
|
||||
- high
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-profile-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("could not close temp file: %v", err)
|
||||
}
|
||||
|
||||
opts := &types.Options{}
|
||||
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile should not fail: %v", err)
|
||||
}
|
||||
|
||||
if tempPath != "" {
|
||||
t.Errorf("expected empty temp path for profile without secrets, got %s", tempPath)
|
||||
}
|
||||
|
||||
if len(opts.SecretsFile) != 0 {
|
||||
t.Errorf("expected 0 secrets files for profile without secrets, got %d", len(opts.SecretsFile))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonexistent profile", func(t *testing.T) {
|
||||
opts := &types.Options{}
|
||||
_, err := processInlineSecretsFromProfile(filepath.Join(t.TempDir(), "nonexistent.yaml"), opts)
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInlineTargetsParsing(t *testing.T) {
|
||||
t.Run("multiline list key treated as inline targets", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "example.com\ntest.com\nscanme.sh\n",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
opts.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 3 {
|
||||
t.Fatalf("expected 3 targets, got %d: %v", len(opts.Targets), opts.Targets)
|
||||
}
|
||||
if opts.Targets[0] != "example.com" {
|
||||
t.Errorf("expected first target 'example.com', got '%s'", opts.Targets[0])
|
||||
}
|
||||
if opts.Targets[1] != "test.com" {
|
||||
t.Errorf("expected second target 'test.com', got '%s'", opts.Targets[1])
|
||||
}
|
||||
if opts.Targets[2] != "scanme.sh" {
|
||||
t.Errorf("expected third target 'scanme.sh', got '%s'", opts.Targets[2])
|
||||
}
|
||||
if opts.TargetsFilePath != "" {
|
||||
t.Errorf("TargetsFilePath should be cleared, got '%s'", opts.TargetsFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single line list key remains as file path", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "/path/to/targets.txt",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
t.Error("single-line path should not be treated as inline targets")
|
||||
}
|
||||
|
||||
if opts.TargetsFilePath != "/path/to/targets.txt" {
|
||||
t.Errorf("file path should remain unchanged, got '%s'", opts.TargetsFilePath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inline targets with comments and blank lines", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
TargetsFilePath: "example.com\n# this is a comment\n\ntest.com\n",
|
||||
}
|
||||
|
||||
if strings.Contains(opts.TargetsFilePath, "\n") {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.TargetsFilePath), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
opts.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 2 {
|
||||
t.Fatalf("expected 2 targets (comments/blanks filtered), got %d: %v", len(opts.Targets), opts.Targets)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("targets-inline key", func(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
InlineTargetsList: "host1.com\nhost2.com\nhost3.com",
|
||||
}
|
||||
|
||||
if opts.InlineTargetsList != "" {
|
||||
inlineTargets := strings.Split(strings.TrimSpace(opts.InlineTargetsList), "\n")
|
||||
for _, target := range inlineTargets {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
opts.Targets = append(opts.Targets, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(opts.Targets) != 3 {
|
||||
t.Fatalf("expected 3 targets from targets-inline, got %d", len(opts.Targets))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInlineSecretsFileFormat(t *testing.T) {
|
||||
// Verify the generated secrets file has the correct YAML structure
|
||||
// that matches what authx.GetAuthDataFromYAML expects
|
||||
profileContent := `
|
||||
secrets:
|
||||
static:
|
||||
- type: header
|
||||
domains:
|
||||
- api.example.com
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer test-token
|
||||
dynamic:
|
||||
- template: oauth-flow.yaml
|
||||
variables:
|
||||
- key: username
|
||||
value: testuser
|
||||
type: cookie
|
||||
domains:
|
||||
- auth.example.com
|
||||
`
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-secrets-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpFile.WriteString(profileContent); err != nil {
|
||||
t.Fatalf("could not write profile: %v", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
t.Fatalf("could not close temp file: %v", err)
|
||||
}
|
||||
|
||||
opts := &types.Options{}
|
||||
tempPath, err := processInlineSecretsFromProfile(tmpFile.Name(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("processInlineSecretsFromProfile failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(tempPath)
|
||||
}()
|
||||
|
||||
data, err := os.ReadFile(opts.SecretsFile[0])
|
||||
if err != nil {
|
||||
t.Fatalf("could not read secrets file: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
// The secrets section should contain static and dynamic at root level,
|
||||
// matching the Authx struct yaml tags
|
||||
if !strings.Contains(content, "static:") {
|
||||
t.Errorf("secrets file should have 'static:' key for Authx compatibility, got:\n%s", content)
|
||||
}
|
||||
if !strings.Contains(content, "dynamic:") {
|
||||
t.Errorf("secrets file should have 'dynamic:' key for Authx compatibility, got:\n%s", content)
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
const inlineSecretsFatalChildEnv = "NUCLEI_INLINE_SECRETS_FATAL_CHILD"
|
||||
|
||||
func TestValidateResetPathRejectsBroadTargets(t *testing.T) {
|
||||
if err := validateResetPath(""); err == nil {
|
||||
t.Fatal("empty reset path was accepted")
|
||||
}
|
||||
if err := validateResetPath(string(filepath.Separator)); err == nil {
|
||||
t.Fatal("filesystem root reset path was accepted")
|
||||
}
|
||||
if err := validateResetPath(filepath.Join(string(filepath.Separator), "var")); err == nil {
|
||||
t.Fatal("broad top-level reset path was accepted")
|
||||
}
|
||||
if err := validateResetPath(os.TempDir()); err == nil {
|
||||
t.Fatal("system temporary directory reset path was accepted")
|
||||
}
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
if err := validateResetPath(cwd); err == nil {
|
||||
t.Fatal("current working directory reset path was accepted")
|
||||
}
|
||||
}
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
if err := validateResetPath(homeDir); err == nil {
|
||||
t.Fatal("user home reset path was accepted")
|
||||
}
|
||||
}
|
||||
if err := validateResetPath(t.TempDir()); err != nil {
|
||||
t.Fatalf("scoped reset path was rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveResetPathsValidatesAllTargetsBeforeDeletion(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
configDir := filepath.Join(root, "config")
|
||||
if err := os.Mkdir(configDir, 0o700); err != nil {
|
||||
t.Fatalf("create config directory: %v", err)
|
||||
}
|
||||
|
||||
paths := []resetPath{
|
||||
{name: "config", path: configDir},
|
||||
{name: "unsafe", path: string(filepath.Separator)},
|
||||
}
|
||||
if err := removeResetPaths(paths); err == nil {
|
||||
t.Fatal("unsafe reset target was accepted")
|
||||
}
|
||||
if _, err := os.Stat(configDir); err != nil {
|
||||
t.Fatalf("config directory was removed before validation completed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupInlineSecretsDirsRemovesPrivateDirectory(t *testing.T) {
|
||||
secretsDir := filepath.Join(t.TempDir(), "nuclei-secrets-run")
|
||||
if err := os.Mkdir(secretsDir, 0o700); err != nil {
|
||||
t.Fatalf("create inline secrets directory: %v", err)
|
||||
}
|
||||
secretsPath := filepath.Join(secretsDir, "inline-secrets.yaml")
|
||||
if err := os.WriteFile(secretsPath, []byte("static: []\n"), 0o600); err != nil {
|
||||
t.Fatalf("write inline secrets file: %v", err)
|
||||
}
|
||||
|
||||
inlineSecretsTempMu.Lock()
|
||||
previous := inlineSecretsTempDir
|
||||
inlineSecretsTempDir = secretsDir
|
||||
inlineSecretsTempMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
inlineSecretsTempMu.Lock()
|
||||
inlineSecretsTempDir = previous
|
||||
inlineSecretsTempMu.Unlock()
|
||||
})
|
||||
|
||||
cleanupInlineSecretsDirs()
|
||||
if _, err := os.Stat(secretsDir); !os.IsNotExist(err) {
|
||||
t.Fatalf("inline secrets directory still exists: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanupInlineSecretsDirsIsConcurrentSafe(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "nuclei-secrets")
|
||||
if err := os.Mkdir(dir, 0o700); err != nil {
|
||||
t.Fatalf("create inline secrets directory: %v", err)
|
||||
}
|
||||
|
||||
inlineSecretsTempMu.Lock()
|
||||
previous := inlineSecretsTempDir
|
||||
inlineSecretsTempDir = dir
|
||||
inlineSecretsTempMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
inlineSecretsTempMu.Lock()
|
||||
inlineSecretsTempDir = previous
|
||||
inlineSecretsTempMu.Unlock()
|
||||
})
|
||||
|
||||
start := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
for range 8 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
cleanupInlineSecretsDirs()
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
t.Fatalf("inline secrets directory still exists: %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfigFatalCleansInlineSecretsDirectory(t *testing.T) {
|
||||
if os.Getenv(inlineSecretsFatalChildEnv) == "1" {
|
||||
options = &types.Options{Logger: gologger.DefaultLogger}
|
||||
os.Args = []string{
|
||||
os.Args[0],
|
||||
"-profile", os.Getenv("NUCLEI_TEST_PROFILE"),
|
||||
"-secret-file", os.Getenv("NUCLEI_TEST_MISSING_SECRET"),
|
||||
"-disable-update-check",
|
||||
}
|
||||
readConfig()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
profilePath := filepath.Join(root, "profile.yaml")
|
||||
if err := os.WriteFile(profilePath, []byte("secrets:\n static: []\n"), 0o600); err != nil {
|
||||
t.Fatalf("write profile: %v", err)
|
||||
}
|
||||
missingSecret := filepath.Join(root, "missing-secrets.yaml")
|
||||
tempRoot := filepath.Join(root, "tmp")
|
||||
if err := os.Mkdir(tempRoot, 0o700); err != nil {
|
||||
t.Fatalf("create temporary root: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestReadConfigFatalCleansInlineSecretsDirectory$", "-test.count=1")
|
||||
cmd.Env = isolatedNucleiStorageEnv(root, tempRoot,
|
||||
inlineSecretsFatalChildEnv+"=1",
|
||||
"NUCLEI_TEST_PROFILE="+profilePath,
|
||||
"NUCLEI_TEST_MISSING_SECRET="+missingSecret,
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatal("child process did not exit on the missing secrets file")
|
||||
}
|
||||
if !bytes.Contains(output, []byte("Secrets file")) || !bytes.Contains(output, []byte("does not exist")) {
|
||||
t.Fatalf("child did not reach the missing secrets file failure:\n%s", output)
|
||||
}
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(tempRoot, "nuclei-secrets-*"))
|
||||
if err != nil {
|
||||
t.Fatalf("list inline secrets directories: %v", err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("inline secrets directories remain after fatal exit: %v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
func isolatedNucleiStorageEnv(root, tempRoot string, extra ...string) []string {
|
||||
env := append(os.Environ(),
|
||||
"TMPDIR="+tempRoot,
|
||||
"TMP="+tempRoot,
|
||||
"TEMP="+tempRoot,
|
||||
"XDG_CONFIG_HOME="+filepath.Join(root, "config"),
|
||||
"XDG_STATE_HOME="+filepath.Join(root, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(root, "cache"),
|
||||
"XDG_DATA_HOME="+filepath.Join(root, "data"),
|
||||
"NUCLEI_CONFIG_DIR="+filepath.Join(root, "config", "nuclei"),
|
||||
)
|
||||
return append(env, extra...)
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
id: leaktest-basic-http
|
||||
|
||||
info:
|
||||
name: CLI Leak Test Basic HTTP
|
||||
author: pdteam
|
||||
severity: info
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/"
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 204
|
||||
@@ -1,11 +1,45 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.27.3
|
||||
go 1.25.7
|
||||
|
||||
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.68
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectdiscovery/clistats v0.1.4
|
||||
github.com/projectdiscovery/fastdialer v0.5.12
|
||||
github.com/projectdiscovery/hmap v0.0.101
|
||||
github.com/projectdiscovery/interactsh v1.3.1
|
||||
github.com/projectdiscovery/rawhttp v0.1.90
|
||||
github.com/projectdiscovery/retryabledns v1.0.115
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.17
|
||||
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.55.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/text v0.37.0
|
||||
)
|
||||
|
||||
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
|
||||
@@ -13,205 +47,141 @@ 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/v2 v2.27.0
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
github.com/alitto/pond v1.9.2
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.7
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/antchfx/xpath v1.3.9
|
||||
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.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/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/service/s3 v1.99.0
|
||||
github.com/bytedance/sonic v1.15.3
|
||||
github.com/bytedance/sonic v1.15.0
|
||||
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/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
|
||||
github.com/elazarl/goproxy v1.7.2
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fullstorydev/grpcurl v1.9.3
|
||||
github.com/getkin/kin-openapi v0.149.0
|
||||
github.com/getsops/sops/v3 v3.9.2
|
||||
github.com/go-echarts/go-echarts/v2 v2.6.0
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/getkin/kin-openapi v0.132.0
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
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/go-sql-driver/mysql v1.9.3
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/go-github/v92 v92.0.0
|
||||
github.com/google/go-github/v30 v30.1.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/klauspost/compress v1.18.7
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/logrusorgru/aurora/v4 v4.0.0
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
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/openrdap/rdap v0.9.1
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/clistats v0.1.7
|
||||
github.com/projectdiscovery/dsl v0.9.0
|
||||
github.com/projectdiscovery/fastdialer v0.6.0
|
||||
github.com/projectdiscovery/dsl v0.8.20
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/goflags v0.2.1
|
||||
github.com/projectdiscovery/goimpacket v0.1.1-0.20261007205806-6042a8213eeb
|
||||
github.com/projectdiscovery/goja v0.0.0-20261012075649-9b78f2e05b9c
|
||||
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb
|
||||
github.com/projectdiscovery/goflags v0.1.74
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d
|
||||
github.com/projectdiscovery/gologger v1.1.73
|
||||
github.com/projectdiscovery/gologger v1.1.71
|
||||
github.com/projectdiscovery/gostruct v0.0.2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20261102102118-9294133b15a2
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
|
||||
github.com/projectdiscovery/hmap v0.0.102
|
||||
github.com/projectdiscovery/httpx v1.12.0
|
||||
github.com/projectdiscovery/interactsh v1.4.1
|
||||
github.com/projectdiscovery/httpx v1.9.0
|
||||
github.com/projectdiscovery/mapcidr v1.1.97
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
|
||||
github.com/projectdiscovery/networkpolicy v0.1.56
|
||||
github.com/projectdiscovery/ratelimit v0.0.94
|
||||
github.com/projectdiscovery/rawhttp v0.2.0
|
||||
github.com/projectdiscovery/networkpolicy v0.1.42
|
||||
github.com/projectdiscovery/ratelimit v0.0.88
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.116
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.31
|
||||
github.com/projectdiscovery/sarif v0.1.0
|
||||
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
|
||||
github.com/projectdiscovery/tlsx v1.2.2
|
||||
github.com/projectdiscovery/uncover v1.2.1
|
||||
github.com/projectdiscovery/useragent v0.0.109
|
||||
github.com/projectdiscovery/utils v0.11.13-0.20261103094816-5c0b20e35350
|
||||
github.com/projectdiscovery/wappalyzergo v0.3.6
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.7
|
||||
github.com/redis/go-redis/v9 v9.19.0
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/projectdiscovery/useragent v0.0.108
|
||||
github.com/projectdiscovery/utils v0.11.1
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.88
|
||||
github.com/redis/go-redis/v9 v9.11.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/spaolacci/murmur3 v1.1.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/syndtr/goleveldb v1.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
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.59.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.23.0
|
||||
golang.org/x/sys v0.48.0
|
||||
golang.org/x/term v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
golang.org/x/tools v0.49.0
|
||||
google.golang.org/grpc v1.83.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.43.0
|
||||
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
|
||||
filippo.io/edwards25519 v1.1.1 // 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/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/BurntSushi/toml v1.4.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.13.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.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
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // 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.18.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // 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.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // 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/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/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/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
|
||||
github.com/brianvoe/gofakeit/v7 v7.2.1 // indirect
|
||||
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.2 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.3 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.5 // 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/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
|
||||
@@ -222,92 +192,67 @@ require (
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.5 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.4 // 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
|
||||
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect
|
||||
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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
|
||||
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
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.30.0 // indirect
|
||||
github.com/gaissmai/bart v0.28.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
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-pg/zerochecker v0.2.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
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.3 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // 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.9.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/api v1.15.0 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
|
||||
github.com/hdm/jarm-go v0.0.7 // 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
|
||||
@@ -319,46 +264,45 @@ 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/josharian/intern v1.0.0 // 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
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // 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.6 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.3 // 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
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/oasdiff/yaml v0.1.1 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.14 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect
|
||||
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
@@ -366,34 +310,31 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.3.6 // indirect
|
||||
github.com/openrdap/rdap v0.9.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.23 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/projectdiscovery/asnmap v1.1.1 // indirect
|
||||
github.com/projectdiscovery/blackrock v0.0.2 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.3.2 // indirect
|
||||
github.com/projectdiscovery/blackrock v0.0.1 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.2.43 // 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.3 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/sashabaranov/go-openai v1.37.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // 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/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // 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
|
||||
@@ -409,14 +350,12 @@ 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.4-0.20260918082853-3c3d7dc4a0cb // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
@@ -428,46 +367,59 @@ 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 v1.7.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zcalusic/sysinfo v1.1.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
github.com/zmap/zcrypto v0.0.0-20261015050653-aa1de3126e87 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
google.golang.org/api v0.264.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/goburrow/cache v0.1.4 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/go-querystring v1.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.52.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.45.0
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
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
-1
@@ -1,6 +1,6 @@
|
||||
module nuclei-tools
|
||||
|
||||
go 1.27.1
|
||||
go 1.26.1
|
||||
|
||||
tool (
|
||||
github.com/dvyukov/go-fuzz/go-fuzz
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/projectdiscovery/goflags"
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
// Selection contains the config and profile selected for one invocation.
|
||||
type Selection struct {
|
||||
Config string
|
||||
Profile string
|
||||
}
|
||||
|
||||
// Load applies automatic config files, one selected config file, and one
|
||||
// selected profile in priority order. Explicit CLI flags retain precedence.
|
||||
func Load(
|
||||
cfg *catalogconfig.Config,
|
||||
flagSet *goflags.FlagSet,
|
||||
selected *Selection,
|
||||
resolveProfile func(string) (string, error),
|
||||
args ...string,
|
||||
) ([]error, error) {
|
||||
userConfigFile := cfg.GetFlagsConfigFilePath()
|
||||
defaultUserConfigFile := filepath.Join(xdg.ConfigHome, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
automaticFiles := automaticConfigFiles(xdg.ConfigDirs, runtime.GOOS, userConfigFile)
|
||||
|
||||
return loadConfigFiles(userConfigFile, defaultUserConfigFile, automaticFiles, flagSet, selected, resolveProfile, args...)
|
||||
}
|
||||
|
||||
func loadConfigFiles(
|
||||
userConfigFile string,
|
||||
defaultUserConfigFile string,
|
||||
automaticFiles []string,
|
||||
flagSet *goflags.FlagSet,
|
||||
selected *Selection,
|
||||
resolveProfile func(string) (string, error),
|
||||
args ...string,
|
||||
) ([]error, error) {
|
||||
|
||||
var warnings []error
|
||||
if err := prepareUserConfigFile(userConfigFile, defaultUserConfigFile); err != nil {
|
||||
warnings = append(warnings, err)
|
||||
}
|
||||
|
||||
flagSet.SetConfigFilePaths(automaticFiles...)
|
||||
if err := flagSet.Parse(args...); err != nil {
|
||||
warnings = append(warnings, fmt.Errorf("automatic configuration was ignored: %w", err))
|
||||
}
|
||||
|
||||
if err := mergeSelectedFiles(flagSet, selected, resolveProfile); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
func mergeSelectedFiles(
|
||||
flagSet *goflags.FlagSet,
|
||||
selected *Selection,
|
||||
resolveProfile func(string) (string, error),
|
||||
) error {
|
||||
configFile := selected.Config
|
||||
if configFile != "" {
|
||||
if err := flagSet.MergeConfigFile(configFile); err != nil {
|
||||
return fmt.Errorf("read config file %q: %w", configFile, err)
|
||||
}
|
||||
|
||||
// A selected config may choose a profile, but it cannot redirect its
|
||||
// completed config stage.
|
||||
selected.Config = configFile
|
||||
}
|
||||
|
||||
if selected.Profile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
profileFile, err := resolveProfile(selected.Profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := flagSet.MergeConfigFile(profileFile); err != nil {
|
||||
return fmt.Errorf("read template profile %q: %w", profileFile, err)
|
||||
}
|
||||
|
||||
// A profile cannot redirect either completed file-selection stage.
|
||||
selected.Config = configFile
|
||||
selected.Profile = profileFile
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func automaticConfigFiles(configDirs []string, goos, userConfigFile string) []string {
|
||||
highestFirst := make([]string, 0, len(configDirs)+2)
|
||||
highestFirst = append(highestFirst, userConfigFile)
|
||||
|
||||
if unixSystemFile := unixSystemConfigFilePath(goos); unixSystemFile != "" {
|
||||
highestFirst = append(highestFirst, unixSystemFile)
|
||||
}
|
||||
|
||||
for _, configDir := range configDirs {
|
||||
if filepath.IsAbs(configDir) {
|
||||
highestFirst = append(highestFirst, filepath.Join(configDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName))
|
||||
}
|
||||
}
|
||||
|
||||
files := make([]string, 0, len(highestFirst))
|
||||
seen := make(map[string]struct{}, len(highestFirst))
|
||||
|
||||
for _, file := range highestFirst {
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cleanFile := filepath.Clean(file)
|
||||
if _, exists := seen[cleanFile]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[cleanFile] = struct{}{}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
slices.Reverse(files)
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func unixSystemConfigFilePath(goos string) string {
|
||||
switch goos {
|
||||
case "aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
|
||||
return path.Join("/etc", catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func prepareUserConfigFile(configFile, defaultConfigFile string) error {
|
||||
if filepath.Clean(configFile) == filepath.Clean(defaultConfigFile) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configFile); err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("inspect custom config file %q: %w", configFile, err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(defaultConfigFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read default config file %q: %w", defaultConfigFile, err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
|
||||
return fmt.Errorf("create custom config directory %q: %w", filepath.Dir(configFile), err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(configFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("create custom config file %q: %w", configFile, err)
|
||||
}
|
||||
|
||||
written := false
|
||||
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
if !written {
|
||||
_ = os.Remove(configFile)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := file.Write(data); err != nil {
|
||||
return fmt.Errorf("copy default config from %q to %q: %w", defaultConfigFile, configFile, err)
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
return fmt.Errorf("close custom config file %q: %w", configFile, err)
|
||||
}
|
||||
|
||||
written = true
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,443 +0,0 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
func TestConfigurationPrecedence(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
preferredSystemDir := filepath.Join(dir, "preferred-system")
|
||||
fallbackSystemDir := filepath.Join(dir, "fallback-system")
|
||||
preferredSystemPath := filepath.Join(preferredSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
fallbackSystemPath := filepath.Join(fallbackSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
unixSystemPath := filepath.Join(dir, "etc", catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
explicitPath := filepath.Join(dir, "explicit.yaml")
|
||||
profilePath := filepath.Join(dir, "profile.yaml")
|
||||
|
||||
writeConfigFile(t, fallbackSystemPath, `
|
||||
fallback-only: fallback
|
||||
preferred-wins: fallback
|
||||
local-wins: fallback
|
||||
user-wins: fallback
|
||||
explicit-wins: fallback
|
||||
profile-wins: fallback
|
||||
cli-wins: fallback
|
||||
default-wins: fallback
|
||||
config: ignored.yaml
|
||||
profile: ignored.yaml
|
||||
`)
|
||||
writeConfigFile(t, preferredSystemPath, `
|
||||
preferred-wins: preferred
|
||||
local-wins: preferred
|
||||
user-wins: preferred
|
||||
explicit-wins: preferred
|
||||
profile-wins: preferred
|
||||
cli-wins: preferred
|
||||
default-wins: preferred
|
||||
`)
|
||||
writeConfigFile(t, unixSystemPath, `
|
||||
local-wins: local
|
||||
user-wins: local
|
||||
explicit-wins: local
|
||||
profile-wins: local
|
||||
cli-wins: local
|
||||
default-wins: local
|
||||
`)
|
||||
writeConfigFile(t, userPath, fmt.Sprintf(`
|
||||
user-wins: user
|
||||
explicit-wins: user
|
||||
profile-wins: user
|
||||
cli-wins: user
|
||||
default-wins: user
|
||||
config: %q
|
||||
profile: ignored-user.yaml
|
||||
`, explicitPath))
|
||||
writeConfigFile(t, explicitPath, fmt.Sprintf(`
|
||||
explicit-wins: explicit
|
||||
profile-wins: explicit
|
||||
cli-wins: explicit
|
||||
default-wins: explicit
|
||||
profile: %q
|
||||
`, profilePath))
|
||||
writeConfigFile(t, profilePath, `
|
||||
profile-wins: profile
|
||||
cli-wins: profile
|
||||
default-wins: profile
|
||||
`)
|
||||
|
||||
var fallbackOnly, preferredWins, localWins, userWins, explicitWins, profileWins, cliWins, defaultWins string
|
||||
var selected Selection
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.StringVar(&fallbackOnly, "fallback-only", "built-in", "")
|
||||
flagSet.StringVar(&preferredWins, "preferred-wins", "built-in", "")
|
||||
flagSet.StringVar(&localWins, "local-wins", "built-in", "")
|
||||
flagSet.StringVar(&userWins, "user-wins", "built-in", "")
|
||||
flagSet.StringVar(&explicitWins, "explicit-wins", "built-in", "")
|
||||
flagSet.StringVar(&profileWins, "profile-wins", "built-in", "")
|
||||
flagSet.StringVar(&cliWins, "cli-wins", "built-in", "")
|
||||
flagSet.StringVar(&defaultWins, "default-wins", "built-in", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
|
||||
automaticFiles := []string{fallbackSystemPath, preferredSystemPath, unixSystemPath, userPath}
|
||||
automaticErr, err := loadTestFlags(flagSet, automaticFiles, &selected, existingProfilePath, "-cli-wins=cli", "-default-wins=built-in")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
|
||||
assertEqual(t, "fallback-only", fallbackOnly, "fallback")
|
||||
assertEqual(t, "preferred-wins", preferredWins, "preferred")
|
||||
assertEqual(t, "local-wins", localWins, "local")
|
||||
assertEqual(t, "user-wins", userWins, "user")
|
||||
assertEqual(t, "explicit-wins", explicitWins, "explicit")
|
||||
assertEqual(t, "profile-wins", profileWins, "profile")
|
||||
assertEqual(t, "cli-wins", cliWins, "cli")
|
||||
assertEqual(t, "default-wins", defaultWins, "built-in")
|
||||
assertEqual(t, "selected config", selected.Config, explicitPath)
|
||||
assertEqual(t, "selected profile", selected.Profile, profilePath)
|
||||
}
|
||||
|
||||
func TestAutomaticConfigFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
preferredSystemDir := filepath.Join(dir, "preferred")
|
||||
fallbackSystemDir := filepath.Join(dir, "fallback")
|
||||
userConfigDir := filepath.Join(dir, "user")
|
||||
preferredSystemFile := filepath.Join(preferredSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
fallbackSystemFile := filepath.Join(fallbackSystemDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
userConfigFile := filepath.Join(userConfigDir, catalogconfig.BinaryName, catalogconfig.CLIConfigFileName)
|
||||
unixSystemFile := unixSystemConfigFilePath("linux")
|
||||
|
||||
got := automaticConfigFiles([]string{preferredSystemDir, "relative-system", fallbackSystemDir}, "linux", userConfigFile)
|
||||
want := []string{
|
||||
fallbackSystemFile,
|
||||
preferredSystemFile,
|
||||
unixSystemFile,
|
||||
userConfigFile,
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("automatic config files = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
got = automaticConfigFiles([]string{preferredSystemDir, "/etc", userConfigDir}, "linux", userConfigFile)
|
||||
want = []string{
|
||||
preferredSystemFile,
|
||||
unixSystemFile,
|
||||
userConfigFile,
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("automatic config files with duplicate system paths = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
got = automaticConfigFiles([]string{preferredSystemDir, "/etc"}, "linux", unixSystemFile)
|
||||
want = []string{
|
||||
preferredSystemFile,
|
||||
unixSystemFile,
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("automatic config files with Unix path used as user path = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHigherPriorityFilesReplaceCollections(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
systemPath := filepath.Join(dir, "system.yaml")
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
explicitPath := filepath.Join(dir, "explicit.yaml")
|
||||
profilePath := filepath.Join(dir, "profile.yaml")
|
||||
|
||||
writeConfigFile(t, systemPath, "replace: [system-a, system-b]\nclear: [system]\nvar: [system=one]\n")
|
||||
writeConfigFile(t, userPath, fmt.Sprintf("replace: [user]\nclear: [user]\nvar: [user=two]\nconfig: %q\n", explicitPath))
|
||||
writeConfigFile(t, explicitPath, fmt.Sprintf("replace: [explicit-a, explicit-b]\nclear: [explicit]\nvar: [explicit=three]\nprofile: %q\n", profilePath))
|
||||
writeConfigFile(t, profilePath, "replace: [profile]\nclear: []\nvar: [profile=four]\n")
|
||||
|
||||
var replace, clear goflags.StringSlice
|
||||
var variables goflags.RuntimeMap
|
||||
var selected Selection
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.StringSliceVar(&replace, "replace", nil, "", goflags.StringSliceOptions)
|
||||
flagSet.StringSliceVar(&clear, "clear", nil, "", goflags.StringSliceOptions)
|
||||
flagSet.RuntimeMapVar(&variables, "var", nil, "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-config="+explicitPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
if !slices.Equal(replace, []string{"profile"}) {
|
||||
t.Fatalf("replace = %v, want [profile]", replace)
|
||||
}
|
||||
if len(clear) != 0 {
|
||||
t.Fatalf("clear = %v, want an empty list", clear)
|
||||
}
|
||||
if got := variables.AsMap(); len(got) != 1 || got["profile"] != "four" {
|
||||
t.Fatalf("var = %v, want only profile=four", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsingCreatesPrimaryUserConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
createSystem bool
|
||||
wantValue string
|
||||
forbidInUser string
|
||||
}{
|
||||
{name: "missing system", wantValue: "built-in"},
|
||||
{name: "system values are not materialized", createSystem: true, wantValue: "system", forbidInUser: "system"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
systemPath := filepath.Join(dir, "system.yaml")
|
||||
userPath := filepath.Join(dir, "user", "yaml")
|
||||
if test.createSystem {
|
||||
writeConfigFile(t, systemPath, "value: system\n")
|
||||
}
|
||||
|
||||
value := "built-in"
|
||||
var selected Selection
|
||||
var noop bool
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.StringVar(&value, "value", "built-in", "value description")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
flagSet.BoolVar(&noop, "noop", false, "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-noop=false")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
assertEqual(t, "value", value, test.wantValue)
|
||||
|
||||
data, err := os.ReadFile(userPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read generated user config: %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "#value: built-in") {
|
||||
t.Fatalf("generated user config is not a commented built-in config:\n%s", content)
|
||||
}
|
||||
if test.forbidInUser != "" && strings.Contains(content, test.forbidInUser) {
|
||||
t.Fatalf("generated user config materialized %q:\n%s", test.forbidInUser, content)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectedConfigLoadsAfterMalformedAutomaticConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
systemPath := filepath.Join(dir, "system.yaml")
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
explicitPath := filepath.Join(dir, "explicit.yaml")
|
||||
writeConfigFile(t, systemPath, "value: system\nautomatic-only: system\n")
|
||||
writeConfigFile(t, userPath, "value: [\n")
|
||||
writeConfigFile(t, explicitPath, "value: explicit\n")
|
||||
|
||||
value := "built-in"
|
||||
automaticOnly := "built-in"
|
||||
var selected Selection
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.StringVar(&value, "value", "built-in", "")
|
||||
flagSet.StringVar(&automaticOnly, "automatic-only", "built-in", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-config="+explicitPath)
|
||||
if err != nil {
|
||||
t.Fatalf("valid explicit config failed after malformed automatic config: %v", err)
|
||||
}
|
||||
if automaticErr == nil {
|
||||
t.Fatal("expected malformed automatic configuration error")
|
||||
}
|
||||
assertEqual(t, "value", value, "explicit")
|
||||
assertEqual(t, "automatic-only", automaticOnly, "built-in")
|
||||
}
|
||||
|
||||
func TestDeferredUpdateConfiguration(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
updateDir := filepath.Join(dir, "templates")
|
||||
writeConfigFile(t, userPath, fmt.Sprintf(`
|
||||
disable-update-check: true
|
||||
update: true
|
||||
update-templates: true
|
||||
update-template-dir: %q
|
||||
`, updateDir))
|
||||
|
||||
var disableUpdateCheck, updateBinary, updateTemplates, noop bool
|
||||
var selected Selection
|
||||
var selectedUpdateDir string
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.BoolVar(&disableUpdateCheck, "disable-update-check", false, "")
|
||||
flagSet.BoolVar(&updateBinary, "update", false, "")
|
||||
flagSet.BoolVar(&updateTemplates, "update-templates", false, "")
|
||||
flagSet.StringVar(&selectedUpdateDir, "update-template-dir", "", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
flagSet.BoolVar(&noop, "noop", false, "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{filepath.Join(dir, "missing-system.yaml"), userPath}, &selected, existingProfilePath, "-noop=false")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
if !disableUpdateCheck || !updateBinary || !updateTemplates {
|
||||
t.Fatalf("update values were not loaded: disable=%t update=%t templates=%t", disableUpdateCheck, updateBinary, updateTemplates)
|
||||
}
|
||||
assertEqual(t, "update template directory", selectedUpdateDir, updateDir)
|
||||
|
||||
testConfig := &catalogconfig.Config{}
|
||||
if !testConfig.CanCheckForUpdates() {
|
||||
t.Fatal("configuration loading applied disable-update-check too early")
|
||||
}
|
||||
if disableUpdateCheck {
|
||||
testConfig.DisableUpdateCheck()
|
||||
}
|
||||
if testConfig.CanCheckForUpdates() {
|
||||
t.Fatal("deferred disable-update-check was not applied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFilesDoNotExecuteCallbacks(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
systemPath := filepath.Join(dir, "system.yaml")
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
explicitPath := filepath.Join(dir, "explicit.yaml")
|
||||
profilePath := filepath.Join(dir, "profile.yaml")
|
||||
callbackYAML := "reset: true\nversion: true\ntemplates-version: true\n"
|
||||
writeConfigFile(t, systemPath, callbackYAML)
|
||||
writeConfigFile(t, userPath, fmt.Sprintf("%sconfig: %q\n", callbackYAML, explicitPath))
|
||||
writeConfigFile(t, explicitPath, fmt.Sprintf("%sprofile: %q\n", callbackYAML, profilePath))
|
||||
writeConfigFile(t, profilePath, callbackYAML)
|
||||
|
||||
callbackCount := 0
|
||||
var selected Selection
|
||||
var noop bool
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.CallbackVar(func() { callbackCount++ }, "reset", "")
|
||||
flagSet.CallbackVar(func() { callbackCount++ }, "version", "")
|
||||
flagSet.CallbackVar(func() { callbackCount++ }, "templates-version", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
flagSet.BoolVar(&noop, "noop", false, "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-noop=false")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
if callbackCount != 0 {
|
||||
t.Fatalf("configuration executed %d callbacks", callbackCount)
|
||||
}
|
||||
|
||||
flagSet = goflags.NewFlagSet()
|
||||
flagSet.CallbackVar(func() { callbackCount++ }, "reset", "")
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
automaticErr, err = loadTestFlags(flagSet, []string{systemPath, userPath}, &selected, existingProfilePath, "-reset")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration with CLI callback failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration with CLI callback failed: %v", automaticErr)
|
||||
}
|
||||
if callbackCount != 1 {
|
||||
t.Fatalf("CLI callback count = %d, want 1", callbackCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectedFilesCannotRedirectTheirOwnStage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
userPath := filepath.Join(dir, "user.yaml")
|
||||
configPath := filepath.Join(dir, "config.yaml")
|
||||
otherConfigPath := filepath.Join(dir, "other-config.yaml")
|
||||
profilePath := filepath.Join(dir, "profile.yaml")
|
||||
otherProfilePath := filepath.Join(dir, "other-profile.yaml")
|
||||
|
||||
writeConfigFile(t, userPath, fmt.Sprintf("config: %q\n", configPath))
|
||||
writeConfigFile(t, configPath, fmt.Sprintf("config: %q\nprofile: %q\n", otherConfigPath, profilePath))
|
||||
writeConfigFile(t, otherConfigPath, "other-config-loaded: true\n")
|
||||
writeConfigFile(t, profilePath, fmt.Sprintf("config: %q\nprofile: %q\n", otherConfigPath, otherProfilePath))
|
||||
writeConfigFile(t, otherProfilePath, "other-profile-loaded: true\n")
|
||||
|
||||
var selected Selection
|
||||
var otherConfigLoaded, otherProfileLoaded bool
|
||||
flagSet := goflags.NewFlagSet()
|
||||
flagSet.StringVar(&selected.Config, "config", "", "")
|
||||
flagSet.StringVar(&selected.Profile, "profile", "", "")
|
||||
flagSet.BoolVar(&otherConfigLoaded, "other-config-loaded", false, "")
|
||||
flagSet.BoolVar(&otherProfileLoaded, "other-profile-loaded", false, "")
|
||||
|
||||
automaticErr, err := loadTestFlags(flagSet, []string{userPath}, &selected, existingProfilePath, "--")
|
||||
if err != nil {
|
||||
t.Fatalf("load configuration failed: %v", err)
|
||||
}
|
||||
if automaticErr != nil {
|
||||
t.Fatalf("automatic configuration failed: %v", automaticErr)
|
||||
}
|
||||
assertEqual(t, "selected config", selected.Config, configPath)
|
||||
assertEqual(t, "selected profile", selected.Profile, profilePath)
|
||||
if otherConfigLoaded || otherProfileLoaded {
|
||||
t.Fatalf("loading followed a redirected stage: config=%t profile=%t", otherConfigLoaded, otherProfileLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
func loadTestFlags(
|
||||
flagSet *goflags.FlagSet,
|
||||
automaticFiles []string,
|
||||
selected *Selection,
|
||||
resolveProfile func(string) (string, error),
|
||||
args ...string,
|
||||
) (error, error) {
|
||||
userConfigFile := automaticFiles[len(automaticFiles)-1]
|
||||
warnings, err := loadConfigFiles(userConfigFile, userConfigFile, automaticFiles, flagSet, selected, resolveProfile, args...)
|
||||
return errors.Join(warnings...), err
|
||||
}
|
||||
|
||||
func existingProfilePath(path string) (string, error) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func writeConfigFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatalf("create config directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(strings.TrimPrefix(content, "\n")), 0o600); err != nil {
|
||||
t.Fatalf("write config file %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual[T comparable](t *testing.T, name string, got, want T) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("%s = %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
func TestUnixSystemConfigFilePath(t *testing.T) {
|
||||
for _, goos := range []string{"aix", "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris"} {
|
||||
if got, want := unixSystemConfigFilePath(goos), "/etc/nuclei/config.yaml"; got != want {
|
||||
t.Fatalf("unix system config for %s = %q, want %q", goos, got, want)
|
||||
}
|
||||
}
|
||||
for _, goos := range []string{"windows", "plan9", "js"} {
|
||||
if got := unixSystemConfigFilePath(goos); got != "" {
|
||||
t.Fatalf("system config for %s = %q, want empty", goos, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareUserConfigFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
defaultConfigFile := filepath.Join(dir, "default", catalogconfig.CLIConfigFileName)
|
||||
customConfigFile := filepath.Join(dir, "custom", catalogconfig.CLIConfigFileName)
|
||||
if err := os.MkdirAll(filepath.Dir(defaultConfigFile), 0o700); err != nil {
|
||||
t.Fatalf("create default config directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(defaultConfigFile, []byte("value: inherited\n"), 0o600); err != nil {
|
||||
t.Fatalf("write default config: %v", err)
|
||||
}
|
||||
|
||||
if err := prepareUserConfigFile(customConfigFile, defaultConfigFile); err != nil {
|
||||
t.Fatalf("prepare user config file: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(customConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read inherited config: %v", err)
|
||||
}
|
||||
if got, want := string(data), "value: inherited\n"; got != want {
|
||||
t.Fatalf("inherited config = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(defaultConfigFile, []byte("value: changed\n"), 0o600); err != nil {
|
||||
t.Fatalf("update default config: %v", err)
|
||||
}
|
||||
if err := prepareUserConfigFile(customConfigFile, defaultConfigFile); err != nil {
|
||||
t.Fatalf("prepare existing user config: %v", err)
|
||||
}
|
||||
data, err = os.ReadFile(customConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read existing user config: %v", err)
|
||||
}
|
||||
if got, want := string(data), "value: inherited\n"; got != want {
|
||||
t.Fatalf("existing user config = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
missingConfigFile := filepath.Join(dir, "missing-custom", catalogconfig.CLIConfigFileName)
|
||||
if err := prepareUserConfigFile(missingConfigFile, filepath.Join(dir, "missing-default.yaml")); err != nil {
|
||||
t.Fatalf("prepare config without default source: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(missingConfigFile); !os.IsNotExist(err) {
|
||||
t.Fatalf("missing default source created user config: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
catalogconfig "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
)
|
||||
|
||||
// TemplatesDirectory returns the final template directory used while loading
|
||||
// configuration and resolving profiles.
|
||||
func TemplatesDirectory(options *types.Options, config *catalogconfig.Config) string {
|
||||
if templatesDir, overridden := TemplatesDirectoryOverride(options); overridden {
|
||||
return templatesDir
|
||||
}
|
||||
|
||||
return config.GetTemplateDir()
|
||||
}
|
||||
|
||||
// TemplatesDirectoryOverride returns an explicit flag/config or environment
|
||||
// template directory.
|
||||
func TemplatesDirectoryOverride(options *types.Options) (string, bool) {
|
||||
templatesDir := options.NewTemplatesDirectory
|
||||
if templatesDir == "" {
|
||||
templatesDir = os.Getenv(catalogconfig.NucleiTemplatesDirEnv)
|
||||
}
|
||||
|
||||
if templatesDir == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if absPath, err := filepath.Abs(templatesDir); err == nil {
|
||||
return absPath, true
|
||||
}
|
||||
|
||||
return templatesDir, true
|
||||
}
|
||||
|
||||
// ResolveProfilePath resolves a profile path or profile ID against the active
|
||||
// template root.
|
||||
func ResolveProfilePath(profilePath, templatesDir string) (string, error) {
|
||||
defaultProfilesPath := filepath.Join(templatesDir, "profiles")
|
||||
if filepath.Ext(profilePath) == "" {
|
||||
path, err := findProfilePathByID(profilePath, defaultProfilesPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("%q is not a profile ID or profile path", profilePath)
|
||||
}
|
||||
|
||||
profilePath = path
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(profilePath) {
|
||||
if filepath.Dir(profilePath) == "profiles" {
|
||||
defaultProfilesPath = templatesDir
|
||||
}
|
||||
|
||||
currentDir, err := os.Getwd()
|
||||
if err == nil && fileutil.FileExists(filepath.Join(currentDir, profilePath)) {
|
||||
profilePath = filepath.Join(currentDir, profilePath)
|
||||
} else {
|
||||
profilePath = filepath.Join(defaultProfilesPath, profilePath)
|
||||
}
|
||||
}
|
||||
|
||||
if !fileutil.FileExists(profilePath) {
|
||||
return "", fmt.Errorf("given template profile file %q does not exist", profilePath)
|
||||
}
|
||||
|
||||
return profilePath, nil
|
||||
}
|
||||
|
||||
func findProfilePathByID(profileID, profilesDir string) (string, error) {
|
||||
var profilePath string
|
||||
err := filepath.WalkDir(profilesDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
if ext != extensions.YAML && ext != extensions.YML {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.TrimSuffix(filepath.Base(path), ext) == profileID {
|
||||
profilePath = path
|
||||
return fs.SkipAll
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("search profile ID %q in %q: %w", profileID, profilesDir, err)
|
||||
}
|
||||
|
||||
return profilePath, nil
|
||||
}
|
||||
|
||||
// ApplyProfile processes profile-only inline targets and secrets after its
|
||||
// flag values have been merged. It returns the private directory that owns a
|
||||
// generated inline-secrets file.
|
||||
func ApplyProfile(profilePath string, options *types.Options) (string, error) {
|
||||
if profilePath == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
options.Targets = appendInlineTargets(options.Targets, options.InlineTargetsList)
|
||||
if strings.Contains(options.TargetsFilePath, "\n") {
|
||||
options.Targets = appendInlineTargets(options.Targets, options.TargetsFilePath)
|
||||
options.TargetsFilePath = ""
|
||||
}
|
||||
|
||||
return processInlineSecretsFromProfile(profilePath, options)
|
||||
}
|
||||
|
||||
func appendInlineTargets(targets []string, input string) []string {
|
||||
for target := range strings.SplitSeq(strings.TrimSpace(input), "\n") {
|
||||
target = strings.TrimSpace(target)
|
||||
if target != "" && !strings.HasPrefix(target, "#") {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
}
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
type profileSecrets struct {
|
||||
Secrets any `yaml:"secrets"`
|
||||
}
|
||||
|
||||
func processInlineSecretsFromProfile(profilePath string, options *types.Options) (string, error) {
|
||||
data, err := os.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read profile file %q: %w", profilePath, err)
|
||||
}
|
||||
|
||||
var profile profileSecrets
|
||||
if err := yaml.Unmarshal(data, &profile); err != nil {
|
||||
return "", fmt.Errorf("parse profile YAML %q: %w", profilePath, err)
|
||||
}
|
||||
|
||||
if profile.Secrets == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
secretsData, err := yaml.Marshal(profile.Secrets)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal inline profile secrets: %w", err)
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "nuclei-secrets-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create inline secrets directory: %w", err)
|
||||
}
|
||||
|
||||
removeDir := true
|
||||
defer func() {
|
||||
if removeDir {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}
|
||||
}()
|
||||
|
||||
tempPath := filepath.Join(tempDir, "inline-secrets.yaml")
|
||||
if err := os.WriteFile(tempPath, secretsData, 0o600); err != nil {
|
||||
return "", fmt.Errorf("write inline secrets file %q: %w", tempPath, err)
|
||||
}
|
||||
|
||||
removeDir = false
|
||||
options.SecretsFile = append(options.SecretsFile, tempPath)
|
||||
|
||||
return tempDir, nil
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
package fuzzplayground
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This file turns the playground into a small but realistic "modern app" auth
|
||||
// test bench. It serves a range of login styles seen in the wild — classic
|
||||
// server-rendered forms, JS/SPA-rendered forms, username-first multi-step flows,
|
||||
// SPA logins that stash a JWT in web storage, CSRF-protected forms, a
|
||||
// disabled-until-valid submit button (a real regression class), and an
|
||||
// OAuth-style redirect dance — plus protected endpoints (whoami) and an
|
||||
// authenticated, fuzzable endpoint. It lets us exercise the auto-login /
|
||||
// recording / capture engine and authenticated scanning entirely in-repo,
|
||||
// without external apps.
|
||||
//
|
||||
// Credentials are fixed and shared across every style.
|
||||
const (
|
||||
AuthUsername = "[email protected]"
|
||||
AuthPassword = "nuclei-rocks"
|
||||
authSessionCookie = "PSESSION"
|
||||
)
|
||||
|
||||
// authStore is a tiny in-memory session/CSRF store for the auth playground.
|
||||
type authStore struct {
|
||||
mu sync.Mutex
|
||||
sessions map[string]string // session token -> username
|
||||
csrf map[string]struct{}
|
||||
}
|
||||
|
||||
func newAuthStore() *authStore {
|
||||
// Pre-seed the SPA-token style's fixed session so the JWT it stashes in web
|
||||
// storage also authenticates protected APIs (bearer path).
|
||||
return &authStore{
|
||||
sessions: map[string]string{"spa-static-session": AuthUsername},
|
||||
csrf: map[string]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func randToken() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// jwtFor returns a JWT-shaped token (eyJ...) so token-regex extraction has a
|
||||
// realistic target. It is not a real signed JWT; the playground treats the
|
||||
// session token embedded in it as the source of truth.
|
||||
func jwtFor(session string) string {
|
||||
// header.payload.signature where payload carries the opaque session id
|
||||
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
|
||||
hex.EncodeToString([]byte(session)) + ".c2lnbmF0dXJl"
|
||||
}
|
||||
|
||||
func (s *authStore) issue(username string) (session, jwt string) {
|
||||
session = randToken()
|
||||
s.mu.Lock()
|
||||
s.sessions[session] = username
|
||||
s.mu.Unlock()
|
||||
return session, jwtFor(session)
|
||||
}
|
||||
|
||||
func (s *authStore) userFor(session string) (string, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
u, ok := s.sessions[session]
|
||||
return u, ok
|
||||
}
|
||||
|
||||
func (s *authStore) issueCSRF() string {
|
||||
tok := randToken()
|
||||
s.mu.Lock()
|
||||
s.csrf[tok] = struct{}{}
|
||||
s.mu.Unlock()
|
||||
return tok
|
||||
}
|
||||
|
||||
func (s *authStore) consumeCSRF(tok string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.csrf[tok]; ok {
|
||||
delete(s.csrf, tok)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// authenticatedUser resolves the caller's identity from either the session
|
||||
// cookie or a bearer token (the JWT carries the hex-encoded session id).
|
||||
func (s *authStore) authenticatedUser(r *http.Request) (string, bool) {
|
||||
if c, err := r.Cookie(authSessionCookie); err == nil && c.Value != "" {
|
||||
if u, ok := s.userFor(c.Value); ok {
|
||||
return u, true
|
||||
}
|
||||
}
|
||||
auth := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
if sess := sessionFromJWT(strings.TrimPrefix(auth, "Bearer ")); sess != "" {
|
||||
if u, ok := s.userFor(sess); ok {
|
||||
return u, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// sessionFromJWT extracts the opaque session id embedded in a playground JWT.
|
||||
func sessionFromJWT(token string) string {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return ""
|
||||
}
|
||||
raw, err := hex.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func setSessionCookie(w http.ResponseWriter, session string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: authSessionCookie,
|
||||
Value: session,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
// registerAuthRoutes wires every login style and the protected endpoints onto a
|
||||
// per-server auth store.
|
||||
func registerAuthRoutes(mux *http.ServeMux) {
|
||||
st := newAuthStore()
|
||||
|
||||
// 1. Classic server-rendered form login (works with the static HTTP engine).
|
||||
mux.HandleFunc("GET /auth/form-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, formLoginPage("", ""))
|
||||
})
|
||||
mux.HandleFunc("POST /auth/form-login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("username") == AuthUsername && r.FormValue("password") == AuthPassword {
|
||||
session, _ := st.issue(AuthUsername)
|
||||
setSessionCookie(w, session)
|
||||
http.Redirect(w, r, "/auth/dashboard", http.StatusFound)
|
||||
return
|
||||
}
|
||||
writeHTML(w, http.StatusOK, formLoginPage(r.FormValue("username"), "Invalid credentials"))
|
||||
})
|
||||
|
||||
// 2. CSRF-protected server-rendered form: a hidden token must be echoed back.
|
||||
mux.HandleFunc("GET /auth/csrf-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, csrfLoginPage(st.issueCSRF(), ""))
|
||||
})
|
||||
mux.HandleFunc("POST /auth/csrf-login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !st.consumeCSRF(r.FormValue("csrf_token")) {
|
||||
writeHTML(w, http.StatusForbidden, csrfLoginPage(st.issueCSRF(), "Invalid CSRF token"))
|
||||
return
|
||||
}
|
||||
if r.FormValue("username") == AuthUsername && r.FormValue("password") == AuthPassword {
|
||||
session, _ := st.issue(AuthUsername)
|
||||
setSessionCookie(w, session)
|
||||
http.Redirect(w, r, "/auth/dashboard", http.StatusFound)
|
||||
return
|
||||
}
|
||||
writeHTML(w, http.StatusOK, csrfLoginPage(st.issueCSRF(), "Invalid credentials"))
|
||||
})
|
||||
|
||||
// 3. JS/SPA-rendered form: the raw HTML has no <form>; JS builds it and posts
|
||||
// via fetch. Only a real browser can log in here.
|
||||
mux.HandleFunc("GET /auth/spa-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, spaLoginPage)
|
||||
})
|
||||
mux.HandleFunc("POST /auth/api/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad request"})
|
||||
return
|
||||
}
|
||||
if body.Username == AuthUsername && body.Password == AuthPassword {
|
||||
session, jwt := st.issue(AuthUsername)
|
||||
setSessionCookie(w, session)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"token": jwt})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
})
|
||||
|
||||
// 4. Username-first multi-step flow (Google/Microsoft-style): password field
|
||||
// is revealed only after the "Next" button.
|
||||
mux.HandleFunc("GET /auth/multistep-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, multiStepLoginPage)
|
||||
})
|
||||
|
||||
// 5. Disabled-until-valid submit button — a real regression class: the submit
|
||||
// stays disabled until JS marks the form valid, so a naive immediate click
|
||||
// hangs. The engine must settle/fall back to Enter.
|
||||
mux.HandleFunc("GET /auth/strict-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, strictLoginPage)
|
||||
})
|
||||
|
||||
// 6. SPA login that stores a JWT in localStorage (no cookie at all).
|
||||
mux.HandleFunc("GET /auth/spa-token-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, spaTokenLoginPage)
|
||||
})
|
||||
|
||||
// 7. OAuth-style redirect flow: authorize -> (auto consent) -> callback sets
|
||||
// the session and redirects to the dashboard.
|
||||
mux.HandleFunc("GET /auth/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/auth/oauth/callback?code="+randToken(), http.StatusFound)
|
||||
})
|
||||
mux.HandleFunc("GET /auth/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("code") == "" {
|
||||
writeString(w, http.StatusBadRequest, "missing code")
|
||||
return
|
||||
}
|
||||
session, _ := st.issue(AuthUsername)
|
||||
setSessionCookie(w, session)
|
||||
http.Redirect(w, r, "/auth/dashboard", http.StatusFound)
|
||||
})
|
||||
|
||||
// 8. Multi-form page: a decoy search form (no password) precedes the real
|
||||
// login form. The detector must score forms and pick the credential one
|
||||
// rather than blindly taking the first <form>.
|
||||
mux.HandleFunc("GET /auth/multiform-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, multiFormLoginPage)
|
||||
})
|
||||
|
||||
// 9. JS-set cookie: login succeeds via XHR that returns the session id in the
|
||||
// body (no Set-Cookie), and client JS writes document.cookie. The headless
|
||||
// engine must capture cookies from the browser jar, not just Set-Cookie.
|
||||
mux.HandleFunc("GET /auth/jscookie-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, jsCookieLoginPage)
|
||||
})
|
||||
mux.HandleFunc("POST /auth/api/login-jsbody", func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad request"})
|
||||
return
|
||||
}
|
||||
if body.Username == AuthUsername && body.Password == AuthPassword {
|
||||
session, _ := st.issue(AuthUsername)
|
||||
// Deliberately no Set-Cookie: the client sets the cookie via JS.
|
||||
writeJSON(w, http.StatusOK, map[string]string{"session": session})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
})
|
||||
|
||||
// 10. Delayed render: the login form is injected after an async tick, so an
|
||||
// engine that probes for the password field immediately would miss it and
|
||||
// must wait for it to appear.
|
||||
mux.HandleFunc("GET /auth/delayed-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, delayedLoginPage)
|
||||
})
|
||||
|
||||
// 11. Header-token login: a server-rendered form whose POST returns the
|
||||
// session token in a response header (no cookie), the token-in-header
|
||||
// API style. The engine must read the token from the header.
|
||||
mux.HandleFunc("GET /auth/header-token-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, headerTokenLoginPage(""))
|
||||
})
|
||||
mux.HandleFunc("POST /auth/header-token-login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.FormValue("username") == AuthUsername && r.FormValue("password") == AuthPassword {
|
||||
_, jwt := st.issue(AuthUsername)
|
||||
// Token in a header, deliberately no Set-Cookie.
|
||||
w.Header().Set("X-Auth-Token", jwt)
|
||||
writeHTML(w, http.StatusOK, `<html><head><title>Dashboard</title></head><body><h1>Welcome, signed in</h1></body></html>`)
|
||||
return
|
||||
}
|
||||
writeHTML(w, http.StatusOK, headerTokenLoginPage("Invalid credentials"))
|
||||
})
|
||||
|
||||
// 12. SPA whose XHR login returns the token only in a response header (no
|
||||
// cookie, no body/storage token). Exercises the headless engine's
|
||||
// passive response-header interception.
|
||||
mux.HandleFunc("GET /auth/spa-header-token-login", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, spaHeaderTokenLoginPage)
|
||||
})
|
||||
mux.HandleFunc("POST /auth/api/login-header", func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "bad request"})
|
||||
return
|
||||
}
|
||||
if body.Username == AuthUsername && body.Password == AuthPassword {
|
||||
_, jwt := st.issue(AuthUsername)
|
||||
w.Header().Set("X-Auth-Token", jwt)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
|
||||
})
|
||||
|
||||
// Protected landing page: the login-success heuristic relies on the final
|
||||
// page having no password field, which this page satisfies.
|
||||
mux.HandleFunc("GET /auth/dashboard", func(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := st.authenticatedUser(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/auth/form-login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(`<html><head><title>Dashboard</title></head>
|
||||
<body><h1>Welcome, %s</h1><a href="/auth/logout">Logout</a></body></html>`, html.EscapeString(user)))
|
||||
})
|
||||
|
||||
// whoami: protected API used to assert a captured session authenticates.
|
||||
mux.HandleFunc("GET /auth/whoami", func(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := st.authenticatedUser(r)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"user": user})
|
||||
})
|
||||
|
||||
// Authenticated, fuzzable endpoint: only reachable with a valid session, and
|
||||
// reflects q without sanitization (reflected XSS) so authenticated fuzzing
|
||||
// has a genuine target behind the login wall.
|
||||
mux.HandleFunc("GET /auth/api/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := st.authenticatedUser(r); !ok {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
q := r.URL.Query().Get("q")
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf("<html><body><div>results for: %s</div></body></html>", q))
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
if ck, err := r.Cookie(authSessionCookie); err == nil {
|
||||
st.mu.Lock()
|
||||
delete(st.sessions, ck.Value)
|
||||
st.mu.Unlock()
|
||||
}
|
||||
setSessionCookie(w, "")
|
||||
http.Redirect(w, r, "/auth/form-login", http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
// --- Login page templates --------------------------------------------------
|
||||
|
||||
func formLoginPage(username, errMsg string) string {
|
||||
var banner string
|
||||
if errMsg != "" {
|
||||
banner = fmt.Sprintf(`<p class="error">%s</p>`, html.EscapeString(errMsg))
|
||||
}
|
||||
return fmt.Sprintf(`<html><head><title>Login</title></head><body>
|
||||
<h1>Sign in</h1>%s
|
||||
<form method="post" action="/auth/form-login">
|
||||
<input type="text" name="username" placeholder="Email" value="%s">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign in</button>
|
||||
</form></body></html>`, banner, html.EscapeString(username))
|
||||
}
|
||||
|
||||
func csrfLoginPage(csrf, errMsg string) string {
|
||||
var banner string
|
||||
if errMsg != "" {
|
||||
banner = fmt.Sprintf(`<p class="error">%s</p>`, html.EscapeString(errMsg))
|
||||
}
|
||||
return fmt.Sprintf(`<html><head><title>Login</title></head><body>
|
||||
<h1>Sign in</h1>%s
|
||||
<form method="post" action="/auth/csrf-login">
|
||||
<input type="hidden" name="csrf_token" value="%s">
|
||||
<input type="text" name="username" placeholder="Email">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign in</button>
|
||||
</form></body></html>`, banner, html.EscapeString(csrf))
|
||||
}
|
||||
|
||||
// spaLoginPage builds the form entirely in JS (no <form> in the raw HTML) and
|
||||
// submits it via fetch, then redirects on success.
|
||||
const spaLoginPage = `<html><head><title>SPA Login</title></head><body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
var app = document.getElementById('app');
|
||||
var email = document.createElement('input'); email.id = 'email'; email.type = 'email';
|
||||
var pass = document.createElement('input'); pass.id = 'password'; pass.type = 'password';
|
||||
var btn = document.createElement('button'); btn.id = 'submit'; btn.textContent = 'Sign in';
|
||||
app.appendChild(email); app.appendChild(pass); app.appendChild(btn);
|
||||
btn.addEventListener('click', function () {
|
||||
fetch('/auth/api/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({username: email.value, password: pass.value})
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d.token) { document.body.innerHTML = '<h1>Welcome, signed in</h1>'; }
|
||||
else { document.body.innerHTML += '<p>login failed</p>'; }
|
||||
});
|
||||
});
|
||||
</script></body></html>`
|
||||
|
||||
// multiStepLoginPage hides the password field until "Next" is clicked.
|
||||
const multiStepLoginPage = `<html><head><title>Multi-step Login</title></head><body>
|
||||
<form id="f" method="post" action="/auth/form-login">
|
||||
<input type="text" name="username" id="username" placeholder="Email">
|
||||
<button type="button" id="next">Next</button>
|
||||
<div id="pwwrap" style="display:none">
|
||||
<input type="password" name="password" id="password" placeholder="Password">
|
||||
<button type="submit" id="submit">Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('next').addEventListener('click', function () {
|
||||
document.getElementById('pwwrap').style.display = 'block';
|
||||
});
|
||||
</script></body></html>`
|
||||
|
||||
// strictLoginPage keeps the submit button disabled until both fields are
|
||||
// non-empty, reproducing the disabled-until-valid regression class.
|
||||
const strictLoginPage = `<html><head><title>Strict Login</title></head><body>
|
||||
<form method="post" action="/auth/form-login">
|
||||
<input type="text" name="username" id="username" placeholder="Email">
|
||||
<input type="password" name="password" id="password" placeholder="Password">
|
||||
<button type="submit" id="submit" disabled>Sign in</button>
|
||||
</form>
|
||||
<script>
|
||||
var u = document.getElementById('username'), p = document.getElementById('password'), b = document.getElementById('submit');
|
||||
function validate() { b.disabled = !(u.value.length > 0 && p.value.length > 0); }
|
||||
u.addEventListener('input', validate); p.addEventListener('input', validate);
|
||||
</script></body></html>`
|
||||
|
||||
// spaTokenLoginPage validates client-side and stores a JWT in localStorage with
|
||||
// no cookie, the canonical token-in-web-storage case.
|
||||
var spaTokenLoginPage = fmt.Sprintf(`<html><head><title>SPA Token Login</title></head><body>
|
||||
<form id="f">
|
||||
<input type="email" name="username" id="email" placeholder="Email">
|
||||
<input type="password" name="password" id="password" placeholder="Password">
|
||||
<button type="submit" id="submit">Sign in</button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('f').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var u = document.getElementById('email').value;
|
||||
var p = document.getElementById('password').value;
|
||||
if (u === %q && p === %q) {
|
||||
window.localStorage.setItem('access_token', %q);
|
||||
window.sessionStorage.setItem('uid', '1');
|
||||
document.body.innerHTML = '<h1>Welcome, signed in</h1>';
|
||||
} else {
|
||||
document.body.innerHTML += '<p>login failed</p>';
|
||||
}
|
||||
});
|
||||
</script></body></html>`, AuthUsername, AuthPassword, jwtFor("spa-static-session"))
|
||||
|
||||
// multiFormLoginPage places a decoy search form (no password field) before the
|
||||
// real login form to exercise best-form selection over first-form selection.
|
||||
const multiFormLoginPage = `<html><head><title>Multi-form Login</title></head><body>
|
||||
<form id="search" method="get" action="/auth/api/search">
|
||||
<input type="text" name="q" placeholder="Search">
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<hr>
|
||||
<form id="login" method="post" action="/auth/form-login">
|
||||
<input type="text" name="username" placeholder="Email">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign in</button>
|
||||
</form></body></html>`
|
||||
|
||||
// jsCookieLoginPage logs in via XHR and sets the session cookie from JS
|
||||
// (document.cookie) rather than relying on a Set-Cookie response header.
|
||||
const jsCookieLoginPage = `<html><head><title>JS Cookie Login</title></head><body>
|
||||
<form id="f">
|
||||
<input type="email" name="username" id="email" placeholder="Email">
|
||||
<input type="password" name="password" id="password" placeholder="Password">
|
||||
<button type="submit" id="submit">Sign in</button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('f').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
fetch('/auth/api/login-jsbody', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('email').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d.session) {
|
||||
document.cookie = 'PSESSION=' + d.session + '; path=/';
|
||||
document.body.innerHTML = '<h1>Welcome, signed in</h1>';
|
||||
} else {
|
||||
document.body.innerHTML += '<p>login failed</p>';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script></body></html>`
|
||||
|
||||
// headerTokenLoginPage is a classic server-rendered form whose POST returns the
|
||||
// session token in a response header rather than a cookie or body.
|
||||
func headerTokenLoginPage(errMsg string) string {
|
||||
var banner string
|
||||
if errMsg != "" {
|
||||
banner = fmt.Sprintf(`<p class="error">%s</p>`, html.EscapeString(errMsg))
|
||||
}
|
||||
return fmt.Sprintf(`<html><head><title>Header Token Login</title></head><body>
|
||||
<h1>Sign in</h1>%s
|
||||
<form method="post" action="/auth/header-token-login">
|
||||
<input type="text" name="username" placeholder="Email">
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
<button type="submit">Sign in</button>
|
||||
</form></body></html>`, banner)
|
||||
}
|
||||
|
||||
// spaHeaderTokenLoginPage logs in via XHR; the token comes back only in a
|
||||
// response header (the page never reads or stores it), so the session is
|
||||
// recoverable only by observing response headers.
|
||||
const spaHeaderTokenLoginPage = `<html><head><title>SPA Header Token Login</title></head><body>
|
||||
<form id="f">
|
||||
<input type="email" name="username" id="email" placeholder="Email">
|
||||
<input type="password" name="password" id="password" placeholder="Password">
|
||||
<button type="submit" id="submit">Sign in</button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('f').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
fetch('/auth/api/login-header', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('email').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d.ok) { document.body.innerHTML = '<h1>Welcome, signed in</h1>'; }
|
||||
else { document.body.innerHTML += '<p>login failed</p>'; }
|
||||
});
|
||||
});
|
||||
</script></body></html>`
|
||||
|
||||
// delayedLoginPage injects the login form only after an async tick, simulating a
|
||||
// SPA that renders its form after bootstrapping.
|
||||
const delayedLoginPage = `<html><head><title>Delayed Login</title></head><body>
|
||||
<div id="app"><p>loading...</p></div>
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
document.getElementById('app').innerHTML =
|
||||
'<form method="post" action="/auth/form-login">' +
|
||||
'<input type="text" name="username" placeholder="Email">' +
|
||||
'<input type="password" name="password" placeholder="Password">' +
|
||||
'<button type="submit">Sign in</button></form>';
|
||||
}, 700);
|
||||
</script></body></html>`
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -39,6 +40,9 @@ func GetPlaygroundServer() *PlaygroundServer {
|
||||
mux.HandleFunc("POST /user", patchUnsanitizedUserHandler)
|
||||
mux.HandleFunc("GET /blog/posts", getPostsHandler)
|
||||
|
||||
registerAnalyzerRoutes(mux)
|
||||
registerAuthRoutes(mux)
|
||||
|
||||
handler := recoverPlaygroundRequest(logPlaygroundRequest(mux))
|
||||
return &PlaygroundServer{handler: handler}
|
||||
}
|
||||
@@ -65,6 +69,239 @@ func (s *PlaygroundServer) Close() error {
|
||||
return s.server.Close()
|
||||
}
|
||||
|
||||
// registerAnalyzerRoutes wires a dedicated, analyzer-friendly test bench used by
|
||||
// the fuzzer "analyzer" templates. Each endpoint is genuinely vulnerable but
|
||||
// responds purely in-band and deterministically (no external network egress),
|
||||
// so the corresponding analyzer's generic probes reliably trigger detection in
|
||||
// CI. The query parameter is always "q" to keep the templates uniform.
|
||||
func registerAnalyzerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /analyzer/sqli", analyzerSQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/ssti", analyzerSSTIHandler)
|
||||
mux.HandleFunc("GET /analyzer/lfi", analyzerLFIHandler)
|
||||
mux.HandleFunc("GET /analyzer/cmdi", analyzerCMDiHandler)
|
||||
mux.HandleFunc("GET /analyzer/ssrf", analyzerSSRFHandler)
|
||||
mux.HandleFunc("GET /analyzer/redirect", analyzerRedirectHandler)
|
||||
mux.HandleFunc("GET /analyzer/crlf", analyzerCRLFHandler)
|
||||
mux.HandleFunc("GET /analyzer/cors", analyzerCORSHandler)
|
||||
mux.HandleFunc("GET /analyzer/host-header", analyzerHostHeaderHandler)
|
||||
|
||||
// Benign counterparts: these reflect or echo input but are NOT vulnerable,
|
||||
// so the analyzers must NOT raise a finding against them (false-positive
|
||||
// guard at the CLI level).
|
||||
mux.HandleFunc("GET /analyzer/safe/reflect", analyzerSafeReflectHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/redirect", analyzerSafeRedirectHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/cors", analyzerSafeCORSHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/headers", analyzerSafeHeadersHandler)
|
||||
mux.HandleFunc("GET /analyzer/safe/host", analyzerSafeHostHandler)
|
||||
|
||||
// Non-query positions: prove the analyzers fuzz path / header / cookie / body
|
||||
// components through the real pipeline, not just query parameters.
|
||||
mux.HandleFunc("GET /analyzer/path/sqli/{id}", analyzerPathSQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/header/sqli", analyzerHeaderSQLiHandler)
|
||||
mux.HandleFunc("POST /analyzer/body/sqli", analyzerBodySQLiHandler)
|
||||
mux.HandleFunc("GET /analyzer/cookie/ssti", analyzerCookieSSTIHandler)
|
||||
}
|
||||
|
||||
// reArithmeticTemplate emulates a real template engine: it matches an arithmetic
|
||||
// multiplication wrapped in a delimiter pair (EL ${}, Jinja {{}}, #{}, *{},
|
||||
// Razor @(), ERB <%= %>, Smarty {}) and replaces the WHOLE delimited expression
|
||||
// with its product, preserving any surrounding text (e.g. the analyzer's
|
||||
// sentinels), exactly as a vulnerable engine would.
|
||||
var reArithmeticTemplate = regexp.MustCompile(`(?:\$\{|\{\{|#\{|\*\{|@\(|<%=|\{)\s*(\d+)\s*\*\s*(\d+)\s*(?:\}\}|%>|\}|\))`)
|
||||
|
||||
// analyzerCmdiSeparators are shell metacharacter sequences followed by the `id`
|
||||
// command that the cmdi analyzer injects; presence of any indicates the input
|
||||
// reached a shell.
|
||||
var analyzerCmdiSeparators = []string{";id", "|id", "||id", "&&id", "&id", "`id`", "$(id)", "\nid"}
|
||||
|
||||
// analyzerSQLiHandler is vulnerable to error-based SQLi via the real sqlite DB:
|
||||
// a quote in q breaks the query and surfaces a genuine "unrecognized token"
|
||||
// sqlite error, which the sqli_error analyzer fingerprints.
|
||||
func analyzerSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
posts, err := getUnsanitizedPostsByLang(db, q)
|
||||
if err != nil {
|
||||
writeString(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, posts)
|
||||
}
|
||||
|
||||
// analyzerSSTIHandler evaluates arithmetic template expressions in q.
|
||||
func analyzerSSTIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
out := reArithmeticTemplate.ReplaceAllStringFunc(r.URL.Query().Get("q"), func(m string) string {
|
||||
sub := reArithmeticTemplate.FindStringSubmatch(m)
|
||||
a, _ := strconv.Atoi(sub[1])
|
||||
b, _ := strconv.Atoi(sub[2])
|
||||
return strconv.Itoa(a * b)
|
||||
})
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, out))
|
||||
}
|
||||
|
||||
// analyzerLFIHandler returns file contents for path-traversal payloads in q.
|
||||
func analyzerLFIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
switch {
|
||||
case strings.Contains(q, "etc/passwd"):
|
||||
writeString(w, http.StatusOK, "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n")
|
||||
case strings.Contains(strings.ToLower(q), "win.ini"):
|
||||
writeString(w, http.StatusOK, "; for 16-bit app support\r\n[fonts]\r\n")
|
||||
default:
|
||||
writeString(w, http.StatusOK, "file not found")
|
||||
}
|
||||
}
|
||||
|
||||
// analyzerCMDiHandler simulates a shell that concatenates q: an injected
|
||||
// separator followed by `id` yields command output.
|
||||
func analyzerCMDiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
for _, sep := range analyzerCmdiSeparators {
|
||||
if strings.Contains(q, sep) {
|
||||
writeString(w, http.StatusOK, "uid=0(root) gid=0(root) groups=0(root)")
|
||||
return
|
||||
}
|
||||
}
|
||||
writeString(w, http.StatusOK, fmt.Sprintf("ping output for %s", q))
|
||||
}
|
||||
|
||||
// analyzerSSRFHandler simulates an in-band SSRF: requesting a cloud metadata
|
||||
// endpoint returns the (mock) instance identity document. It does NOT perform a
|
||||
// real outbound request, keeping the test hermetic.
|
||||
func analyzerSSRFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
if strings.Contains(q, "169.254.169.254") || strings.Contains(q, "metadata.google.internal") {
|
||||
writeString(w, http.StatusOK, `{"accountId":"123456789012","imageId":"ami-0abcd1234ef567890","instanceId":"i-0abcd1234ef567890","region":"us-east-1"}`)
|
||||
return
|
||||
}
|
||||
writeString(w, http.StatusOK, "fetched: nothing interesting")
|
||||
}
|
||||
|
||||
// analyzerRedirectHandler reflects q straight into the Location header.
|
||||
func analyzerRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, r.URL.Query().Get("q"), http.StatusFound)
|
||||
}
|
||||
|
||||
// analyzerCRLFHandler naively builds response headers from q, splitting on
|
||||
// newlines — a textbook response-splitting bug.
|
||||
func analyzerCRLFHandler(w http.ResponseWriter, r *http.Request) {
|
||||
for _, line := range strings.Split(r.URL.Query().Get("q"), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
idx := strings.Index(line, ": ")
|
||||
if idx <= 0 || strings.ContainsAny(line[:idx], " \t") {
|
||||
continue
|
||||
}
|
||||
name, val := line[:idx], line[idx+2:]
|
||||
if strings.EqualFold(name, "Set-Cookie") {
|
||||
w.Header().Add("Set-Cookie", val)
|
||||
} else {
|
||||
w.Header().Set(name, val)
|
||||
}
|
||||
}
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerCORSHandler reflects an arbitrary Origin and allows credentials.
|
||||
func analyzerCORSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if origin := r.Header.Get("Origin"); origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerHostHeaderHandler reflects the (attacker-controlled) X-Forwarded-Host
|
||||
// into an absolute link in the body, without performing any outbound request.
|
||||
func analyzerHostHeaderHandler(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = r.Host
|
||||
}
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(`<a href="https://%s/reset?token=abc">reset</a>`, host))
|
||||
}
|
||||
|
||||
// --- Benign handlers (must not trigger any analyzer) -----------------------
|
||||
|
||||
// analyzerSafeReflectHandler reflects q verbatim with no evaluation, no DB, no
|
||||
// command execution and no file access; it is the benign counterpart for the
|
||||
// ssti, sqli, cmdi, lfi and ssrf analyzers.
|
||||
func analyzerSafeReflectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, "you searched for: "+r.URL.Query().Get("q")))
|
||||
}
|
||||
|
||||
// analyzerSafeRedirectHandler always redirects to a fixed trusted location,
|
||||
// ignoring user input.
|
||||
func analyzerSafeRedirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/home", http.StatusFound)
|
||||
}
|
||||
|
||||
// analyzerSafeCORSHandler only ever allows a single trusted origin.
|
||||
func analyzerSafeCORSHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.example.com")
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerSafeHeadersHandler returns static headers and never reflects input.
|
||||
func analyzerSafeHeadersHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("X-Static", "constant")
|
||||
writeString(w, http.StatusOK, "ok")
|
||||
}
|
||||
|
||||
// analyzerSafeHostHandler always builds links from a fixed, trusted host.
|
||||
func analyzerSafeHostHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
writeHTML(w, http.StatusOK, `<a href="https://app.example.com/reset?token=abc">reset</a>`)
|
||||
}
|
||||
|
||||
// --- Non-query position handlers -------------------------------------------
|
||||
|
||||
// sqliFromValue runs the value through the real sqlite query so a quote yields a
|
||||
// genuine "unrecognized token" error that the sqli_error analyzer fingerprints.
|
||||
func sqliFromValue(w http.ResponseWriter, value string) {
|
||||
posts, err := getUnsanitizedPostsByLang(db, value)
|
||||
if err != nil {
|
||||
writeString(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, posts)
|
||||
}
|
||||
|
||||
// analyzerPathSQLiHandler is error-based SQLi on a path segment ({id}).
|
||||
func analyzerPathSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sqliFromValue(w, r.PathValue("id"))
|
||||
}
|
||||
|
||||
// analyzerHeaderSQLiHandler is error-based SQLi on the X-Search request header.
|
||||
func analyzerHeaderSQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sqliFromValue(w, r.Header.Get("X-Search"))
|
||||
}
|
||||
|
||||
// analyzerBodySQLiHandler is error-based SQLi on the JSON body "name" field.
|
||||
func analyzerBodySQLiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
_ = json.Unmarshal(body, &payload)
|
||||
sqliFromValue(w, payload.Name)
|
||||
}
|
||||
|
||||
// analyzerCookieSSTIHandler evaluates arithmetic template expressions found in
|
||||
// the "lang" cookie. SSTI (not SQLi) is used here because Go's cookie value
|
||||
// sanitization strips the quotes SQLi relies on, whereas SSTI payload
|
||||
// characters ({ } * $) are cookie-legal.
|
||||
func analyzerCookieSSTIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
val := "en"
|
||||
if c, err := r.Cookie("lang"); err == nil {
|
||||
val = c.Value
|
||||
}
|
||||
out := reArithmeticTemplate.ReplaceAllStringFunc(val, func(m string) string {
|
||||
sub := reArithmeticTemplate.FindStringSubmatch(m)
|
||||
a, _ := strconv.Atoi(sub[1])
|
||||
b, _ := strconv.Atoi(sub[2])
|
||||
return strconv.Itoa(a * b)
|
||||
})
|
||||
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, "lang="+out))
|
||||
}
|
||||
|
||||
var bodyTemplate = `<html>
|
||||
<head>
|
||||
<title>Fuzz Playground</title>
|
||||
@@ -88,6 +325,16 @@ func indexHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
<li><a href="/user/75/profile">User Profile Page SQLI (path parameter)</a></li>
|
||||
<li><a href="/user">POST on /user SQLI (body parameter)</a></li>
|
||||
<li><a href="/blog/posts">SQLI in cookie lang parameter value (eg. lang=en)</a></li>
|
||||
|
||||
<li><a href="/analyzer/sqli?q=en">Analyzer bench: SQLi (query)</a></li>
|
||||
<li><a href="/analyzer/ssti?q=test">Analyzer bench: SSTI (query)</a></li>
|
||||
<li><a href="/analyzer/lfi?q=home.txt">Analyzer bench: LFI (query)</a></li>
|
||||
<li><a href="/analyzer/cmdi?q=127.0.0.1">Analyzer bench: CMDi (query)</a></li>
|
||||
<li><a href="/analyzer/ssrf?q=https://example.com">Analyzer bench: SSRF (query)</a></li>
|
||||
<li><a href="/analyzer/redirect?q=/dashboard">Analyzer bench: Open Redirect (query)</a></li>
|
||||
<li><a href="/analyzer/crlf?q=/home">Analyzer bench: CRLF (query)</a></li>
|
||||
<li><a href="/analyzer/cors?q=x">Analyzer bench: CORS (query)</a></li>
|
||||
<li><a href="/analyzer/host-header?q=x">Analyzer bench: Host Header Injection (query)</a></li>
|
||||
|
||||
</ul>
|
||||
`))
|
||||
@@ -284,6 +531,12 @@ func writeJSON(w http.ResponseWriter, statusCode int, value interface{}) {
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
|
||||
func writeString(w http.ResponseWriter, statusCode int, value string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
w.WriteHeader(statusCode)
|
||||
_, _ = io.WriteString(w, value)
|
||||
}
|
||||
|
||||
func recoverPlaygroundRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
|
||||
+5
-29
@@ -4,8 +4,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -89,7 +87,11 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
|
||||
tmp.Update()
|
||||
u.uploadURL = tmp.URL
|
||||
|
||||
u.client = newUploadClient()
|
||||
// create http client
|
||||
opts := retryablehttp.DefaultOptionsSingle
|
||||
opts.NoAdjustTimeout = true
|
||||
opts.Timeout = time.Duration(3) * time.Minute
|
||||
u.client = retryablehttp.NewClient(opts)
|
||||
|
||||
// create context
|
||||
ctx, u.cancel = context.WithCancel(ctx)
|
||||
@@ -99,32 +101,6 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// newUploadClient returns the client for cloud uploads. The upload sends the
|
||||
// API key and the scan results, so the client verifies the server certificate.
|
||||
// The retryablehttp default clients skip verification.
|
||||
func newUploadClient() *retryablehttp.Client {
|
||||
transport := retryablehttp.DefaultReusePooledTransport()
|
||||
// fastdialer DialTLS does not verify certificates, so net/http does the
|
||||
// TLS handshake over the fastdialer connection.
|
||||
transport.DialTLSContext = nil
|
||||
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
|
||||
opts := retryablehttp.DefaultOptionsSingle
|
||||
opts.NoAdjustTimeout = true
|
||||
opts.Timeout = time.Duration(3) * time.Minute
|
||||
opts.HttpClient = &http.Client{Transport: transport}
|
||||
// a certificate error does not go away on retry
|
||||
defaultRetry := retryablehttp.DefaultRetryPolicy()
|
||||
opts.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
||||
var certErr *tls.CertificateVerificationError
|
||||
if errors.As(err, &certErr) {
|
||||
return false, nil
|
||||
}
|
||||
return defaultRetry(ctx, resp, err)
|
||||
}
|
||||
return retryablehttp.NewClient(opts)
|
||||
}
|
||||
|
||||
// SetScanID sets the scan id for the upload writer
|
||||
func (u *UploadWriter) SetScanID(id string) error {
|
||||
if !xidRegex.MatchString(id) {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package pdcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestUploadWriter(t *testing.T, gotKey *atomic.Value) (*UploadWriter, *httptest.Server) {
|
||||
t.Helper()
|
||||
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotKey.Store(r.Header.Get(pdcpauth.ApiKeyHeaderName))
|
||||
_, _ = w.Write([]byte(`{"id":"abcdefghij0123456789"}`))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
creds := &pdcpauth.PDCPCredentials{APIKey: "test-key", Server: srv.URL}
|
||||
u, err := NewUploadWriter(context.Background(), gologger.DefaultLogger, creds)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(u.Close)
|
||||
return u, srv
|
||||
}
|
||||
|
||||
func TestUploadRejectsUntrustedCertificate(t *testing.T) {
|
||||
var gotKey atomic.Value
|
||||
u, _ := newTestUploadWriter(t, &gotKey)
|
||||
|
||||
err := u.upload([]byte("{}\n"))
|
||||
require.Error(t, err)
|
||||
var unknownAuthority x509.UnknownAuthorityError
|
||||
require.ErrorAs(t, err, &unknownAuthority)
|
||||
require.Nil(t, gotKey.Load(), "API key must not reach a server with an untrusted certificate")
|
||||
}
|
||||
|
||||
func TestUploadTrustedCertificate(t *testing.T) {
|
||||
var gotKey atomic.Value
|
||||
u, srv := newTestUploadWriter(t, &gotKey)
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(srv.Certificate())
|
||||
transport, ok := u.client.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}
|
||||
|
||||
require.NoError(t, u.upload([]byte("{}\n")))
|
||||
require.Equal(t, "test-key", gotKey.Load())
|
||||
require.Equal(t, "abcdefghij0123456789", u.scanID)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
)
|
||||
|
||||
type directoryRecord struct {
|
||||
label string
|
||||
path string
|
||||
}
|
||||
|
||||
func directoryRecords() []directoryRecord {
|
||||
return []directoryRecord{
|
||||
{label: "Nuclei Config Directory", path: config.DefaultConfig.GetConfigDir()},
|
||||
{label: "Nuclei State Directory", path: config.DefaultConfig.GetStateDir()},
|
||||
{label: "Nuclei Cache Directory", path: config.DefaultConfig.GetCacheDir()},
|
||||
{label: "PDCP Directory", path: pdcpauth.PDCPDir},
|
||||
}
|
||||
}
|
||||
|
||||
// AppendDirectoryInfo writes nuclei config/state/cache/PDCP directory paths.
|
||||
func AppendDirectoryInfo(w io.Writer) {
|
||||
for _, record := range directoryRecords() {
|
||||
_, _ = fmt.Fprintf(w, "%s: %s\n", record.label, record.path)
|
||||
}
|
||||
}
|
||||
|
||||
// DirectoryInfo returns nuclei config/state/cache/PDCP directory paths as a string.
|
||||
func DirectoryInfo() string {
|
||||
var b strings.Builder
|
||||
|
||||
AppendDirectoryInfo(&b)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// LogDirectoryInfo logs nuclei config/state/cache/PDCP directory paths at info level.
|
||||
func LogDirectoryInfo(logger *gologger.Logger) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range directoryRecords() {
|
||||
logger.Info().Msgf("%s: %s", record.label, record.path)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
||||
)
|
||||
|
||||
type directoryLogWriter struct {
|
||||
records []string
|
||||
}
|
||||
|
||||
func (w *directoryLogWriter) Write(data []byte, _ levels.Level) {
|
||||
w.records = append(w.records, string(data))
|
||||
}
|
||||
|
||||
func TestDirectoryInfoContainsKnownLabels(t *testing.T) {
|
||||
info := DirectoryInfo()
|
||||
cfg := config.DefaultConfig
|
||||
|
||||
require.Equal(t, strings.Join([]string{
|
||||
fmt.Sprintf("Nuclei Config Directory: %s", cfg.GetConfigDir()),
|
||||
fmt.Sprintf("Nuclei State Directory: %s", cfg.GetStateDir()),
|
||||
fmt.Sprintf("Nuclei Cache Directory: %s", cfg.GetCacheDir()),
|
||||
fmt.Sprintf("PDCP Directory: %s", pdcpauth.PDCPDir),
|
||||
"",
|
||||
}, "\n"), info)
|
||||
require.Equal(t, 4, strings.Count(info, "\n"))
|
||||
}
|
||||
|
||||
func TestLogDirectoryInfoContainsKnownLabels(t *testing.T) {
|
||||
writer := &directoryLogWriter{}
|
||||
logger := &gologger.Logger{}
|
||||
logger.SetFormatter(formatter.NewCLI(true))
|
||||
logger.SetWriter(writer)
|
||||
logger.SetMaxLevel(levels.LevelInfo)
|
||||
|
||||
LogDirectoryInfo(logger)
|
||||
|
||||
require.Equal(t, []string{
|
||||
fmt.Sprintf("[INF] Nuclei Config Directory: %s", config.DefaultConfig.GetConfigDir()),
|
||||
fmt.Sprintf("[INF] Nuclei State Directory: %s", config.DefaultConfig.GetStateDir()),
|
||||
fmt.Sprintf("[INF] Nuclei Cache Directory: %s", config.DefaultConfig.GetCacheDir()),
|
||||
fmt.Sprintf("[INF] PDCP Directory: %s", pdcpauth.PDCPDir),
|
||||
}, writer.records)
|
||||
}
|
||||
@@ -20,34 +20,19 @@ func DoHealthCheck(options *types.Options) string {
|
||||
fmt.Fprintf(&test, "Architecture: %s\n", runtime.GOARCH)
|
||||
fmt.Fprintf(&test, "Go Version: %s\n", runtime.Version())
|
||||
fmt.Fprintf(&test, "Compiler: %s\n", runtime.Compiler)
|
||||
AppendDirectoryInfo(&test)
|
||||
|
||||
var testResult string
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
if err := cfg.InitializationError(); err != nil {
|
||||
fmt.Fprintf(&test, "Configuration initialization => Ko (%s)\n", err)
|
||||
} else {
|
||||
fmt.Fprintln(&test, "Configuration initialization => Ok")
|
||||
}
|
||||
|
||||
for _, filename := range []string{
|
||||
cfg.GetFlagsConfigFilePath(),
|
||||
cfg.GetTemplatesStateFilePath(),
|
||||
cfg.GetActiveIgnoreFilePath(),
|
||||
cfg.GetChecksumFilePath(),
|
||||
} {
|
||||
for _, filename := range []string{cfg.GetFlagsConfigFilePath(), cfg.GetIgnoreFilePath(), cfg.GetChecksumFilePath()} {
|
||||
ok, err := fileutil.IsReadable(filename)
|
||||
if ok {
|
||||
testResult = "Ok"
|
||||
} else {
|
||||
testResult = "Ko"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
testResult += fmt.Sprintf(" (%s)", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&test, "File \"%s\" Read => %s\n", filename, testResult)
|
||||
ok, err = fileutil.IsWriteable(filename)
|
||||
if ok {
|
||||
@@ -55,48 +40,37 @@ func DoHealthCheck(options *types.Options) string {
|
||||
} else {
|
||||
testResult = "Ko"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
testResult += fmt.Sprintf(" (%s)", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&test, "File \"%s\" Write => %s\n", filename, testResult)
|
||||
}
|
||||
|
||||
c4, err := net.Dial("tcp4", "scanme.sh:80")
|
||||
if err == nil && c4 != nil {
|
||||
_ = c4.Close()
|
||||
}
|
||||
|
||||
testResult = "Ok"
|
||||
if err != nil {
|
||||
testResult = fmt.Sprintf("Ko (%s)", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&test, "IPv4 connectivity to scanme.sh:80 => %s\n", testResult)
|
||||
|
||||
c6, err := net.Dial("tcp6", "scanme.sh:80")
|
||||
if err == nil && c6 != nil {
|
||||
_ = c6.Close()
|
||||
}
|
||||
|
||||
testResult = "Ok"
|
||||
if err != nil {
|
||||
testResult = fmt.Sprintf("Ko (%s)", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&test, "IPv6 connectivity to scanme.sh:80 => %s\n", testResult)
|
||||
|
||||
u4, err := net.Dial("udp4", "scanme.sh:53")
|
||||
if err == nil && u4 != nil {
|
||||
_ = u4.Close()
|
||||
}
|
||||
|
||||
testResult = "Ok"
|
||||
if err != nil {
|
||||
testResult = fmt.Sprintf("Ko (%s)", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&test, "IPv4 UDP connectivity to scanme.sh:53 => %s\n", testResult)
|
||||
|
||||
return test.String()
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/hmap/store/hybrid"
|
||||
"github.com/projectdiscovery/httpx/common/httpx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
@@ -39,9 +38,6 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
|
||||
}
|
||||
httpxOptions.RetryMax = r.options.Retries
|
||||
httpxOptions.Timeout = time.Duration(r.options.Timeout) * time.Second
|
||||
if len(r.options.InternalResolversList) > 0 {
|
||||
httpxOptions.Resolvers = r.options.InternalResolversList
|
||||
}
|
||||
|
||||
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
|
||||
if dialers == nil {
|
||||
@@ -72,15 +68,12 @@ func (r *Runner) initializeTemplatesHTTPInput() (*hybrid.HybridMap, error) {
|
||||
}
|
||||
|
||||
swg.Add()
|
||||
go func(target *contextargs.MetaInput) {
|
||||
go func(input *contextargs.MetaInput) {
|
||||
defer swg.Done()
|
||||
|
||||
// a pinned target is probed through its ip, and each ip keeps its
|
||||
// own result, as servers of one host may serve different schemes
|
||||
ctx := contextargs.WithCustomIP(context.Background(), target)
|
||||
if result := utils.ProbeURLWithContext(ctx, target.Input, httpxClient); result != "" {
|
||||
if result := utils.ProbeURL(input.Input, httpxClient); result != "" {
|
||||
count.Add(1)
|
||||
_ = hm.Set(input.ProbeKey(target.Input, target.CustomIP), []byte(result))
|
||||
_ = hm.Set(input.Input, []byte(result))
|
||||
}
|
||||
}(value)
|
||||
return true
|
||||
|
||||
+226
-71
@@ -1,11 +1,16 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider/authx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider/autologin"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
@@ -15,9 +20,7 @@ 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"
|
||||
@@ -39,30 +42,25 @@ func GetAuthTmplStore(opts *types.Options, catalog catalog.Catalog, execOpts *pr
|
||||
}
|
||||
tmpls = append(tmpls, data...)
|
||||
}
|
||||
// The auth store only wants the secret-file templates. Copy first so
|
||||
// this scoping cannot clear Templates, ExcludeTags, or the other filters
|
||||
// on the caller's options. The CLI already copies; the SDK passes the
|
||||
// engine options directly.
|
||||
scoped := opts.Copy()
|
||||
scoped.Templates = tmpls
|
||||
scoped.Workflows = nil
|
||||
scoped.RemoteTemplateDomainList = nil
|
||||
scoped.TemplateURLs = nil
|
||||
scoped.WorkflowURLs = nil
|
||||
scoped.ExcludedTemplates = nil
|
||||
scoped.Tags = nil
|
||||
scoped.ExcludeTags = nil
|
||||
scoped.IncludeTemplates = nil
|
||||
scoped.Authors = nil
|
||||
scoped.Severities = nil
|
||||
scoped.ExcludeSeverities = nil
|
||||
scoped.IncludeTags = nil
|
||||
scoped.IncludeIds = nil
|
||||
scoped.ExcludeIds = nil
|
||||
scoped.Protocols = nil
|
||||
scoped.ExcludeProtocols = nil
|
||||
scoped.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(scoped, catalog, execOpts)
|
||||
opts.Templates = tmpls
|
||||
opts.Workflows = nil
|
||||
opts.RemoteTemplateDomainList = nil
|
||||
opts.TemplateURLs = nil
|
||||
opts.WorkflowURLs = nil
|
||||
opts.ExcludedTemplates = nil
|
||||
opts.Tags = nil
|
||||
opts.ExcludeTags = nil
|
||||
opts.IncludeTemplates = nil
|
||||
opts.Authors = nil
|
||||
opts.Severities = nil
|
||||
opts.ExcludeSeverities = nil
|
||||
opts.IncludeTags = nil
|
||||
opts.IncludeIds = nil
|
||||
opts.ExcludeIds = nil
|
||||
opts.Protocols = nil
|
||||
opts.ExcludeProtocols = nil
|
||||
opts.IncludeConditions = nil
|
||||
cfg := loader.NewConfig(opts, catalog, execOpts)
|
||||
cfg.StoreId = loader.AuthStoreId
|
||||
store, err := loader.New(cfg)
|
||||
if err != nil {
|
||||
@@ -71,6 +69,208 @@ func GetAuthTmplStore(opts *types.Options, catalog catalog.Catalog, execOpts *pr
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// captureOnceStoreFromOptions launches a visible browser at -auth-login-url,
|
||||
// lets the user log in manually (pressing Enter in the terminal when done) and
|
||||
// builds an in-memory static auth store from the captured session (cookies and
|
||||
// an optional token), scoped to the login host. Unlike auto-login this captures
|
||||
// a point-in-time session with no automated re-authentication.
|
||||
func captureOnceStoreFromOptions(opts *types.Options) (*authx.Authx, error) {
|
||||
u, err := url.Parse(opts.AuthLoginURL)
|
||||
if err != nil {
|
||||
return nil, errkit.Wrap(err, "invalid -auth-login-url")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return nil, errkit.New("capture-once: -auth-login-url is required and must include a host")
|
||||
}
|
||||
|
||||
rt := buildAutoLoginRuntimeOptions(opts)
|
||||
cfg := autologin.Config{
|
||||
LoginURL: opts.AuthLoginURL,
|
||||
Proxy: rt.Proxy,
|
||||
CDPEndpoint: rt.CDPEndpoint,
|
||||
UseInstalledChrome: rt.UseInstalledChrome,
|
||||
UserAgent: rt.UserAgent,
|
||||
CustomHeaders: rt.CustomHeaders,
|
||||
}
|
||||
|
||||
ready := func() error {
|
||||
fmt.Fprintf(os.Stderr, "[auth-capture] Log in to %s in the opened browser window, then press Enter here to capture the session...", opts.AuthLoginURL)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
_, rerr := reader.ReadString('\n')
|
||||
return rerr
|
||||
}
|
||||
|
||||
session, err := autologin.CaptureOnce(context.Background(), cfg, ready)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return captureSessionToStore(session, u.Host)
|
||||
}
|
||||
|
||||
// captureSessionToStore converts a captured login session into a static auth
|
||||
// store scoped to host: cookies as a Cookie secret, an extracted token as a
|
||||
// BearerToken secret, and any web storage as a WebStorage secret (replayed into
|
||||
// headless scan pages). It is split out from captureOnceStoreFromOptions so the
|
||||
// session->store mapping is unit-testable without driving a browser.
|
||||
func captureSessionToStore(session *autologin.Session, host string) (*authx.Authx, error) {
|
||||
if session == nil {
|
||||
return nil, errkit.New("capture-once: nil session")
|
||||
}
|
||||
var secrets []authx.Secret
|
||||
if len(session.Cookies) > 0 {
|
||||
cookies := make([]authx.Cookie, 0, len(session.Cookies))
|
||||
for _, c := range session.Cookies {
|
||||
cookies = append(cookies, authx.Cookie{Key: c.Name, Value: c.Value})
|
||||
}
|
||||
secrets = append(secrets, authx.Secret{
|
||||
Type: string(authx.CookiesAuth),
|
||||
Domains: []string{host},
|
||||
Cookies: cookies,
|
||||
})
|
||||
}
|
||||
if session.Token != "" {
|
||||
secrets = append(secrets, authx.Secret{
|
||||
Type: string(authx.BearerTokenAuth),
|
||||
Domains: []string{host},
|
||||
Token: session.Token,
|
||||
})
|
||||
}
|
||||
if len(session.LocalStorage) > 0 || len(session.SessionStorage) > 0 {
|
||||
// Carry captured web storage so a token-in-web-storage SPA session
|
||||
// replays into headless scan pages (HTTP scans ignore this secret).
|
||||
secrets = append(secrets, authx.Secret{
|
||||
Type: string(authx.WebStorageAuth),
|
||||
Domains: []string{host},
|
||||
LocalStorage: session.LocalStorage,
|
||||
SessionStorage: session.SessionStorage,
|
||||
})
|
||||
}
|
||||
if len(secrets) == 0 {
|
||||
return nil, errkit.New("capture-once: captured session had no replayable cookies, token or web storage")
|
||||
}
|
||||
for i := range secrets {
|
||||
if err := secrets[i].Validate(); err != nil {
|
||||
return nil, errkit.Wrap(err, "capture-once: invalid captured secret")
|
||||
}
|
||||
}
|
||||
return &authx.Authx{ID: "cli-capture-once", Secrets: secrets}, nil
|
||||
}
|
||||
|
||||
// buildAutoLoginRuntimeOptions maps scan-level options into the auto-login
|
||||
// runtime options so a (headless) auto-login uses the same identity and network
|
||||
// path as the scan: user-agent and custom headers (-H), proxy, CDP endpoint and
|
||||
// Chrome settings.
|
||||
func buildAutoLoginRuntimeOptions(opts *types.Options) *authx.AutoLoginRuntimeOptions {
|
||||
// Mirror the proxy precedence used everywhere else in the codebase (HTTP
|
||||
// proxy first, SOCKS as fallback) so a SOCKS-proxied scan does not silently
|
||||
// bypass the proxy during auto-login.
|
||||
proxy := opts.AliveHttpProxy
|
||||
if proxy == "" {
|
||||
proxy = opts.AliveSocksProxy
|
||||
}
|
||||
rt := &authx.AutoLoginRuntimeOptions{
|
||||
Proxy: proxy,
|
||||
CDPEndpoint: opts.CDPEndpoint,
|
||||
UseInstalledChrome: opts.UseInstalledChrome,
|
||||
ShowBrowser: opts.ShowBrowser,
|
||||
}
|
||||
for _, header := range opts.CustomHeaders {
|
||||
parts := strings.SplitN(header, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
if key == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(key, "User-Agent") {
|
||||
rt.UserAgent = value
|
||||
continue
|
||||
}
|
||||
if rt.CustomHeaders == nil {
|
||||
rt.CustomHeaders = map[string]string{}
|
||||
}
|
||||
rt.CustomHeaders[key] = value
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
// autoLoginStoreFromOptions assembles an in-memory Authx store containing a
|
||||
// single auto-login dynamic secret built from the -auth-login-url flag set. The
|
||||
// captured session is scoped to the login URL's host.
|
||||
func autoLoginStoreFromOptions(opts *types.Options) (*authx.Authx, error) {
|
||||
// The login URL may be supplied directly (-auth-login-url) or derived from a
|
||||
// recording's first navigate step; resolve the host scope from whichever is
|
||||
// available.
|
||||
loginURL := opts.AuthLoginURL
|
||||
if loginURL == "" && opts.AuthRecording != "" {
|
||||
steps, err := autologin.StepsFromRecordingFile(opts.AuthRecording, opts.AuthUsername, opts.AuthPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loginURL = autologin.FirstNavigateURL(steps)
|
||||
}
|
||||
u, err := url.Parse(loginURL)
|
||||
if err != nil {
|
||||
return nil, errkit.Wrap(err, "invalid auto-login url")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return nil, errkit.New("auto-login: could not determine host (set -auth-login-url or provide a recording with a navigate step)")
|
||||
}
|
||||
reauthCodes, err := parseStatusCodes(opts.AuthReauthStatusCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &authx.Authx{
|
||||
ID: "cli-auto-login",
|
||||
Dynamic: []authx.Dynamic{
|
||||
{
|
||||
Secret: &authx.Secret{Domains: []string{u.Host}},
|
||||
// Session lifecycle: re-run the login on the configured expiry
|
||||
// status codes and/or once the session ages past the interval, so
|
||||
// a long scan does not silently continue with a dead session.
|
||||
RefreshInterval: opts.AuthRefreshInterval,
|
||||
ReauthStatusCodes: reauthCodes,
|
||||
AutoLogin: &authx.AutoLoginConfig{
|
||||
LoginURL: loginURL,
|
||||
Username: opts.AuthUsername,
|
||||
Password: opts.AuthPassword,
|
||||
UsernameField: opts.AuthUsernameField,
|
||||
PasswordField: opts.AuthPasswordField,
|
||||
Headless: opts.AuthHeadless,
|
||||
Recording: opts.AuthRecording,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseStatusCodes parses a comma-separated list of HTTP status codes (e.g.
|
||||
// "401,403") into a slice of ints. An empty string yields a nil slice.
|
||||
func parseStatusCodes(raw string) ([]int, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var codes []int
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
code, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return nil, errkit.Wrapf(err, "invalid -auth-reauth-status-codes value %q", part)
|
||||
}
|
||||
if code < 100 || code > 599 {
|
||||
return nil, errkit.New("invalid -auth-reauth-status-codes value %q: must be a valid HTTP status code", part)
|
||||
}
|
||||
codes = append(codes, code)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// GetLazyAuthFetchCallback returns a lazy fetch callback for auth secrets
|
||||
func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret {
|
||||
return func(d *authx.Dynamic) error {
|
||||
@@ -86,8 +286,6 @@ 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()
|
||||
@@ -158,46 +356,3 @@ 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider/authx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider/autologin"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCaptureSessionToStore(t *testing.T) {
|
||||
session := &autologin.Session{
|
||||
Cookies: []*http.Cookie{{Name: "session", Value: "sess-dave"}},
|
||||
Token: "eyJ.payload.sig",
|
||||
LocalStorage: map[string]string{"jwt": "eyJ.payload.sig"},
|
||||
SessionStorage: map[string]string{"csrf": "tok"},
|
||||
}
|
||||
store, err := captureSessionToStore(session, "app.example.com")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, store.Secrets, 3, "cookies, token and web storage must each map to a secret")
|
||||
|
||||
byType := map[string]authx.Secret{}
|
||||
for _, s := range store.Secrets {
|
||||
byType[s.Type] = s
|
||||
require.Equal(t, []string{"app.example.com"}, s.Domains, "every secret is scoped to the login host")
|
||||
require.NoError(t, s.Validate())
|
||||
}
|
||||
|
||||
require.Equal(t, "session", byType[string(authx.CookiesAuth)].Cookies[0].Key)
|
||||
require.Equal(t, "eyJ.payload.sig", byType[string(authx.BearerTokenAuth)].Token)
|
||||
|
||||
ws := byType[string(authx.WebStorageAuth)]
|
||||
require.Equal(t, "eyJ.payload.sig", ws.LocalStorage["jwt"])
|
||||
require.Equal(t, "tok", ws.SessionStorage["csrf"])
|
||||
|
||||
// The web storage secret must surface as a BrowserStorageProvider so the
|
||||
// headless engine can seed it into scanned pages.
|
||||
bsp, ok := ws.GetStrategy().(authx.BrowserStorageProvider)
|
||||
require.True(t, ok)
|
||||
local, sess := bsp.WebStorage()
|
||||
require.Equal(t, "eyJ.payload.sig", local["jwt"])
|
||||
require.Equal(t, "tok", sess["csrf"])
|
||||
}
|
||||
|
||||
func TestCaptureSessionToStore_StorageOnly(t *testing.T) {
|
||||
// A pure token-in-localStorage SPA: no cookies, no extracted token, only
|
||||
// web storage — it must still produce a usable store.
|
||||
session := &autologin.Session{LocalStorage: map[string]string{"jwt": "x"}}
|
||||
store, err := captureSessionToStore(session, "spa.example.com")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, store.Secrets, 1)
|
||||
require.Equal(t, string(authx.WebStorageAuth), store.Secrets[0].Type)
|
||||
}
|
||||
|
||||
func TestCaptureSessionToStore_Empty(t *testing.T) {
|
||||
_, err := captureSessionToStore(&autologin.Session{}, "app.example.com")
|
||||
require.Error(t, err, "an empty session must not produce a store")
|
||||
}
|
||||
|
||||
func TestAutoLoginStoreFromOptions_RecordingDerivesHost(t *testing.T) {
|
||||
recording := `{"steps": [
|
||||
{"type": "navigate", "url": "https://recorded.example.com/signin"},
|
||||
{"type": "change", "value": "bob", "selectors": [["#user"]]}
|
||||
]}`
|
||||
path := filepath.Join(t.TempDir(), "flow.json")
|
||||
require.NoError(t, os.WriteFile(path, []byte(recording), 0o600))
|
||||
|
||||
// No -auth-login-url: the host scope must come from the recording.
|
||||
opts := &types.Options{AuthRecording: path, AuthUsername: "bob", AuthPassword: "p"}
|
||||
store, err := autoLoginStoreFromOptions(opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, store.Dynamic, 1)
|
||||
require.Equal(t, []string{"recorded.example.com"}, store.Dynamic[0].Domains)
|
||||
require.Equal(t, path, store.Dynamic[0].AutoLogin.Recording)
|
||||
}
|
||||
|
||||
func TestAutoLoginStoreFromOptions(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
AuthLoginURL: "https://app.example.com:8443/login",
|
||||
AuthUsername: "alice",
|
||||
AuthPassword: "s3cr3t",
|
||||
AuthUsernameField: "email",
|
||||
AuthHeadless: true,
|
||||
}
|
||||
store, err := autoLoginStoreFromOptions(opts)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, store.Dynamic, 1)
|
||||
|
||||
dyn := store.Dynamic[0]
|
||||
require.NotNil(t, dyn.Secret)
|
||||
require.Equal(t, []string{"app.example.com:8443"}, dyn.Domains, "session should be scoped to the login host")
|
||||
require.NotNil(t, dyn.AutoLogin)
|
||||
require.Equal(t, "https://app.example.com:8443/login", dyn.AutoLogin.LoginURL)
|
||||
require.Equal(t, "alice", dyn.AutoLogin.Username)
|
||||
require.Equal(t, "email", dyn.AutoLogin.UsernameField)
|
||||
require.True(t, dyn.AutoLogin.Headless)
|
||||
|
||||
// The assembled store must validate as a real auto-login dynamic.
|
||||
require.NoError(t, dyn.Validate())
|
||||
}
|
||||
|
||||
func TestAutoLoginStoreFromOptions_InvalidURL(t *testing.T) {
|
||||
_, err := autoLoginStoreFromOptions(&types.Options{AuthLoginURL: "://nohost"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAutoLoginStoreFromOptions_SessionLifecycle(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
AuthLoginURL: "https://app.example.com/login",
|
||||
AuthUsername: "alice",
|
||||
AuthPassword: "s3cr3t",
|
||||
AuthReauthStatusCodes: "401, 403",
|
||||
AuthRefreshInterval: "15m",
|
||||
}
|
||||
store, err := autoLoginStoreFromOptions(opts)
|
||||
require.NoError(t, err)
|
||||
dyn := store.Dynamic[0]
|
||||
require.Equal(t, []int{401, 403}, dyn.ReauthStatusCodes, "reauth status codes must be threaded onto the dynamic")
|
||||
require.Equal(t, "15m", dyn.RefreshInterval)
|
||||
// The lifecycle-configured store must validate (refresh-interval is parsed).
|
||||
require.NoError(t, dyn.Validate())
|
||||
}
|
||||
|
||||
func TestAutoLoginStoreFromOptions_InvalidReauthCodes(t *testing.T) {
|
||||
_, err := autoLoginStoreFromOptions(&types.Options{
|
||||
AuthLoginURL: "https://app.example.com/login",
|
||||
AuthPassword: "s3cr3t",
|
||||
AuthReauthStatusCodes: "401,nope",
|
||||
})
|
||||
require.Error(t, err, "non-numeric reauth status code must be rejected")
|
||||
}
|
||||
|
||||
func TestParseStatusCodes(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want []int
|
||||
wantErr bool
|
||||
}{
|
||||
{"", nil, false},
|
||||
{" ", nil, false},
|
||||
{"401", []int{401}, false},
|
||||
{"401,403, 419", []int{401, 403, 419}, false},
|
||||
{"401,,403", []int{401, 403}, false},
|
||||
{"abc", nil, true},
|
||||
{"99", nil, true},
|
||||
{"600", nil, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got, err := parseStatusCodes(tc.in)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err, "input %q", tc.in)
|
||||
continue
|
||||
}
|
||||
require.NoError(t, err, "input %q", tc.in)
|
||||
require.Equal(t, tc.want, got, "input %q", tc.in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAutoLoginRuntimeOptions(t *testing.T) {
|
||||
opts := &types.Options{
|
||||
CustomHeaders: []string{"User-Agent: NucleiScan/1.0", "X-Env: staging", "malformed-header"},
|
||||
AliveHttpProxy: "http://127.0.0.1:8080",
|
||||
CDPEndpoint: "ws://127.0.0.1:9222",
|
||||
UseInstalledChrome: true,
|
||||
ShowBrowser: true,
|
||||
}
|
||||
rt := buildAutoLoginRuntimeOptions(opts)
|
||||
require.Equal(t, "NucleiScan/1.0", rt.UserAgent, "User-Agent should be split out of custom headers")
|
||||
require.Equal(t, "staging", rt.CustomHeaders["X-Env"])
|
||||
require.NotContains(t, rt.CustomHeaders, "User-Agent", "UA must not be duplicated into custom headers")
|
||||
require.Equal(t, "http://127.0.0.1:8080", rt.Proxy)
|
||||
require.Equal(t, "ws://127.0.0.1:9222", rt.CDPEndpoint)
|
||||
require.True(t, rt.UseInstalledChrome)
|
||||
require.True(t, rt.ShowBrowser)
|
||||
}
|
||||
|
||||
func TestBuildAutoLoginRuntimeOptions_SocksProxyFallback(t *testing.T) {
|
||||
// With only a SOCKS proxy configured, the auto-login must still pick it up
|
||||
// (regression: previously only AliveHttpProxy was threaded).
|
||||
rt := buildAutoLoginRuntimeOptions(&types.Options{
|
||||
AliveSocksProxy: "socks5://127.0.0.1:1080",
|
||||
})
|
||||
require.Equal(t, "socks5://127.0.0.1:1080", rt.Proxy)
|
||||
|
||||
// HTTP proxy takes precedence when both are set.
|
||||
rt = buildAutoLoginRuntimeOptions(&types.Options{
|
||||
AliveHttpProxy: "http://127.0.0.1:8080",
|
||||
AliveSocksProxy: "socks5://127.0.0.1:1080",
|
||||
})
|
||||
require.Equal(t, "http://127.0.0.1:8080", rt.Proxy)
|
||||
}
|
||||
@@ -6,9 +6,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -140,27 +137,3 @@ 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")
|
||||
})
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
utilsllm "github.com/projectdiscovery/utils/llm"
|
||||
)
|
||||
|
||||
// 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},
|
||||
})
|
||||
}
|
||||
|
||||
// llmJudgeClient is an llmMatcherClient whose provider classifies natively, so
|
||||
// llm matchers ask it through Judge instead of Complete.
|
||||
type llmJudgeClient struct {
|
||||
*llmMatcherClient
|
||||
warnComplete sync.Once
|
||||
}
|
||||
|
||||
func (a *llmJudgeClient) Judge(ctx context.Context, question string, labels, inputs []string) (llmclient.Verdict, error) {
|
||||
verdict, err := a.client.Judge(ctx, utilsllm.JudgeRequest{Question: question, Labels: labels, Inputs: inputs})
|
||||
if err != nil {
|
||||
return llmclient.Verdict{}, err
|
||||
}
|
||||
return llmclient.Verdict{Label: verdict.Label, Confidence: verdict.Confidence, Model: verdict.Model}, nil
|
||||
}
|
||||
|
||||
// Complete is only reached by llm extractors, which need free text a
|
||||
// classifier cannot produce. Returning here spends no call budget and
|
||||
// makes no provider request. Extractors turn the error into an empty
|
||||
// result, and the warning says why.
|
||||
func (a *llmJudgeClient) Complete(context.Context, string, bool) (string, error) {
|
||||
a.warnComplete.Do(func() {
|
||||
gologger.Warning().Msgf("llm provider %q cannot run llm extractors, they return nothing", utilsllm.ProviderJev)
|
||||
})
|
||||
return "", errors.New("llm provider cannot run extractors")
|
||||
}
|
||||
|
||||
// configureLLM builds the scan's llm client from options. It returns nil unless
|
||||
// -llm is set, so nothing reaches a model until the user opts in. The API key is
|
||||
// read from the environment by the provider layer, never from options.
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
matcherClient := &llmMatcherClient{client: client, model: options.LLMModel}
|
||||
if client.CanJudge() {
|
||||
return &llmJudgeClient{llmMatcherClient: matcherClient}, nil
|
||||
}
|
||||
return matcherClient, nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigureLLMExposesJudgeOnlyForClassifiers(t *testing.T) {
|
||||
t.Setenv("LLM_API_KEY", "key")
|
||||
|
||||
client, err := configureLLM(&types.Options{EnableLLM: true, LLMProvider: "jev", LLMModel: "jev-latest"})
|
||||
require.NoError(t, err)
|
||||
require.Implements(t, (*llmclient.Judge)(nil), client)
|
||||
|
||||
client, err = configureLLM(&types.Options{EnableLLM: true, LLMProvider: "ollama", LLMModel: "llama3.2"})
|
||||
require.NoError(t, err)
|
||||
_, isJudge := client.(llmclient.Judge)
|
||||
require.False(t, isJudge, "chat providers keep the Complete path")
|
||||
}
|
||||
|
||||
func TestJevExtractorDoesNotSpendCallBudget(t *testing.T) {
|
||||
t.Setenv("LLM_API_KEY", "key")
|
||||
// ProxyFromEnvironment caches the first request in the process. Match the
|
||||
// proxy the interactsh client test expects, and keep the local server direct.
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
var calls atomic.Int32
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
_, _ = w.Write([]byte(`{"model":"jev-1.14.0","answers":{"q":{"type":"noul","noul":0.9}}}`))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client, err := configureLLM(&types.Options{
|
||||
EnableLLM: true,
|
||||
LLMProvider: "jev",
|
||||
LLMBaseURL: server.URL + "/v1",
|
||||
LLMModel: "jev-latest",
|
||||
LLMMaxCalls: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.Complete(context.Background(), "extract the title", true)
|
||||
require.ErrorContains(t, err, "cannot run extractors")
|
||||
_, err = client.Complete(context.Background(), "extract another", true)
|
||||
require.Error(t, err)
|
||||
require.Zero(t, calls.Load())
|
||||
|
||||
judge := client.(llmclient.Judge)
|
||||
verdict, err := judge.Judge(context.Background(), "login page?", nil, []string{"<form>"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "yes", verdict.Label)
|
||||
require.EqualValues(t, 1, calls.Load())
|
||||
}
|
||||
+18
-30
@@ -18,9 +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/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
@@ -57,18 +55,10 @@ func ConfigureOptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readsTargetsFromStdin reports whether targets are read from a stdin pipe.
|
||||
// Targets given with -u, -l, or uncover take its place: under cron, CI or an
|
||||
// agent stdin is often a pipe nothing writes to or closes, and reading it
|
||||
// would hold the scan until it does.
|
||||
func readsTargetsFromStdin(options *types.Options, stdinIsPipe bool) bool {
|
||||
hasOtherTargets := len(options.Targets) > 0 || options.TargetsFilePath != "" || options.Uncover || len(options.UncoverQuery) > 0
|
||||
return stdinIsPipe && !options.DisableStdin && !hasOtherTargets
|
||||
}
|
||||
|
||||
// ParseOptions parses the command line flags provided by a user
|
||||
func ParseOptions(options *types.Options) {
|
||||
options.Stdin = readsTargetsFromStdin(options, fileutil.HasStdin())
|
||||
// Check if stdin pipe was given
|
||||
options.Stdin = !options.DisableStdin && fileutil.HasStdin()
|
||||
|
||||
// Read the inputs from env variables that not passed by flag.
|
||||
readEnvInputVars(options)
|
||||
@@ -150,7 +140,7 @@ func ParseOptions(options *types.Options) {
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOptions validates the configuration options passed
|
||||
// validateOptions validates the configuration options passed
|
||||
func ValidateOptions(options *types.Options) error {
|
||||
if err := validateOptions.Struct(options); err != nil {
|
||||
if _, ok := err.(*validator.InvalidValidationError); ok {
|
||||
@@ -176,18 +166,10 @@ func ValidateOptions(options *types.Options) error {
|
||||
if options.ShouldFollowHTTPRedirects() && options.DisableRedirects {
|
||||
return errors.New("both follow redirects and disable redirects specified")
|
||||
}
|
||||
switch options.UpdateTemplatesChannel {
|
||||
case "", installer.TemplatesChannelStable, installer.TemplatesChannelLatest:
|
||||
default:
|
||||
return fmt.Errorf("invalid templates update channel %q, must be %q or %q", options.UpdateTemplatesChannel, installer.TemplatesChannelStable, installer.TemplatesChannelLatest)
|
||||
}
|
||||
// loading the proxy server list from file or cli and test the connectivity
|
||||
if err := loadProxyServers(options); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateReplayProxy(options.ReplayProxy); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.Validate {
|
||||
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
|
||||
}
|
||||
@@ -228,14 +210,25 @@ func ValidateOptions(options *types.Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
// An empty -iv leaves one unpinned target so the dialer can fall back
|
||||
// between address families. -sa resolves and scans every selected address.
|
||||
// Explicit -iv 4, -iv 6 and -iv 4,6 retain their usual selection semantics.
|
||||
// verify that a valid ip version type was selected (4, 6)
|
||||
if len(options.IPVersion) == 0 {
|
||||
// add ipv4 as default
|
||||
options.IPVersion = append(options.IPVersion, "4")
|
||||
}
|
||||
var useIPV4, useIPV6 bool
|
||||
for _, ipv := range options.IPVersion {
|
||||
if ipv != "4" && ipv != "6" {
|
||||
switch ipv {
|
||||
case "4":
|
||||
useIPV4 = true
|
||||
case "6":
|
||||
useIPV6 = true
|
||||
default:
|
||||
return fmt.Errorf("unsupported ip version: %s", ipv)
|
||||
}
|
||||
}
|
||||
if !useIPV4 && !useIPV6 {
|
||||
return errors.New("ipv4 and/or ipv6 must be selected")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -303,11 +296,6 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -50,7 +49,7 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
strsl := goflags.StringSlice{}
|
||||
for v := range strings.SplitSeq(tt.input, ",") {
|
||||
for _, v := range strings.Split(tt.input, ",") {
|
||||
//nolint
|
||||
strsl.Set(v)
|
||||
}
|
||||
@@ -60,45 +59,3 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOptionsUpdateTemplatesChannel checks that only known templates
|
||||
// update channels are accepted.
|
||||
func TestValidateOptionsUpdateTemplatesChannel(t *testing.T) {
|
||||
for _, channel := range []string{"", installer.TemplatesChannelStable, installer.TemplatesChannelLatest} {
|
||||
require.NoError(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: channel}), "channel %q", channel)
|
||||
}
|
||||
require.ErrorContains(t, ValidateOptions(&types.Options{UpdateTemplatesChannel: "nightly"}), `invalid templates update channel "nightly"`)
|
||||
}
|
||||
|
||||
func TestReadsTargetsFromStdin(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
options *types.Options
|
||||
stdinIsPipe bool
|
||||
want bool
|
||||
}{
|
||||
{"piped stdin alone", &types.Options{}, true, true},
|
||||
{"no pipe", &types.Options{}, false, false},
|
||||
{"disabled", &types.Options{DisableStdin: true}, true, false},
|
||||
{"targets given with -u", &types.Options{Targets: goflags.StringSlice{"example.com"}}, true, false},
|
||||
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
|
||||
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
|
||||
{"uncover enabled", &types.Options{Uncover: true}, true, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The default must remain empty so the provider can distinguish automatic
|
||||
// IPv6 preference from explicit user-supplied -iv selections.
|
||||
func TestValidateOptionsIPVersionPreference(t *testing.T) {
|
||||
automatic := &types.Options{}
|
||||
require.NoError(t, ValidateOptions(automatic))
|
||||
require.Empty(t, automatic.IPVersion)
|
||||
for _, versions := range []goflags.StringSlice{{"4"}, {"6"}, {"4", "6"}} {
|
||||
require.NoError(t, ValidateOptions(&types.Options{IPVersion: versions}))
|
||||
}
|
||||
require.ErrorContains(t, ValidateOptions(&types.Options{IPVersion: goflags.StringSlice{"7"}}), "unsupported ip version")
|
||||
}
|
||||
@@ -543,8 +543,9 @@ func splitPorts(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for p := range strings.SplitSeq(s, ",") {
|
||||
parts := strings.Split(s, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
|
||||
@@ -14,25 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
HTTPS_PROXY_ENV = "HTTPS_PROXY"
|
||||
HTTP_PROXY_ENV = "HTTP_PROXY"
|
||||
)
|
||||
|
||||
var proxyEnvVars = []string{
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
}
|
||||
|
||||
func setProxyEnv(proxyURL string) {
|
||||
for _, env := range proxyEnvVars {
|
||||
_ = os.Setenv(env, proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
// loadProxyServers load list of proxy servers from file or comma separated
|
||||
func loadProxyServers(options *types.Options) error {
|
||||
if len(options.Proxy) == 0 {
|
||||
@@ -69,37 +53,15 @@ func loadProxyServers(options *types.Options) error {
|
||||
return errkit.Wrapf(err, "failed to parse proxy got %v", err)
|
||||
}
|
||||
if options.ProxyInternal {
|
||||
setProxyEnv(proxyURL.String())
|
||||
_ = os.Setenv(HTTP_PROXY_ENV, proxyURL.String())
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS:
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
}
|
||||
options.Logger.Verbose().Msgf("Using %s as proxy server", proxyURL.String())
|
||||
options.AliveHttpProxy = proxyURL.String()
|
||||
case proxyutils.SOCKS5:
|
||||
options.AliveSocksProxy = proxyURL.String()
|
||||
if options.Logger != nil {
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
}
|
||||
options.Logger.Verbose().Msgf("Using %s as socket proxy server", proxyURL.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateReplayProxy rejects a -replay-proxy value the replay client cannot dial
|
||||
func validateReplayProxy(raw string) error {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
proxyURL, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "invalid replay proxy %q", raw)
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case proxyutils.HTTP, proxyutils.HTTPS, proxyutils.SOCKS5:
|
||||
if proxyURL.Host != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid replay proxy %q: want an http, https or socks5 URL", raw)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateReplayProxy(t *testing.T) {
|
||||
for _, valid := range []string{"", "http://127.0.0.1:8080", "https://proxy.example:8443", "socks5://127.0.0.1:1080"} {
|
||||
require.NoError(t, validateReplayProxy(valid), valid)
|
||||
}
|
||||
for _, invalid := range []string{"127.0.0.1:8080", "ftp://127.0.0.1:21", "http://", "://bad"} {
|
||||
require.Error(t, validateReplayProxy(invalid), invalid)
|
||||
}
|
||||
}
|
||||
|
||||
type envState struct {
|
||||
val string
|
||||
set bool
|
||||
}
|
||||
|
||||
func saveAndRestoreProxyEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
original := make(map[string]envState, len(proxyEnvVars))
|
||||
for _, env := range proxyEnvVars {
|
||||
val, set := os.LookupEnv(env)
|
||||
original[env] = envState{val: val, set: set}
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for env, state := range original {
|
||||
if state.set {
|
||||
_ = os.Setenv(env, state.val)
|
||||
} else {
|
||||
_ = os.Unsetenv(env)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetProxyEnv(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
testProxy := "http://127.0.0.1:8080"
|
||||
setProxyEnv(testProxy)
|
||||
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, testProxy, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProxyServers_ProxyInternal(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("proxy internal enabled sets all env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: true,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Equal(t, server.URL, os.Getenv(env), "expected %s to be set", env)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("proxy internal disabled does not set env vars", func(t *testing.T) {
|
||||
saveAndRestoreProxyEnv(t)
|
||||
|
||||
options := &types.Options{
|
||||
Proxy: goflags.StringSlice{server.URL},
|
||||
ProxyInternal: false,
|
||||
Timeout: 5,
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
err := loadProxyServers(options)
|
||||
require.NoError(t, err)
|
||||
for _, env := range proxyEnvVars {
|
||||
require.Empty(t, os.Getenv(env), "expected %s not to be set", env)
|
||||
}
|
||||
})
|
||||
}
|
||||
+118
-210
@@ -3,12 +3,9 @@ package runner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -17,7 +14,6 @@ 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"
|
||||
@@ -49,7 +45,6 @@ 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"
|
||||
@@ -58,7 +53,6 @@ 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"
|
||||
@@ -67,7 +61,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
@@ -89,7 +82,7 @@ var (
|
||||
// Runner is a client for running the enumeration process.
|
||||
type Runner struct {
|
||||
output output.Writer
|
||||
interactsh interactsh.IClient
|
||||
interactsh *interactsh.Client
|
||||
options *types.Options
|
||||
projectFile *projectfile.ProjectFile
|
||||
catalog catalog.Catalog
|
||||
@@ -115,7 +108,7 @@ type Runner struct {
|
||||
httpApiEndpoint *httpapi.Server
|
||||
fuzzStats *fuzzStats.Tracker
|
||||
dastServer *server.DASTServer
|
||||
llmClient llmclient.Client
|
||||
authProvider authprovider.AuthProvider
|
||||
}
|
||||
|
||||
// New creates a new client for running the enumeration process.
|
||||
@@ -125,31 +118,16 @@ func New(options *types.Options) (*Runner, error) {
|
||||
Logger: options.Logger,
|
||||
}
|
||||
|
||||
if err := config.DefaultConfig.InitializationError(); err != nil && !options.HealthCheck {
|
||||
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)
|
||||
}
|
||||
|
||||
if options.Verbose || options.VerboseVerbose {
|
||||
LogDirectoryInfo(runner.Logger)
|
||||
}
|
||||
|
||||
// Version check by default
|
||||
if config.DefaultConfig.CanCheckForUpdates() {
|
||||
latestIgnoreHash, versionCheckErr := installer.NucleiVersionCheck()
|
||||
if versionCheckErr != nil {
|
||||
if err := installer.NucleiVersionCheck(); err != nil {
|
||||
if options.Verbose || options.Debug {
|
||||
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", versionCheckErr)
|
||||
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,34 +142,26 @@ func New(options *types.Options) (*Runner, error) {
|
||||
tm := &installer.TemplateManager{
|
||||
CustomTemplates: ctm,
|
||||
DisablePublicTemplates: options.PublicTemplateDisableDownload,
|
||||
Channel: options.UpdateTemplatesChannel,
|
||||
}
|
||||
templatesVersion := config.DefaultConfig.TemplateVersion
|
||||
if err := tm.FreshInstallIfNotExists(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to install nuclei templates: %s\n", err)
|
||||
runner.Logger.Warning().Msgf("failed to install nuclei templates: %s\n", err)
|
||||
}
|
||||
if err := tm.UpdateIfOutdated(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to update nuclei templates: %s\n", err)
|
||||
runner.Logger.Warning().Msgf("failed to update nuclei templates: %s\n", err)
|
||||
}
|
||||
|
||||
if config.DefaultConfig.IgnoreFileNeedsUpdate(latestIgnoreHash) {
|
||||
if config.DefaultConfig.NeedsIgnoreFileUpdate() {
|
||||
if err := installer.UpdateIgnoreFile(); err != nil {
|
||||
runner.Logger.Warning().Msgf("Failed to update .nuclei-ignore file: %s\n", err)
|
||||
runner.Logger.Warning().Msgf("failed to update nuclei ignore file: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if options.UpdateTemplates {
|
||||
// we automatically check for updates unless explicitly disabled
|
||||
// this print statement is only to inform the user that there are no updates
|
||||
noUpdates := !config.DefaultConfig.NeedsTemplateUpdate()
|
||||
if tm.Channel == installer.TemplatesChannelLatest {
|
||||
// the latest channel updates without a new release
|
||||
noUpdates = config.DefaultConfig.TemplateVersion == templatesVersion
|
||||
}
|
||||
if noUpdates {
|
||||
if !config.DefaultConfig.NeedsTemplateUpdate() {
|
||||
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
|
||||
}
|
||||
|
||||
// manually trigger update of custom templates
|
||||
if ctm != nil {
|
||||
ctm.Update(context.TODO())
|
||||
@@ -208,7 +178,6 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if options.Validate {
|
||||
parser.ShouldValidate = true
|
||||
}
|
||||
|
||||
// TODO: refactor to pass options reference globally without cycles
|
||||
parser.NoStrictSyntax = options.NoStrictSyntax
|
||||
runner.parser = parser
|
||||
@@ -221,21 +190,18 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if engine.MustDisableSandbox() {
|
||||
runner.Logger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox\n")
|
||||
}
|
||||
|
||||
browser, err := engine.New(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runner.browser = browser
|
||||
}
|
||||
|
||||
runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
|
||||
var httpclient *retryablehttp.Client
|
||||
if options.ProxyInternal && (options.AliveHttpProxy != "" || options.AliveSocksProxy != "") {
|
||||
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
|
||||
var err error
|
||||
|
||||
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -245,12 +211,10 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if err := reporting.CreateConfigIfNotExists(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reportingOptions, err := createReportingOptions(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if reportingOptions != nil && httpclient != nil {
|
||||
reportingOptions.HttpClient = httpclient
|
||||
}
|
||||
@@ -260,7 +224,6 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create issue reporting client")
|
||||
}
|
||||
|
||||
runner.issuesClient = client
|
||||
}
|
||||
|
||||
@@ -323,12 +286,10 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create output file")
|
||||
}
|
||||
|
||||
if hpDetector != nil {
|
||||
outputWriter.SetHoneypotDetector(hpDetector)
|
||||
runner.honeypotDetector = hpDetector
|
||||
}
|
||||
|
||||
// setup a proxy writer to automatically upload results to PDCP
|
||||
runner.output = runner.setupPDCPUpload(outputWriter)
|
||||
if options.HTTPStats {
|
||||
@@ -339,11 +300,9 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if options.JSONL && options.EnableProgressBar {
|
||||
options.StatsJSON = true
|
||||
}
|
||||
|
||||
if options.StatsJSON {
|
||||
options.EnableProgressBar = true
|
||||
}
|
||||
|
||||
// Creates the progress tracking object
|
||||
var progressErr error
|
||||
statsInterval := options.StatsInterval
|
||||
@@ -369,30 +328,25 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(file, &resumeCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resumeCfg.Compile()
|
||||
}
|
||||
|
||||
runner.resumeCfg = resumeCfg
|
||||
|
||||
if options.DASTReport || options.DASTServer || options.DASTProxy {
|
||||
if options.DASTReport || options.DASTServer {
|
||||
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 && !options.DASTProxy {
|
||||
if !options.DASTServer {
|
||||
dastServer, err := server.NewStatsServer(runner.fuzzStats)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create dast server")
|
||||
}
|
||||
|
||||
runner.dastServer = dastServer
|
||||
}
|
||||
}
|
||||
@@ -402,7 +356,6 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if request.Error == "none" || request.Error == "" {
|
||||
return
|
||||
}
|
||||
|
||||
runner.fuzzStats.RecordErrorEvent(fuzzStats.ErrorEvent{
|
||||
TemplateID: request.Template,
|
||||
URL: request.Input,
|
||||
@@ -432,9 +385,14 @@ func New(options *types.Options) (*Runner, error) {
|
||||
opts.HTTPClient = httpclient
|
||||
}
|
||||
if opts.HTTPClient == nil {
|
||||
opts.HTTPClient = newInteractshHTTPClient(options)
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
|
||||
}
|
||||
|
||||
interactshClient, err := interactsh.New(opts)
|
||||
if err != nil {
|
||||
runner.Logger.Error().Msgf("Could not create interactsh client: %s", err)
|
||||
@@ -447,11 +405,9 @@ func New(options *types.Options) (*Runner, error) {
|
||||
options.RateLimit = options.RateLimitMinute
|
||||
options.RateLimitDuration = time.Minute
|
||||
}
|
||||
|
||||
if options.RateLimit > 0 && options.RateLimitDuration == 0 {
|
||||
options.RateLimitDuration = time.Second
|
||||
}
|
||||
|
||||
// If per-host rate limiting is enabled, make global rate limiter unlimited
|
||||
if options.PerHostRateLimit {
|
||||
runner.rateLimiter = utils.GetRateLimiter(context.Background(), 0, 0)
|
||||
@@ -461,7 +417,6 @@ func New(options *types.Options) (*Runner, error) {
|
||||
|
||||
// Initialization successful, disable cleanup on error
|
||||
cleanupOnError = false
|
||||
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
@@ -470,7 +425,6 @@ func (r *Runner) runStandardEnumeration(executerOpts *protocols.ExecutorOptions,
|
||||
if r.options.AutomaticScan {
|
||||
return r.executeSmartWorkflowInput(executerOpts, store, engine)
|
||||
}
|
||||
|
||||
return r.executeTemplatesInput(store, engine)
|
||||
}
|
||||
|
||||
@@ -536,6 +490,11 @@ func (r *Runner) Close() {
|
||||
if r.browser != nil {
|
||||
r.browser.Close()
|
||||
}
|
||||
// Drop any in-memory auto-login session so nuclei does not retain live
|
||||
// session material after the scan (server-side sessions expire on their own).
|
||||
if r.authProvider != nil {
|
||||
r.authProvider.Close()
|
||||
}
|
||||
if r.tmpDir != "" {
|
||||
_ = os.RemoveAll(r.tmpDir)
|
||||
}
|
||||
@@ -592,7 +551,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 || r.options.DASTProxy {
|
||||
if r.options.DASTServer {
|
||||
execurOpts := &server.NucleiExecutorOptions{
|
||||
Options: r.options,
|
||||
Output: r.output,
|
||||
@@ -609,7 +568,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
FuzzStatsDB: r.fuzzStats,
|
||||
Logger: r.Logger,
|
||||
}
|
||||
serverOptions := &server.Options{
|
||||
dastServer, err := server.New(&server.Options{
|
||||
Address: r.options.DASTServerAddress,
|
||||
Templates: r.options.Templates,
|
||||
OutputWriter: r.output,
|
||||
@@ -618,28 +577,11 @@ 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()
|
||||
}
|
||||
|
||||
@@ -649,18 +591,16 @@ 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...)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the active ignore policy only when templates can execute.
|
||||
// Exclude ignored file for validation
|
||||
if !r.options.Validate {
|
||||
if err := r.loadIgnoreFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
ignoreFile := config.ReadIgnoreFile()
|
||||
r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
|
||||
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
|
||||
}
|
||||
|
||||
fuzzFreqCache := frequency.New(frequency.DefaultMaxTrackCount, r.options.FuzzParamFrequency)
|
||||
@@ -671,7 +611,6 @@ 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,
|
||||
@@ -696,24 +635,79 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts.ExportReqURLPattern = true
|
||||
}
|
||||
|
||||
if len(r.options.SecretsFile) > 0 && !r.options.Validate {
|
||||
// Clone options so GetAuthTmplStore can modify them without affecting the original
|
||||
authOptions := r.options.Copy()
|
||||
authTmplStore, err := GetAuthTmplStore(authOptions, r.catalog, executorOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load dynamic auth templates")
|
||||
if (len(r.options.SecretsFile) > 0 || r.options.AuthLoginURL != "" || r.options.AuthRecording != "" || r.options.AuthCapture) && !r.options.Validate {
|
||||
autoLoginOpts := buildAutoLoginRuntimeOptions(r.options)
|
||||
var providers []authprovider.AuthProvider
|
||||
|
||||
if len(r.options.SecretsFile) > 0 {
|
||||
// Clone options so GetAuthTmplStore can modify them without affecting the original
|
||||
authOptions := r.options.Copy()
|
||||
authTmplStore, err := GetAuthTmplStore(authOptions, r.catalog, executorOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load dynamic auth templates")
|
||||
}
|
||||
authOpts := &authprovider.AuthProviderOptions{SecretsFiles: r.options.SecretsFile}
|
||||
authOpts.LazyFetchSecret = GetLazyAuthFetchCallback(&AuthLazyFetchOptions{
|
||||
TemplateStore: authTmplStore,
|
||||
ExecOpts: executorOpts,
|
||||
})
|
||||
authOpts.AutoLoginOptions = autoLoginOpts
|
||||
fileProvider, err := authprovider.NewAuthProvider(authOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create auth provider")
|
||||
}
|
||||
providers = append(providers, fileProvider)
|
||||
}
|
||||
authOpts := &authprovider.AuthProviderOptions{SecretsFiles: r.options.SecretsFile}
|
||||
authOpts.LazyFetchSecret = GetLazyAuthFetchCallback(&AuthLazyFetchOptions{
|
||||
TemplateStore: authTmplStore,
|
||||
ExecOpts: executorOpts,
|
||||
})
|
||||
// initialize auth provider
|
||||
provider, err := authprovider.NewAuthProvider(authOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create auth provider")
|
||||
|
||||
// Turnkey auto-login from CLI flags (-auth-login-url): build an in-memory
|
||||
// auto-login secret scoped to the login host, no secrets file needed.
|
||||
if r.options.AuthLoginURL != "" || r.options.AuthRecording != "" {
|
||||
engine := "http"
|
||||
if r.options.AuthHeadless || r.options.AuthRecording != "" {
|
||||
engine = "headless"
|
||||
}
|
||||
target := r.options.AuthLoginURL
|
||||
if target == "" {
|
||||
target = r.options.AuthRecording
|
||||
}
|
||||
r.Logger.Info().Msgf("Auto-login enabled (%s engine) for %s", engine, target)
|
||||
store, err := autoLoginStoreFromOptions(r.options)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not build auto-login auth provider")
|
||||
}
|
||||
cliProvider, err := authprovider.NewStoreAuthProvider(store, nil, autoLoginOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create auto-login auth provider")
|
||||
}
|
||||
providers = append(providers, cliProvider)
|
||||
}
|
||||
executorOpts.AuthProvider = provider
|
||||
|
||||
// Capture-once: open a visible browser for a one-time manual login and
|
||||
// build a static session store from the captured cookies/token.
|
||||
if r.options.AuthCapture {
|
||||
if r.options.AuthLoginURL == "" {
|
||||
return errors.New("-auth-capture requires -auth-login-url (the page to open for manual login)")
|
||||
}
|
||||
r.Logger.Info().Msgf("Capture-once login enabled for %s", r.options.AuthLoginURL)
|
||||
store, err := captureOnceStoreFromOptions(r.options)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not capture login session")
|
||||
}
|
||||
captureProvider, err := authprovider.NewStoreAuthProvider(store, nil, autoLoginOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create capture-once auth provider")
|
||||
}
|
||||
providers = append(providers, captureProvider)
|
||||
}
|
||||
|
||||
switch len(providers) {
|
||||
case 1:
|
||||
executorOpts.AuthProvider = providers[0]
|
||||
default:
|
||||
executorOpts.AuthProvider = authprovider.NewMultiAuthProvider(providers...)
|
||||
}
|
||||
// Retained so Runner.Close can drop captured session material on exit.
|
||||
r.authProvider = executorOpts.AuthProvider
|
||||
}
|
||||
|
||||
if r.options.ShouldUseHostError() {
|
||||
@@ -732,13 +726,6 @@ func (r *Runner) RunEnumeration() error {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if r.options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
|
||||
}
|
||||
if r.options.AutoCalibrate {
|
||||
executorOpts.CatchAll = catchall.New(0)
|
||||
}
|
||||
|
||||
executorEngine := core.New(r.options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -750,20 +737,8 @@ func (r *Runner) RunEnumeration() error {
|
||||
|
||||
// If using input-file flags, only load http fuzzing based templates.
|
||||
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
|
||||
profiles := r.targetProfiles()
|
||||
if profiles != nil {
|
||||
if r.options.AutomaticScan {
|
||||
return errors.New("per-target profiles cannot be combined with automatic scan")
|
||||
}
|
||||
loaderConfig.TargetFilter = profiles.LoadFilter()
|
||||
}
|
||||
if r.hasPinnedTargets() {
|
||||
if err := pinnedTargetConflict(r.options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if (!strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST) && !r.options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
if !strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST {
|
||||
// if input type is not list (implicitly enable fuzzing)
|
||||
r.options.DAST = true
|
||||
}
|
||||
store, err := loader.New(loaderConfig)
|
||||
@@ -806,15 +781,6 @@ func (r *Runner) RunEnumeration() error {
|
||||
if err := store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Each template is parsed once per scan, so the compiled cache only holds
|
||||
// pre-compilation snapshots that nothing reads after loading.
|
||||
if parser, ok := r.parser.(*templates.Parser); ok {
|
||||
parser.PurgeCompiled()
|
||||
}
|
||||
if profiles != nil {
|
||||
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
|
||||
executorOpts.TargetScope = profiles
|
||||
}
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
@@ -949,24 +915,6 @@ 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()
|
||||
@@ -1009,7 +957,7 @@ func (r *Runner) executeSmartWorkflowInput(executorOpts *protocols.ExecutorOptio
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create automatic scan service")
|
||||
}
|
||||
if err := service.Execute(context.Background()); err != nil {
|
||||
if err := service.Execute(); err != nil {
|
||||
return nil, errors.Wrap(err, "could not execute automatic scan")
|
||||
}
|
||||
result := &atomic.Bool{}
|
||||
@@ -1076,13 +1024,19 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
|
||||
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
|
||||
stats.ForceDisplayWarning(templates.SkippedUnverifiedTemplateStats)
|
||||
stats.ForceDisplayWarning(templates.SkippedRequestSignatureTemplateStats)
|
||||
stats.ForceDisplayWarning(templates.SkippedWorkflowStepStats)
|
||||
|
||||
cfg := config.DefaultConfig
|
||||
|
||||
updateutils.Aurora = r.colorizer
|
||||
gologger.Info().Msg(versionInfo(cfg, r.colorizer, config.Version, cfg.LatestNucleiVersion, "nuclei"))
|
||||
gologger.Info().Msg(templateVersionInfo(cfg, r.colorizer))
|
||||
versionInfo := func(version, latestVersion, versionType string) string {
|
||||
if !cfg.CanCheckForUpdates() {
|
||||
return fmt.Sprintf("Current %s version: %v (%s) - remove '-duc' flag to enable update checks", versionType, version, r.colorizer.BrightYellow("unknown"))
|
||||
}
|
||||
return fmt.Sprintf("Current %s version: %v %v", versionType, version, updateutils.GetVersionDescription(version, latestVersion))
|
||||
}
|
||||
|
||||
gologger.Info().Msg(versionInfo(config.Version, cfg.LatestNucleiVersion, "nuclei"))
|
||||
gologger.Info().Msg(versionInfo(cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion, "nuclei-templates"))
|
||||
if !HideAutoSaveMsg {
|
||||
if r.pdcpUploadErrMsg != "" {
|
||||
r.Logger.Warning().Msgf("%s", r.pdcpUploadErrMsg)
|
||||
@@ -1116,40 +1070,19 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
|
||||
}
|
||||
}
|
||||
|
||||
func versionInfo(cfg *config.Config, colorizer *aurora.Aurora, version, latestVersion, versionType string) string {
|
||||
if !cfg.CanCheckForUpdates() {
|
||||
return fmt.Sprintf("Current %s version: %v (%s) - remove '-duc' flag to enable update checks", versionType, version, colorizer.BrightYellow("unknown"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Current %s version: %v %v", versionType, version, updateutils.GetVersionDescription(version, latestVersion))
|
||||
}
|
||||
|
||||
func templateVersionInfo(cfg *config.Config, colorizer *aurora.Aurora) string {
|
||||
return versionInfo(cfg, colorizer, cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion, "nuclei-templates")
|
||||
}
|
||||
|
||||
// SaveResumeConfig to file
|
||||
func (r *Runner) SaveResumeConfig(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if !fileutil.FolderExists(dir) {
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return fmt.Errorf("create resume directory %q: %w", dir, err)
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resumeCfgClone := r.resumeCfg.Clone()
|
||||
resumeCfgClone.ResumeFrom = resumeCfgClone.Current
|
||||
data, _ := json.MarshalIndent(resumeCfgClone, "", "\t")
|
||||
|
||||
data, err := json.MarshalIndent(resumeCfgClone, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode resume configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, permissionutil.ConfigFilePermission); err != nil {
|
||||
return fmt.Errorf("write resume configuration %q: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.WriteFile(path, data, permissionutil.ConfigFilePermission)
|
||||
}
|
||||
|
||||
// upload existing scan results to cloud with progress
|
||||
@@ -1253,28 +1186,3 @@ func init() {
|
||||
HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false)
|
||||
EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false)
|
||||
}
|
||||
|
||||
func newInteractshHTTPClient(options *types.Options) *retryablehttp.Client {
|
||||
httpOpts := retryablehttp.DefaultOptionsSingle
|
||||
httpOpts.Timeout = 20 * time.Second // for stability reasons
|
||||
if options.Timeout > 20 {
|
||||
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
if dialers := protocolstate.GetDialersWithId(options.ExecutionId); dialers != nil && dialers.Fastdialer != nil {
|
||||
// The pooled default keeps ProxyFromEnvironment and the TLS config that
|
||||
// INTERACTSH_TLS_VERIFY edits. Nuclei only exports HTTP_PROXY when -pi
|
||||
// is set, and that path uses the pooled client, so -proxy without -pi
|
||||
// still dials the interactsh server directly.
|
||||
transport := retryablehttp.DefaultReusePooledTransport()
|
||||
transport.DialContext = dialers.Fastdialer.Dial
|
||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialers.Fastdialer.DialTLS(ctx, network, addr)
|
||||
}
|
||||
httpOpts.HttpClient = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: httpOpts.Timeout,
|
||||
}
|
||||
}
|
||||
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
|
||||
return retryablehttp.NewClient(httpOpts)
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -154,112 +143,3 @@ func TestWalkReflectStructHandlesNestedStructs(t *testing.T) {
|
||||
require.Equal(t, "2", testStruct.Struct.B)
|
||||
require.Equal(t, "true", testStruct.Struct.C)
|
||||
}
|
||||
|
||||
func TestNewInteractshHTTPClient(t *testing.T) {
|
||||
// ProxyFromEnvironment reads the environment once per process. Set it
|
||||
// before the first request. NO_PROXY keeps the local dial direct.
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "https_proxy", "no_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
t.Setenv("NO_PROXY", "127.0.0.1,localhost")
|
||||
|
||||
options := &types.Options{
|
||||
ExecutionId: "test-interactsh-client-dialer",
|
||||
Timeout: 35,
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(options))
|
||||
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
|
||||
|
||||
client := newInteractshHTTPClient(options)
|
||||
require.NotNil(t, client)
|
||||
require.Equal(t, 35*time.Second, client.HTTPClient.Timeout)
|
||||
|
||||
transport, ok := client.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.Proxy)
|
||||
require.NotNil(t, transport.DialContext)
|
||||
require.NotNil(t, transport.DialTLSContext)
|
||||
require.NotNil(t, transport.TLSClientConfig, "INTERACTSH_TLS_VERIFY edits this config")
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
resp, err := client.HTTPClient.Get(srv.URL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
host := srv.Listener.Addr().(*net.TCPAddr).IP.String()
|
||||
dialers := protocolstate.GetDialersWithId(options.ExecutionId)
|
||||
require.Equal(t, host, dialers.Fastdialer.GetDialedIP(host))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
require.NoError(t, err)
|
||||
proxyURL, err := transport.Proxy(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proxyURL)
|
||||
require.Equal(t, "http://127.0.0.1:9", proxyURL.String())
|
||||
}
|
||||
|
||||
func TestInitializeTemplatesHTTPInputUsesResolvers(t *testing.T) {
|
||||
for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
|
||||
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
queries := new(atomic.Int32)
|
||||
server := &dns.Server{PacketConn: listener, Handler: dns.HandlerFunc(func(writer dns.ResponseWriter, request *dns.Msg) {
|
||||
queries.Add(1)
|
||||
response := new(dns.Msg).SetReply(request)
|
||||
for _, question := range request.Question {
|
||||
if question.Qtype == dns.TypeA {
|
||||
response.Answer = append(response.Answer, &dns.A{Hdr: dns.RR_Header{Name: question.Name, Rrtype: dns.TypeA, Class: dns.ClassINET}, A: net.ParseIP("127.0.0.1")})
|
||||
}
|
||||
}
|
||||
_ = writer.WriteMsg(response)
|
||||
})}
|
||||
started := make(chan struct{})
|
||||
server.NotifyStartedFunc = func() { close(started) }
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- server.ActivateAndServe() }()
|
||||
<-started
|
||||
t.Cleanup(func() { require.NoError(t, server.Shutdown()); require.NoError(t, <-done) })
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
resolverFile := filepath.Join(t.TempDir(), "resolv.conf")
|
||||
require.NoError(t, os.WriteFile(resolverFile, nil, 0o600))
|
||||
t.Setenv("RESOLVERS_PATH", resolverFile)
|
||||
|
||||
options := &types.Options{
|
||||
ExecutionId: t.Name(),
|
||||
Timeout: 5,
|
||||
BulkSize: 1,
|
||||
InternalResolversList: []string{listener.LocalAddr().String()},
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(options))
|
||||
t.Cleanup(func() { protocolstate.Close(options.ExecutionId) })
|
||||
|
||||
target := "probe.nuclei.test:" + strconv.Itoa(srv.Listener.Addr().(*net.TCPAddr).Port)
|
||||
r := &Runner{
|
||||
options: options,
|
||||
inputProvider: provider.NewSimpleInputProviderWithUrls(options.ExecutionId, target),
|
||||
Logger: &gologger.Logger{},
|
||||
}
|
||||
|
||||
hm, err := r.initializeTemplatesHTTPInput()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = hm.Close() })
|
||||
|
||||
probed, ok := hm.Get(input.ProbeKey(target, ""))
|
||||
require.True(t, ok, "the probe should resolve the target through the -r resolver")
|
||||
require.Equal(t, "http://"+target, string(probed))
|
||||
require.Positive(t, queries.Load())
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// targetProfiles returns the per-target profiles read from input, or nil when
|
||||
// no target has one.
|
||||
func (r *Runner) targetProfiles() *targetprofile.Registry {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
TargetProfiles() *targetprofile.Registry
|
||||
})
|
||||
if !ok || !provider.TargetProfiles().Scoped() {
|
||||
return nil
|
||||
}
|
||||
return provider.TargetProfiles()
|
||||
}
|
||||
|
||||
// hasPinnedTargets reports whether a target line pinned a target to an ip.
|
||||
func (r *Runner) hasPinnedTargets() bool {
|
||||
provider, ok := r.inputProvider.(interface {
|
||||
PinnedTargets() bool
|
||||
})
|
||||
return ok && provider.PinnedTargets()
|
||||
}
|
||||
|
||||
// pinnedTargetConflict returns an error for an option that cannot run with
|
||||
// targets pinned to an ip.
|
||||
func pinnedTargetConflict(options *types.Options) error {
|
||||
switch {
|
||||
case options.AutomaticScan:
|
||||
return errors.New("targets pinned to an ip cannot be combined with automatic scan")
|
||||
case options.AliveHttpProxy != "" || options.AliveSocksProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with a proxy, as the proxy resolves the host itself")
|
||||
case options.ReplayProxy != "":
|
||||
return errors.New("targets pinned to an ip cannot be combined with -replay-proxy, as the replay proxy resolves the host itself")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPinnedTargetConflict(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options *types.Options
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "no conflicting option", options: &types.Options{}},
|
||||
{name: "automatic scan", options: &types.Options{AutomaticScan: true}, wantErr: true},
|
||||
{name: "http proxy", options: &types.Options{AliveHttpProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
{name: "socks proxy", options: &types.Options{AliveSocksProxy: "socks5://127.0.0.1:1080"}, wantErr: true},
|
||||
{name: "replay proxy", options: &types.Options{ReplayProxy: "http://127.0.0.1:8080"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantErr, pinnedTargetConflict(tt.options) != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,13 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
"github.com/logrusorgru/aurora/v4"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
@@ -40,17 +38,12 @@ func (r *Runner) verboseTemplate(tpl *templates.Template) {
|
||||
tpl.Info.SeverityHolder.Severity))
|
||||
}
|
||||
|
||||
// listedTemplate names its fields like a result event, so a -jsonl listing
|
||||
// joins with scan results on the same keys.
|
||||
type listedTemplate struct {
|
||||
Template string `json:"template,omitempty"`
|
||||
TemplateID string `json:"template-id"`
|
||||
TemplatePath string `json:"template-path"`
|
||||
Info model.Info `json:"info"`
|
||||
}
|
||||
|
||||
func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
r.listingHeader("templates")
|
||||
r.Logger.Print().Msgf(
|
||||
"\nListing available %v nuclei templates for %v",
|
||||
config.DefaultConfig.TemplateVersion,
|
||||
config.DefaultConfig.TemplatesDirectory,
|
||||
)
|
||||
// order templates alphabetically by path
|
||||
templates := store.Templates()
|
||||
sort.Slice(templates, func(i, j int) bool {
|
||||
@@ -58,10 +51,6 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
})
|
||||
|
||||
for _, tpl := range templates {
|
||||
if r.options.JSONL && !r.options.TemplateDisplay {
|
||||
r.listTemplateJSON(tpl)
|
||||
continue
|
||||
}
|
||||
if hasExtraFlags(r.options) {
|
||||
if r.options.TemplateDisplay {
|
||||
colorize := !r.options.NoColor
|
||||
@@ -89,36 +78,12 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) listTemplateJSON(tpl *templates.Template) {
|
||||
path, _ := utils.TemplatePathURL(tpl.Path, tpl.ID, "")
|
||||
marshalled, err := json.Marshal(listedTemplate{
|
||||
Template: path,
|
||||
TemplateID: tpl.ID,
|
||||
TemplatePath: tpl.Path,
|
||||
Info: tpl.Info,
|
||||
})
|
||||
if err != nil {
|
||||
r.Logger.Error().Msgf("Could not marshal the template %s: %s", tpl.Path, err)
|
||||
return
|
||||
}
|
||||
r.Logger.Print().Msgf("%s", marshalled)
|
||||
}
|
||||
|
||||
// listingHeader is skipped for -jsonl, where every line must parse as JSON.
|
||||
func (r *Runner) listingHeader(what string) {
|
||||
if r.options.JSONL {
|
||||
return
|
||||
}
|
||||
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
|
||||
r.Logger.Print().Msgf(
|
||||
"\nListing available %v nuclei %s for %v",
|
||||
"\nListing available %v nuclei tags for %v",
|
||||
config.DefaultConfig.TemplateVersion,
|
||||
what,
|
||||
config.DefaultConfig.TemplatesDirectory,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Runner) listAvailableTags(tagsMap map[string]int) {
|
||||
r.listingHeader("tags")
|
||||
|
||||
type kv struct {
|
||||
Key string `json:"tag"`
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/gologger/formatter"
|
||||
"github.com/projectdiscovery/gologger/levels"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const listedTemplateYAML = `id: listed-template
|
||||
info:
|
||||
name: Listed Template
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: listing
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
`
|
||||
|
||||
func TestTemplateListWritesJSONLines(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
cfg := config.DefaultConfig
|
||||
oldRoot := cfg.TemplatesDirectory
|
||||
t.Cleanup(func() { cfg.SetTemplatesDir(oldRoot) })
|
||||
cfg.SetTemplatesDir(root)
|
||||
|
||||
path := filepath.Join(root, "http", "listed-template.yaml")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(listedTemplateYAML), 0o600))
|
||||
|
||||
writer := &ignoreFileLogWriter{}
|
||||
logger := &gologger.Logger{}
|
||||
logger.SetFormatter(formatter.NewCLI(true))
|
||||
logger.SetWriter(writer)
|
||||
logger.SetMaxLevel(levels.LevelInfo)
|
||||
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.ExecutionId = t.Name()
|
||||
options.Logger = logger
|
||||
options.Templates = []string{path}
|
||||
options.TemplateList = true
|
||||
options.JSONL = true
|
||||
testutils.Init(options)
|
||||
t.Cleanup(func() { testutils.Cleanup(options) })
|
||||
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
store, err := loader.New(loader.NewConfig(options, disk.NewCatalog(root), executerOpts))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, store.LoadTemplatesOnlyMetadata())
|
||||
|
||||
require.Len(t, store.Templates(), 1)
|
||||
|
||||
r := &Runner{options: options, Logger: logger}
|
||||
r.listAvailableStoreTemplates(store)
|
||||
|
||||
require.Len(t, writer.records, 1, "JSONL output carries no banner line")
|
||||
var listed map[string]any
|
||||
require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(writer.records[0].message)), &listed))
|
||||
require.Equal(t, "listed-template", listed["template-id"])
|
||||
require.Equal(t, filepath.Join("http", "listed-template.yaml"), listed["template"])
|
||||
require.Equal(t, "Listed Template", listed["info"].(map[string]any)["name"])
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
func TestTemplateVersionInfoIgnoresLegacyDiscoverySource(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
root := filepath.Join(dir, "system", config.BinaryName, config.NucleiTemplatesDirName)
|
||||
cfg := &config.Config{}
|
||||
cfg.SetStateDir(filepath.Join(dir, "state"))
|
||||
|
||||
state, err := json.Marshal(map[string]string{
|
||||
"nuclei-templates-directory": root,
|
||||
"nuclei-templates-directory-source": "xdg-system",
|
||||
"nuclei-templates-version": "v1.2.3",
|
||||
"nuclei-templates-latest-version": "v99.0.0",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal templates state: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(cfg.GetTemplatesStateFilePath()), 0o700); err != nil {
|
||||
t.Fatalf("create state directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfg.GetTemplatesStateFilePath(), state, 0o600); err != nil {
|
||||
t.Fatalf("write templates state: %v", err)
|
||||
}
|
||||
if err := cfg.ReadTemplatesConfig(); err != nil {
|
||||
t.Fatalf("read templates state: %v", err)
|
||||
}
|
||||
|
||||
info := templateVersionInfo(cfg, nil)
|
||||
if !strings.Contains(info, "v1.2.3") || !strings.Contains(info, "outdated") {
|
||||
t.Fatalf("template version info = %q, want normal version comparison", info)
|
||||
}
|
||||
if strings.Contains(info, "package-managed") {
|
||||
t.Fatalf("template version info retained discovery ownership: %q", info)
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,10 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hostbackoff"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
|
||||
browserEngine "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/catchall"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
@@ -53,7 +51,7 @@ type NucleiExecutorOptions struct {
|
||||
Catalog catalog.Catalog
|
||||
IssuesClient reporting.Client
|
||||
RateLimiter *ratelimit.Limiter
|
||||
Interactsh interactsh.IClient
|
||||
Interactsh *interactsh.Client
|
||||
ProjectFile *projectfile.ProjectFile
|
||||
Browser *browserEngine.Browser
|
||||
FuzzStatsDB *stats.Tracker
|
||||
@@ -108,13 +106,6 @@ func newNucleiExecutor(opts *NucleiExecutorOptions) (*nucleiExecutor, error) {
|
||||
executorOpts.HostErrorsCache = cache
|
||||
}
|
||||
|
||||
if opts.Options.HostBackoff {
|
||||
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: opts.Options.HostBackoffMax})
|
||||
}
|
||||
if opts.Options.AutoCalibrate {
|
||||
executorOpts.CatchAll = catchall.New(0)
|
||||
}
|
||||
|
||||
executorEngine := core.New(opts.Options)
|
||||
executorEngine.SetExecuterOptions(executorOpts)
|
||||
|
||||
@@ -126,8 +117,8 @@ 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.DASTProxy) && !opts.Options.OfflineHTTP {
|
||||
// if input type is not list (implicitly enable fuzzing), unless passive/offlinehttp
|
||||
if !strings.EqualFold(opts.Options.InputFileMode, "list") || opts.Options.DAST || opts.Options.DASTServer {
|
||||
// if input type is not list (implicitly enable fuzzing)
|
||||
opts.Options.DAST = true
|
||||
}
|
||||
store, err := loader.New(loaderConfig)
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
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,15 +75,3 @@ 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,7 +1,6 @@
|
||||
package scope
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
@@ -25,20 +24,3 @@ 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))
|
||||
}
|
||||
+10
-148
@@ -3,10 +3,8 @@ package server
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -15,7 +13,6 @@ 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"
|
||||
@@ -32,7 +29,6 @@ type DASTServer struct {
|
||||
tasksPool *pond.WorkerPool
|
||||
deduplicator *requestDeduplicator
|
||||
scopeManager *scope.Manager
|
||||
proxy *proxy.Proxy
|
||||
startTime time.Time
|
||||
|
||||
// metrics
|
||||
@@ -53,21 +49,6 @@ 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
|
||||
@@ -116,70 +97,17 @@ 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)")
|
||||
}
|
||||
if options.EnableFuzzAPI {
|
||||
gologger.Info().Msgf("DAST Server API: %s", server.buildURL("/fuzz"))
|
||||
}
|
||||
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{},
|
||||
@@ -196,11 +124,6 @@ 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()
|
||||
}
|
||||
@@ -237,18 +160,16 @@ func (s *DASTServer) optionsOrDefault() *Options {
|
||||
}
|
||||
|
||||
func (s *DASTServer) setupHandlers(onlyStats bool) {
|
||||
opts := s.optionsOrDefault()
|
||||
mux := http.NewServeMux()
|
||||
if !onlyStats && opts.EnableFuzzAPI {
|
||||
// POST /fuzz - Queue a request for fuzzing
|
||||
if !onlyStats {
|
||||
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)
|
||||
}
|
||||
@@ -266,37 +187,12 @@ func (s *DASTServer) Start() error {
|
||||
s.setupHandlers(false)
|
||||
}
|
||||
s.httpServer.Addr = s.optionsOrDefault().Address
|
||||
|
||||
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) {
|
||||
if err := s.httpServer.ListenAndServe(); err != nil && 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"`
|
||||
@@ -318,25 +214,11 @@ func (s *DASTServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
s.tasksPool.Submit(func() {
|
||||
s.consumeTaskRequest(req)
|
||||
})
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type StatsResponse struct {
|
||||
@@ -353,8 +235,6 @@ 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 {
|
||||
@@ -367,29 +247,17 @@ 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: api,
|
||||
NucleiDastServerAPI: s.buildURL("/fuzz"),
|
||||
ServerAuthEnabled: s.options.Token != "",
|
||||
NucleiDastProxyAddr: s.options.ProxyAddress,
|
||||
ProxyAuthEnabled: s.options.ProxyUsername != "",
|
||||
},
|
||||
DASTScanStartTime: s.startTime,
|
||||
DASTScanStatistics: DASTScanStatistics{
|
||||
@@ -398,12 +266,6 @@ 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,11 +3,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -67,69 +64,3 @@ 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,10 +241,6 @@ 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">
|
||||
@@ -260,15 +256,6 @@ 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,7 +5,6 @@ package functional_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -60,12 +59,7 @@ func runTestMain(m *testing.M) int {
|
||||
ci: isCI(),
|
||||
runner: testutils.NewRunner(
|
||||
testutils.WithWorkingDir(repoRoot),
|
||||
testutils.WithBaseEnv(
|
||||
"NUCLEI_CONFIG_DIR="+configDir,
|
||||
"NUCLEI_TEMPLATES_DIR="+filepath.Join(configDir, "templates"),
|
||||
"XDG_STATE_HOME="+filepath.Join(configDir, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(configDir, "cache"),
|
||||
),
|
||||
testutils.WithBaseEnv("NUCLEI_CONFIG_DIR="+configDir),
|
||||
),
|
||||
}
|
||||
defer func() {
|
||||
@@ -281,12 +275,6 @@ 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`)
|
||||
@@ -345,29 +333,6 @@ 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
|
||||
@@ -482,31 +447,23 @@ func candidateExecutableNames(name string) []string {
|
||||
}
|
||||
|
||||
func prepareFunctionalEnvironment(binaryPath, repoRoot, configDir string) error {
|
||||
templatesDir := filepath.Join(configDir, "templates")
|
||||
configRoot := filepath.Join(configDir, "nuclei")
|
||||
if err := os.MkdirAll(configRoot, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create functional nuclei config root: %w", err)
|
||||
}
|
||||
ignoreFile := filepath.Join(configRoot, ".nuclei-ignore")
|
||||
if err := os.WriteFile(ignoreFile, nil, 0644); err != nil {
|
||||
return fmt.Errorf("failed to create functional ignore file: %w", err)
|
||||
}
|
||||
for _, args := range [][]string{{"-update-templates"}, {"-validate"}} {
|
||||
cmd := exec.Command(binaryPath, args...)
|
||||
cmd.Dir = repoRoot
|
||||
cmd.Env = append(os.Environ(),
|
||||
"NUCLEI_CONFIG_DIR="+configDir,
|
||||
"NUCLEI_TEMPLATES_DIR="+templatesDir,
|
||||
"XDG_STATE_HOME="+filepath.Join(configDir, "state"),
|
||||
"XDG_CACHE_HOME="+filepath.Join(configDir, "cache"),
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "NUCLEI_CONFIG_DIR="+configDir)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("functional setup command %v failed: %w\n%s", args, err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
}
|
||||
|
||||
ignoreFile, err := os.ReadFile(filepath.Join(templatesDir, ".nuclei-ignore"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read functional ignore file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(configDir, ".nuclei-ignore"), ignoreFile, 0o600); err != nil {
|
||||
return fmt.Errorf("failed to write legacy functional ignore file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -24,10 +24,7 @@
|
||||
{{binary}} -tags cve -exclude-templates http/cves/2020/
|
||||
{{binary}} -tags cve -exclude-templates http/cves/2020/CVE-2020-9757.yaml
|
||||
{{binary}} -tags cve -exclude-templates http/cves/2020/CVE-2020-9757.yaml -exclude-templates http/cves/2021/
|
||||
# Skipped until the next nuclei release ships #7536: release still force-includes
|
||||
# via -include-tags (bypassing -tags), so release/current load counts diverge by design.
|
||||
# Covered by pkg/catalog/index filter unit tests in the meantime.
|
||||
# {{binary}} -tags cve -include-tags dos,fuzz
|
||||
{{binary}} -tags cve -include-tags dos,fuzz
|
||||
{{binary}} -tags cve -ntv 8.8.8,8.8.9
|
||||
{{binary}} -tags cve -severity high
|
||||
{{binary}} -tags cve,dos,fuzz
|
||||
|
||||
@@ -119,9 +119,7 @@ func TestTemplatesDirEnv(t *testing.T) {
|
||||
func TestCustomConfigDir(t *testing.T) {
|
||||
t.Run("ConfigDirIsolated", func(t *testing.T) {
|
||||
customTempDir := t.TempDir()
|
||||
stateHome := t.TempDir()
|
||||
templatesDir := t.TempDir()
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, []string{"NUCLEI_CONFIG_DIR=" + customTempDir, "XDG_STATE_HOME=" + stateHome, "NUCLEI_TEMPLATES_DIR=" + templatesDir}, "-t", "protocols/http/get.yaml", "-u", newTemplateDirTarget(t))
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(suite.debug, []string{"NUCLEI_CONFIG_DIR=" + customTempDir}, "-t", "protocols/http/get.yaml", "-u", newTemplateDirTarget(t))
|
||||
if err != nil {
|
||||
t.Fatalf("custom config dir request failed: %v", err)
|
||||
}
|
||||
@@ -134,14 +132,11 @@ func TestCustomConfigDir(t *testing.T) {
|
||||
for _, file := range files {
|
||||
fileNames = append(fileNames, file.Name())
|
||||
}
|
||||
for _, requiredFile := range []string{"config.yaml", "reporting-config.yaml"} {
|
||||
for _, requiredFile := range []string{".templates-config.json", "config.yaml", "reporting-config.yaml"} {
|
||||
if !slices.Contains(fileNames, requiredFile) {
|
||||
t.Fatalf("missing required config file %q in custom config dir: %v", requiredFile, fileNames)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(stateHome, "nuclei", "templates.json")); err != nil {
|
||||
t.Fatalf("missing templates state file: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -24,16 +24,6 @@ func TestDNS(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Duration", func(t *testing.T) {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/dns/duration.yaml", "one.one.one.one", suite.debug)
|
||||
if err != nil {
|
||||
t.Fatalf("dns duration request failed: %v", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AAAA", func(t *testing.T) {
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/dns/aaaa.yaml", "one.one.one.one", suite.debug)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import "time"
|
||||
|
||||
// Keep duration assertions above the timer granularity of fast local sockets on Windows.
|
||||
const integrationDurationObservationDelay = 10 * time.Millisecond
|
||||
@@ -18,11 +18,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
dbName = "test"
|
||||
dbRepository = "mongo"
|
||||
// 8.x refuses to start on kernels 6.19 through 7.0.13 (SERVER-125742).
|
||||
// The GitHub Ubuntu image is in that range. 7.0 stays up.
|
||||
dbTag = "7.0"
|
||||
dbName = "test"
|
||||
dbRepository = "mongo"
|
||||
dbTag = "8"
|
||||
dbPort = "27017/tcp"
|
||||
mongoDatabaseReadyTimeout = 3 * time.Minute
|
||||
mongoServerSelectionDelay = time.Second
|
||||
|
||||
@@ -5,12 +5,11 @@ package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@@ -19,6 +18,28 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The analyzer-driven DAST integration cases each fire a heavy burst of
|
||||
// differential/heuristic probes through the full CLI against the in-process
|
||||
// playground. On the Windows CI runner this is too slow/flaky and times out
|
||||
// to zero findings (consistently across reruns), while the analyzer logic
|
||||
// itself is already covered cross-platform by the pkg/fuzz/analyzers engine
|
||||
// and e2e tests (the Tests (windows-latest) job runs them and passes). Skip
|
||||
// only the binary-level analyzer cases on Windows to keep CI green without
|
||||
// dropping real coverage.
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
filtered := make([]integrationCase, 0, len(fuzzingTestCases))
|
||||
for _, c := range fuzzingTestCases {
|
||||
if strings.HasPrefix(c.Path, "fuzz/analyzer-") {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
fuzzingTestCases = filtered
|
||||
}
|
||||
|
||||
const (
|
||||
targetFile = "fuzz/testData/ginandjuice.proxify.yaml"
|
||||
)
|
||||
@@ -40,55 +61,145 @@ var fuzzingTestCases = []integrationCase{
|
||||
{Path: "fuzz/fuzz-body-params-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/fuzz-body-xml-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/fuzz-body-generic-sqli.yaml", TestCase: &genericFuzzTestCase{expectedResults: 4}},
|
||||
{Path: "fuzz/fuzz-raw-http-input.yaml", TestCase: &rawHttpInputFuzz{}},
|
||||
|
||||
// Analyzer-driven DAST cases: each runs a real fuzzing template whose
|
||||
// detection is delegated to a built-in analyzer, against the dedicated
|
||||
// analyzer bench in the fuzz playground. A finding is produced only when the
|
||||
// analyzer (not a static matcher) confirms the vulnerability.
|
||||
{Path: "fuzz/analyzer-sqli.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/sqli?q=en", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssti.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/ssti?q=test", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-lfi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/lfi?q=home.txt", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cmdi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/cmdi?q=127.0.0.1", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssrf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/ssrf?q=https://example.com/a.png", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-open-redirect.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/redirect?q=/dashboard", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-crlf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/crlf?q=/home", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cors.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/cors?q=x", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-host-header.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/host-header?q=x", expectedResults: 1}},
|
||||
|
||||
// Negative cases: the same analyzer templates against benign routes must
|
||||
// produce zero findings (false-positive guard at the CLI level).
|
||||
{Path: "fuzz/analyzer-sqli.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=en", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-ssti.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=test", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-lfi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=home.txt", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-cmdi.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=127.0.0.1", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-ssrf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/reflect?q=https://example.com/a.png", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-open-redirect.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/redirect?q=/dashboard", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-crlf.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/headers?q=/home", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-cors.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/cors?q=x", expectedResults: 0}},
|
||||
{Path: "fuzz/analyzer-host-header.yaml", TestCase: &analyzerFuzzTestCase{route: "/analyzer/safe/host?q=x", expectedResults: 0}},
|
||||
|
||||
// Non-query positions: each template fuzzes a different request component
|
||||
// (path / header / body / cookie) of a captured request and relies on an
|
||||
// analyzer for detection. Driven via the proxify-format capture file.
|
||||
{Path: "fuzz/analyzer-path-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-header-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-body-sqli.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cookie-ssti.yaml", TestCase: &analyzerPositionTestCase{expectedResults: 1}},
|
||||
|
||||
// Crawl-to-fuzz via the katana input format: each analyzer template is driven
|
||||
// against a one-line katana JSONL crawl entry for the matching playground
|
||||
// endpoint, exercising the full crawl -> input-format -> fuzz -> analyzer
|
||||
// pipeline through the CLI (-im katana ... -dast) across GET-query,
|
||||
// GET-with-cookie and POST-JSON-body crawl shapes. (nuclei DAST scopes
|
||||
// fuzzing to one request per host, so each case uses its own crawl entry.)
|
||||
{Path: "fuzz/analyzer-sqli.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/sqli?q=en", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssti.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/ssti?q=test", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-lfi.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/lfi?q=home.txt", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cmdi.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/cmdi?q=127.0.0.1", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-ssrf.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/ssrf?q=https://example.com/a.png", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-open-redirect.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/redirect?q=/dashboard", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-crlf.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/crlf?q=/home", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cors.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/cors?q=x", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-host-header.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/host-header?q=x", expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-cookie-ssti.yaml", TestCase: &analyzerKatanaTestCase{method: "GET", endpoint: "http://localhost:8082/analyzer/cookie/ssti", headers: map[string]string{"Cookie": "lang=en"}, expectedResults: 1}},
|
||||
{Path: "fuzz/analyzer-body-sqli.yaml", TestCase: &analyzerKatanaTestCase{method: "POST", endpoint: "http://localhost:8082/analyzer/body/sqli", headers: map[string]string{"Content-Type": "application/json"}, body: `{"name":"en"}`, expectedResults: 1}},
|
||||
}
|
||||
|
||||
// rawHttpInputFuzz seeds the base request from a raw HTTP request file, the
|
||||
// shape users have on hand when there is no spec or captured traffic to feed in.
|
||||
type rawHttpInputFuzz struct{}
|
||||
// analyzerKatanaTestCase drives an analyzer template against a katana JSONL
|
||||
// crawl entry for the matching playground endpoint, exercising the full crawl
|
||||
// -> input-format -> fuzz -> analyzer pipeline through the CLI
|
||||
// (-im katana ... -dast).
|
||||
type analyzerKatanaTestCase struct {
|
||||
method string
|
||||
endpoint string
|
||||
headers map[string]string
|
||||
body string
|
||||
expectedResults int
|
||||
}
|
||||
|
||||
func (h *rawHttpInputFuzz) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
router.POST("/api/login", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
var payload map[string]string
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// stand in for a backend that interpolates the value into a query
|
||||
if strings.Contains(payload["user"], "'") {
|
||||
_, _ = fmt.Fprint(w, "unrecognized token:")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "welcome %s", payload["user"])
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
target, err := url.Parse(ts.URL)
|
||||
func (a *analyzerKatanaTestCase) Execute(filePath string) error {
|
||||
crawlFile, cleanup, err := writeKatanaCrawlEntry(a.method, a.endpoint, a.headers, a.body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawRequest := fmt.Sprintf("POST /api/login HTTP/1.1\r\nHost: %s\r\nContent-Type: application/json\r\n\r\n{\"user\":\"admin\"}", target.Host)
|
||||
targetPath := filepath.Join(os.TempDir(), "nuclei-raw-http-input.http")
|
||||
if err := os.WriteFile(targetPath, []byte(rawRequest), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(targetPath)
|
||||
}()
|
||||
defer cleanup()
|
||||
|
||||
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", targetPath, "-im", "http", "-dast")
|
||||
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", crawlFile, "-im", "katana", "-dast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
return expectResultsCount(results, a.expectedResults)
|
||||
}
|
||||
|
||||
// writeKatanaCrawlEntry writes a single katana JSONL crawl record to a temp file
|
||||
// and returns its path plus a cleanup func.
|
||||
func writeKatanaCrawlEntry(method, endpoint string, headers map[string]string, body string) (string, func(), error) {
|
||||
record := map[string]any{
|
||||
"request": map[string]any{
|
||||
"method": method,
|
||||
"endpoint": endpoint,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
},
|
||||
}
|
||||
line, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
f, err := os.CreateTemp("", "analyzer-katana-*.jsonl")
|
||||
if err != nil {
|
||||
return "", func() {}, err
|
||||
}
|
||||
if _, err := f.Write(append(line, '\n')); err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(f.Name())
|
||||
return "", func() {}, err
|
||||
}
|
||||
_ = f.Close()
|
||||
return f.Name(), func() { _ = os.Remove(f.Name()) }, nil
|
||||
}
|
||||
|
||||
const analyzerPositionsTargetFile = "fuzz/testData/analyzer-positions.proxify.yaml"
|
||||
|
||||
// analyzerPositionTestCase runs an analyzer-driven template that fuzzes a
|
||||
// non-query component against the captured analyzer-bench requests. Only the
|
||||
// request matching the template's fuzzed component produces a finding.
|
||||
type analyzerPositionTestCase struct {
|
||||
expectedResults int
|
||||
}
|
||||
|
||||
func (a *analyzerPositionTestCase) Execute(filePath string) error {
|
||||
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", analyzerPositionsTargetFile, "-im", "yaml", "-dast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, a.expectedResults)
|
||||
}
|
||||
|
||||
// analyzerFuzzTestCase runs an analyzer-driven fuzzing template against the
|
||||
// running fuzz playground (localhost:8082) and asserts the number of findings.
|
||||
type analyzerFuzzTestCase struct {
|
||||
route string
|
||||
expectedResults int
|
||||
}
|
||||
|
||||
func (a *analyzerFuzzTestCase) Execute(filePath string) error {
|
||||
target := "http://localhost:8082" + a.route
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, target, debug, "-dast")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, a.expectedResults)
|
||||
}
|
||||
|
||||
type genericFuzzTestCase struct {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
var headlessTestcases = []integrationCase{
|
||||
{Path: "protocols/headless/headless-basic.yaml", TestCase: &headlessBasic{}},
|
||||
{Path: "protocols/headless/headless-duration.yaml", TestCase: &headlessBasic{}},
|
||||
{Path: "protocols/headless/headless-waitevent.yaml", TestCase: &headlessBasic{}},
|
||||
{Path: "protocols/headless/headless-dsl.yaml", TestCase: &headlessBasic{}},
|
||||
{Path: "protocols/headless/headless-self-contained.yaml", TestCase: &headlessSelfContained{}},
|
||||
|
||||
@@ -6,7 +6,6 @@ package integration_test
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
@@ -45,7 +43,6 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/post-json-body.yaml", TestCase: &httpPostJSONBody{}},
|
||||
{Path: "protocols/http/post-multipart-body.yaml", TestCase: &httpPostMultipartBody{}},
|
||||
{Path: "protocols/http/raw-cookie-reuse.yaml", TestCase: &httpRawCookieReuse{}},
|
||||
{Path: "protocols/http/disable-cookie-reuse.yaml", TestCase: &httpDisableCookieReuse{}},
|
||||
{Path: "protocols/http/raw-dynamic-extractor.yaml", TestCase: &httpRawDynamicExtractor{}},
|
||||
{Path: "protocols/http/raw-get-query.yaml", TestCase: &httpRawGetQuery{}},
|
||||
{Path: "protocols/http/raw-get.yaml", TestCase: &httpRawGet{}},
|
||||
@@ -78,14 +75,11 @@ var httpTestcases = []integrationCase{
|
||||
{Path: "protocols/http/variable-dsl-function.yaml", TestCase: &httpVariableDSLFunction{}},
|
||||
{Path: "protocols/http/get-override-sni.yaml", TestCase: &httpSniAnnotation{}},
|
||||
{Path: "protocols/http/get-sni.yaml", TestCase: &customCLISNI{}},
|
||||
{Path: "protocols/http/tls-metadata.yaml", TestCase: &httpTLSMetadata{}},
|
||||
{Path: "protocols/http/redirect-match-url.yaml", TestCase: &httpRedirectMatchURL{}},
|
||||
{Path: "protocols/http/get-sni-unsafe.yaml", TestCase: &customCLISNIUnsafe{}},
|
||||
{Path: "protocols/http/annotation-timeout.yaml", TestCase: &annotationTimeout{}},
|
||||
{Path: "protocols/http/custom-attack-type.yaml", TestCase: &customAttackType{}},
|
||||
{Path: "protocols/http/get-all-ips.yaml", TestCase: &scanAllIPS{}},
|
||||
{Path: "protocols/http/target-line-ip.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/target-line-ip-pipeline.yaml", TestCase: &httpTargetLineIP{}},
|
||||
{Path: "protocols/http/get-without-scheme.yaml", TestCase: &httpGetWithoutScheme{}},
|
||||
{Path: "protocols/http/cl-body-without-header.yaml", TestCase: &httpCLBodyWithoutHeader{}},
|
||||
{Path: "protocols/http/cl-body-with-header.yaml", TestCase: &httpCLBodyWithHeader{}},
|
||||
@@ -100,9 +94,6 @@ 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{}},
|
||||
{Path: "protocols/http/get.yaml", TestCase: &httpExcludeHostsFileComments{}},
|
||||
}
|
||||
|
||||
type httpMultiVarSharing struct{}
|
||||
@@ -838,7 +829,7 @@ func (h *httpRawUnsafePath) Execute(filepath string) error {
|
||||
}
|
||||
|
||||
actual := []string{}
|
||||
for v := range strings.SplitSeq(results, "\n") {
|
||||
for _, v := range strings.Split(results, "\n") {
|
||||
if strings.Contains(v, "GET") {
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) == 3 {
|
||||
@@ -886,7 +877,7 @@ func (h *httpPaths) Execute(filepath string) error {
|
||||
}
|
||||
|
||||
actual := []string{}
|
||||
for v := range strings.SplitSeq(results, "\n") {
|
||||
for _, v := range strings.Split(results, "\n") {
|
||||
if strings.Contains(v, "GET") {
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) == 3 {
|
||||
@@ -910,29 +901,6 @@ func (h *httpPaths) Execute(filepath string) error {
|
||||
|
||||
type httpRawCookieReuse struct{}
|
||||
|
||||
type httpDisableCookieReuse struct{}
|
||||
|
||||
func (h *httpDisableCookieReuse) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
router.GET("/login", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "nuclei", Value: "test"})
|
||||
_, _ = fmt.Fprint(w, "logged in")
|
||||
})
|
||||
router.GET("/anonymous", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
if _, err := r.Cookie("nuclei"); errors.Is(err, http.ErrNoCookie) {
|
||||
_, _ = fmt.Fprint(w, "anonymous")
|
||||
}
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpRawCookieReuse) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
@@ -1426,7 +1394,7 @@ func (h *httpVariableDSLFunction) Execute(filePath string) error {
|
||||
}
|
||||
|
||||
actual := []string{}
|
||||
for v := range strings.SplitSeq(results, "\n") {
|
||||
for _, v := range strings.Split(results, "\n") {
|
||||
if strings.Contains(v, "GET") {
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) == 3 {
|
||||
@@ -1463,28 +1431,6 @@ func (h *customCLISNI) Execute(filePath string) error {
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
type httpTLSMetadata struct{}
|
||||
|
||||
// Execute verifies TLS handshake metadata from the same HTTP connection is matchable.
|
||||
func (h *httpTLSMetadata) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
if r.TLS != nil && r.TLS.ServerName == "test" {
|
||||
_, _ = w.Write([]byte("test-ok"))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("test-ko"))
|
||||
})
|
||||
ts := httptest.NewTLSServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-sni", "test")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
type httpSniAnnotation struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
@@ -1610,70 +1556,6 @@ func (h *scanAllIPS) Execute(filePath string) error {
|
||||
return expectResultsCount(got, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve to the test servers
|
||||
type httpTargetLineIP struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, name string) (*httptest.Server, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "host=%s server=%s", r.Host, name)
|
||||
}))
|
||||
_ = ts.Listener.Close()
|
||||
ts.Listener = listener
|
||||
ts.Start()
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer one.Close()
|
||||
_, port, err := net.SplitHostPort(one.Listener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := []string{fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.1"}`, port)}
|
||||
want := []string{"one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer two.Close()
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "http://pinned.test:%s", "ip": "127.0.0.2"}`, port))
|
||||
want = append(want, "two")
|
||||
}
|
||||
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
got, err := testutils.RunNucleiBareArgsAndGetResults(debug, nil, "-t", filePath, "-l", targets.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(got, len(want)); err != nil {
|
||||
return err
|
||||
}
|
||||
joined := strings.Join(got, "\n")
|
||||
for _, name := range want {
|
||||
if !strings.Contains(joined, name) {
|
||||
return fmt.Errorf("no result from server %q: %v", name, got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure that ip|host are handled without http|https scheme
|
||||
type httpGetWithoutScheme struct{}
|
||||
|
||||
@@ -1881,7 +1763,7 @@ func (h *httpRawPathSingleSlash) Execute(filepath string) error {
|
||||
}
|
||||
|
||||
var actual string
|
||||
for v := range strings.SplitSeq(results, "\n") {
|
||||
for _, v := range strings.Split(results, "\n") {
|
||||
if strings.Contains(v, "GET") {
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) == 3 {
|
||||
@@ -1906,7 +1788,7 @@ func (h *httpRawUnsafePathSingleSlash) Execute(filepath string) error {
|
||||
}
|
||||
|
||||
var actual string
|
||||
for v := range strings.SplitSeq(results, "\n") {
|
||||
for _, v := range strings.Split(results, "\n") {
|
||||
if strings.Contains(v, "GET") {
|
||||
parts := strings.Fields(v)
|
||||
if len(parts) == 3 {
|
||||
@@ -1920,123 +1802,3 @@ 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
|
||||
}
|
||||
|
||||
// ensure that comments in an -exclude-hosts file are ignored
|
||||
type httpExcludeHostsFileComments struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *httpExcludeHostsFileComments) Execute(filePath string) error {
|
||||
router := httprouter.New()
|
||||
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
_, _ = fmt.Fprintf(w, "This is test matcher text")
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
excludeHosts := func(content string) ([]string, error) {
|
||||
file, err := os.CreateTemp("", "exclude-hosts-*.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = os.Remove(file.Name()) }()
|
||||
_, err = file.WriteString(content)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", file.Name())
|
||||
}
|
||||
|
||||
// a comment line is not a host, even when it reads as an invalid regex
|
||||
results, err := excludeHosts("# hosts out of scope (see ticket\n203.0.113.10\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("comment line: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err != nil {
|
||||
return fmt.Errorf("comment line: %w", err)
|
||||
}
|
||||
|
||||
// a trailing comment does not stop the host from being excluded
|
||||
results, err = excludeHosts("127.0.0.1 # test server\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("trailing comment: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 0); err != nil {
|
||||
return fmt.Errorf("trailing comment: %w", err)
|
||||
}
|
||||
|
||||
// comma separated hosts are still split when passed on the command line
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug, "-eh", "203.0.113.10, 127.0.0.1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("comma separated: %w", err)
|
||||
}
|
||||
if err := expectResultsCount(results, 0); err != nil {
|
||||
return fmt.Errorf("comma separated: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/fuzzplayground"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
)
|
||||
|
||||
type integrationHarness struct {
|
||||
@@ -64,36 +63,12 @@ func TestMain(m *testing.M) {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to determine user home directory: %v\n", err)
|
||||
_ = os.RemoveAll(tempDir)
|
||||
os.Exit(1)
|
||||
}
|
||||
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(baseEnv...),
|
||||
)
|
||||
testutils.SetDefaultRunner(runner)
|
||||
|
||||
@@ -134,16 +109,6 @@ 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) {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/health"
|
||||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
// startGRPCHealthServer starts an in-process gRPC server exposing the standard
|
||||
// health service (status SERVING) and server reflection on a random loopback
|
||||
// port. It returns the "host:port" address and a stop func.
|
||||
func startGRPCHealthServer() (string, func(), error) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("grpc health listen: %w", err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
hs := health.NewServer()
|
||||
hs.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
|
||||
healthpb.RegisterHealthServer(srv, hs)
|
||||
reflection.Register(srv)
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return lis.Addr().String(), srv.Stop, nil
|
||||
}
|
||||
|
||||
// javascriptGRPCHealth exercises the nuclei/grpc library end to end against a
|
||||
// real gRPC server via server reflection.
|
||||
type javascriptGRPCHealth struct{}
|
||||
|
||||
func (j *javascriptGRPCHealth) Execute(filePath string) error {
|
||||
address, stop, err := startGRPCHealthServer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stop()
|
||||
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// javascriptGRPCDenied asserts the nuclei/grpc library refuses to dial a host
|
||||
// on the exclude list (network-policy enforcement) before connecting.
|
||||
type javascriptGRPCDenied struct{}
|
||||
|
||||
func (j *javascriptGRPCDenied) 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)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// startJSHTTPServer starts a local HTTP server exercising the generic paths
|
||||
// used by the nuclei/http javascript integration templates.
|
||||
func startJSHTTPServer() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Test", "ok")
|
||||
_, _ = io.WriteString(w, "hello")
|
||||
})
|
||||
mux.HandleFunc("/echo-header", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, r.Header.Get("X-Token"))
|
||||
})
|
||||
mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
_, _ = w.Write(b)
|
||||
})
|
||||
mux.HandleFunc("/set-cookie", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "sid", Value: "abc"})
|
||||
_, _ = io.WriteString(w, "set")
|
||||
})
|
||||
mux.HandleFunc("/echo-cookie", func(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := r.Cookie("sid")
|
||||
if err != nil || c.Value != "abc" {
|
||||
_, _ = io.WriteString(w, "missing")
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, "sid-ok")
|
||||
})
|
||||
mux.HandleFunc("/redir", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/land", http.StatusFound)
|
||||
})
|
||||
mux.HandleFunc("/land", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, "landed")
|
||||
})
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
type javascriptHTTPGet struct{}
|
||||
|
||||
func (j *javascriptHTTPGet) Execute(filePath string) error {
|
||||
ts := startJSHTTPServer()
|
||||
defer ts.Close()
|
||||
|
||||
// Target as host:port so javascript Host/Port args populate cleanly.
|
||||
hostPort := strings.TrimPrefix(ts.URL, "http://")
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, hostPort, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
type javascriptHTTPClientFlow struct{}
|
||||
|
||||
func (j *javascriptHTTPClientFlow) Execute(filePath string) error {
|
||||
ts := startJSHTTPServer()
|
||||
defer ts.Close()
|
||||
|
||||
hostPort := strings.TrimPrefix(ts.URL, "http://")
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, hostPort, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
type javascriptHTTPDenied struct{}
|
||||
|
||||
func (j *javascriptHTTPDenied) 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)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
)
|
||||
|
||||
// ipmiSimSetup runs OpenIPMI's BMC simulator with an anonymous user,
|
||||
// ADMIN/ADMIN and root/calvin, and cipher suite 0 enabled.
|
||||
const ipmiSimSetup = `apt-get update -qq && apt-get install -y -qq --no-install-recommends openipmi >/dev/null && mkdir -p /etc/ipmi && cat > /etc/ipmi/lan.conf <<'EOF'
|
||||
name "bmc"
|
||||
set_working_mc 0x20
|
||||
startlan 1
|
||||
addr 0.0.0.0 623
|
||||
priv_limit admin
|
||||
allowed_auths_callback none md2 md5 straight
|
||||
allowed_auths_user none md2 md5 straight
|
||||
allowed_auths_operator none md2 md5 straight
|
||||
allowed_auths_admin none md2 md5 straight
|
||||
guid a123456789abcdefa123456789abcdef
|
||||
endlan
|
||||
user 1 true "" "" user 10 none md2 md5 straight
|
||||
user 2 true "ADMIN" "ADMIN" admin 10 none md2 md5 straight
|
||||
user 3 true "root" "calvin" admin 10 none md2 md5 straight
|
||||
EOF
|
||||
printf 'mc_setbmc 0x20\nmc_add 0x20 0 no-device-sdrs 0x23 9 8 0x9f 0x1291 0xf02 persist_sdr\nmc_enable 0x20\n' > /etc/ipmi/emu.cmd
|
||||
exec ipmi_sim -c /etc/ipmi/lan.conf -f /etc/ipmi/emu.cmd -n`
|
||||
|
||||
type javascriptIPMI struct{}
|
||||
|
||||
func (j *javascriptIPMI) Execute(filePath string) error {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create docker pool: %w", err)
|
||||
}
|
||||
if err := pool.Client.Ping(); err != nil {
|
||||
return fmt.Errorf("could not connect to Docker: %w", err)
|
||||
}
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "debian",
|
||||
Tag: "bookworm-slim",
|
||||
Platform: "linux/amd64",
|
||||
Cmd: []string{"sh", "-c", ipmiSimSetup},
|
||||
ExposedPorts: []string{"623/udp"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start ipmi simulator: %w", err)
|
||||
}
|
||||
defer purge(pool, resource)
|
||||
|
||||
address := net.JoinHostPort("127.0.0.1", resource.GetPort("623/udp"))
|
||||
if err := waitForServiceCheck(address, 3*time.Minute, rmcpPingCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// rmcpPingCheck waits for an answer to an RMCP presence ping, since the
|
||||
// port is udp and cannot be probed with a connect.
|
||||
func rmcpPingCheck(address string) error {
|
||||
conn, err := net.Dial("udp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
if _, err := conn.Write([]byte{0x06, 0x00, 0xff, 0x06, 0x00, 0x00, 0x11, 0xbe, 0x80, 0x00, 0x00, 0x00}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
reply := make([]byte, 64)
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 9 || reply[8] != 0x40 {
|
||||
return fmt.Errorf("unexpected rmcp reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type javascriptMSSQLFingerprint struct{}
|
||||
|
||||
func (j *javascriptMSSQLFingerprint) Execute(filePath string) error {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = ln.Close() }()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go handleMSSQLPreloginConn(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
results, err := runSignedNucleiTemplateAndGetResults(filePath, ln.Addr().String(), debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
func handleMSSQLPreloginConn(conn net.Conn) {
|
||||
defer func() { _ = conn.Close() }()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
buf := make([]byte, 256)
|
||||
if _, err := io.ReadAtLeast(conn, buf, 8); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write(sampleMSSQLPreloginResponse())
|
||||
}
|
||||
|
||||
func sampleMSSQLPreloginResponse() []byte {
|
||||
body := []byte{
|
||||
0x00, 0x00, 0x1f, 0x00, 0x06,
|
||||
0x01, 0x00, 0x25, 0x00, 0x01,
|
||||
0x02, 0x00, 0x26, 0x00, 0x01,
|
||||
0x03, 0x00, 0x27, 0x00, 0x00,
|
||||
0x04, 0x00, 0x27, 0x00, 0x01,
|
||||
0x05, 0x00, 0x28, 0x00, 0x00,
|
||||
0xff,
|
||||
0x0f, 0x00, 0x07, 0xd0, 0x00, 0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x01,
|
||||
}
|
||||
header := []byte{0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}
|
||||
binary.BigEndian.PutUint16(header[2:4], uint16(8+len(body)))
|
||||
return append(header, body...)
|
||||
}
|
||||
@@ -5,12 +5,10 @@ package integration_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pg/pg/v10"
|
||||
@@ -18,7 +16,6 @@ import (
|
||||
"github.com/ory/dockertest/v3"
|
||||
dc "github.com/ory/dockertest/v3/docker"
|
||||
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
osutils "github.com/projectdiscovery/utils/os"
|
||||
"github.com/projectdiscovery/utils/reader"
|
||||
"go.uber.org/multierr"
|
||||
@@ -34,25 +31,13 @@ func javascriptGoExecSambaDisabled() bool {
|
||||
|
||||
var jsTestcases = []integrationCase{
|
||||
{Path: "protocols/javascript/redis-pass-brute.yaml", TestCase: &javascriptRedisPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/redis-lua-script.yaml", TestCase: &javascriptRedisLuaScript{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
|
||||
{Path: "protocols/javascript/target-line-ip.yaml", TestCase: &javascriptTargetLineIP{}},
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/grpc-health.yaml", TestCase: &javascriptGRPCHealth{}},
|
||||
{Path: "protocols/javascript/grpc-denied.yaml", TestCase: &javascriptGRPCDenied{}},
|
||||
{Path: "protocols/javascript/websocket-echo.yaml", TestCase: &javascriptWebSocketEcho{}},
|
||||
{Path: "protocols/javascript/websocket-denied.yaml", TestCase: &javascriptWebSocketDenied{}},
|
||||
{Path: "protocols/javascript/http-get.yaml", TestCase: &javascriptHTTPGet{}},
|
||||
{Path: "protocols/javascript/http-client-flow.yaml", TestCase: &javascriptHTTPClientFlow{}},
|
||||
{Path: "protocols/javascript/http-denied.yaml", TestCase: &javascriptHTTPDenied{}},
|
||||
{Path: "protocols/javascript/ipmi.yaml", TestCase: &javascriptIPMI{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/rsync-test.yaml", TestCase: &javascriptRsyncTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/postgres-pass-brute.yaml", TestCase: &javascriptPostgresPassBrute{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/mysql-connect.yaml", TestCase: &javascriptMySQLConnect{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/mysql-fingerprint.yaml", TestCase: &javascriptMySQLFingerprint{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
{Path: "protocols/javascript/mssql-fingerprint.yaml", TestCase: &javascriptMSSQLFingerprint{}},
|
||||
{Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}},
|
||||
{Path: "protocols/javascript/no-port-args.yaml", TestCase: &javascriptNoPortArgs{}},
|
||||
{Path: "protocols/javascript/telnet-auth-test.yaml", TestCase: &javascriptTelnetAuthTest{}, DisableOn: javascriptDockerDisabled, Serial: true},
|
||||
@@ -126,16 +111,6 @@ func (j *javascriptRedisPassBrute) Execute(filePath string) error {
|
||||
}, javascriptServiceReadyTimeout, 0, nil))
|
||||
}
|
||||
|
||||
type javascriptRedisLuaScript struct{}
|
||||
|
||||
func (j *javascriptRedisLuaScript) Execute(filePath string) error {
|
||||
return runJavascriptDockerCase(filePath, newJavascriptDockerSpec("6379/tcp", &dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"redis-server", "--requirepass", "iamadmin"},
|
||||
}, javascriptServiceReadyTimeout, 0, nil))
|
||||
}
|
||||
|
||||
type javascriptSSHServerFingerprint struct{}
|
||||
|
||||
func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
|
||||
@@ -188,57 +163,6 @@ func (j *javascriptMySQLConnect) Execute(filePath string) error {
|
||||
}, javascriptDatabaseReadyTimeout, 0, mysqlReadyCheck("root", "secret")))
|
||||
}
|
||||
|
||||
type javascriptMySQLFingerprint struct{}
|
||||
|
||||
// Execute fingerprints multiple MySQL/MariaDB server versions and asserts the
|
||||
// extended handshake fields (protocol, version, salt, capabilities, auth plugin).
|
||||
func (j *javascriptMySQLFingerprint) Execute(filePath string) error {
|
||||
cases := []struct {
|
||||
name string
|
||||
repository string
|
||||
tag string
|
||||
env []string
|
||||
}{
|
||||
{
|
||||
name: "mysql-5.7",
|
||||
repository: "mysql",
|
||||
tag: "5.7",
|
||||
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
|
||||
},
|
||||
{
|
||||
name: "mysql-8.0",
|
||||
repository: "mysql",
|
||||
tag: "8.0",
|
||||
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
|
||||
},
|
||||
{
|
||||
name: "mysql-8.4",
|
||||
repository: "mysql",
|
||||
tag: "8.4",
|
||||
env: []string{"MYSQL_ROOT_PASSWORD=secret"},
|
||||
},
|
||||
{
|
||||
name: "mariadb-11.4",
|
||||
repository: "mariadb",
|
||||
tag: "11.4",
|
||||
env: []string{"MARIADB_ROOT_PASSWORD=secret"},
|
||||
},
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, tc := range cases {
|
||||
err := runJavascriptDockerCase(filePath, newJavascriptDockerSpec("3306/tcp", &dockertest.RunOptions{
|
||||
Repository: tc.repository,
|
||||
Tag: tc.tag,
|
||||
Env: tc.env,
|
||||
}, javascriptDatabaseReadyTimeout, 0, mysqlReadyCheck("root", "secret")))
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", tc.name, err))
|
||||
}
|
||||
}
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
|
||||
type javascriptMultiPortsSSH struct{}
|
||||
|
||||
func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
|
||||
@@ -472,87 +396,6 @@ func (j *networkMultiStep) Execute(filePath string) error {
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// target lines pin a hostname that does not resolve, so the javascript
|
||||
// connection reaches a server only through the pinned ip
|
||||
type javascriptTargetLineIP struct{}
|
||||
|
||||
func (j *javascriptTargetLineIP) Execute(filePath string) error {
|
||||
serve := func(addr, banner string) (net.Listener, error) {
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write([]byte(banner))
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
one, err := serve("127.0.0.1:0", "one")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = one.Close() }()
|
||||
_, port, err := net.SplitHostPort(one.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
want := map[string]string{"127.0.0.1": "one"}
|
||||
// the same host pinned to a second ip, where 127.0.0.2 is available
|
||||
if two, err := serve(net.JoinHostPort("127.0.0.2", port), "two"); err == nil {
|
||||
defer func() { _ = two.Close() }()
|
||||
want["127.0.0.2"] = "two"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(want))
|
||||
for ip := range want {
|
||||
lines = append(lines, fmt.Sprintf(`{"target": "pinned.test:%s", "ip": %q}`, port, ip))
|
||||
}
|
||||
targets, err := os.CreateTemp("", "target-lines-*.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = os.Remove(targets.Name()) }()
|
||||
if _, err := targets.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = targets.Close()
|
||||
|
||||
if err := ensureSignedIntegrationTemplates(); err != nil {
|
||||
return err
|
||||
}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), "-t", filePath, "-l", targets.Name(), "-jsonl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
got := make(map[string]string)
|
||||
for _, result := range results {
|
||||
var event output.ResultEvent
|
||||
if !strings.HasPrefix(result, "{") || json.Unmarshal([]byte(result), &event) != nil {
|
||||
continue
|
||||
}
|
||||
got[event.IP] = strings.Join(event.ExtractedResults, ",")
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
return fmt.Errorf("got results for %d ips, want %d: %v", len(got), len(want), got)
|
||||
}
|
||||
for ip, banner := range want {
|
||||
if got[ip] != banner {
|
||||
return fmt.Errorf("ip %s answered %q, want %q", ip, got[ip], banner)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expectedNumbers ...int) error {
|
||||
if len(expectedNumbers) == 0 {
|
||||
expectedNumbers = []int{1}
|
||||
|
||||
Loaded 100 of 765 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user