Compare commits

..
Author SHA1 Message Date
Mzack9999 f2f89d0b4a Merge remote-tracking branch 'origin/dev' into feat-performance-improvements
# Conflicts:
#	pkg/operators/cache/cache.go
#	pkg/operators/matchers/match.go
#	pkg/output/multi_writer.go
#	pkg/protocols/common/expressions/expressions.go
#	pkg/protocols/common/expressions/variables.go
2026-06-10 22:58:14 +02:00
Mzack9999 444a21dfe2 review fixes 2026-06-10 22:49:47 +02:00
Mzack9999 a0e4599b2a . 2026-05-11 14:40:35 +02:00
Mzack9999 44fc97217d adding parallel workflow loading 2026-05-11 00:31:34 +02:00
Mzack9999 bebec6027c adding concurrent MultiWriter fan-out 2026-05-11 00:31:27 +02:00
Mzack9999 7f42a669b2 adding buffered tasks channel and larger SPM result buffer 2026-05-11 00:31:21 +02:00
Mzack9999 dac099daee adding skip of duplicate request dump when no debug consumer 2026-05-11 00:31:13 +02:00
Mzack9999 06c977f90c adding cached interactsh HasMatchers on Request 2026-05-11 00:31:03 +02:00
Mzack9999 7e450495a8 adding jq program compile cache 2026-05-11 00:30:34 +02:00
Mzack9999 c48c506692 adding typed fast paths to MergeMapsMany 2026-05-11 00:30:28 +02:00
Mzack9999 0af846340b adding lazy fasttemplate replacer 2026-05-11 00:30:21 +02:00
Mzack9999 1f6a429ca9 adding per-response body decode cache 2026-05-11 00:30:15 +02:00
Mzack9999 589e776426 adding compiled expression cache 2026-05-11 00:30:07 +02:00
671 changed files with 6280 additions and 60511 deletions

No files matched your search

+2 -1
View File
@@ -13,7 +13,8 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go/compat-checks@v1
with:
go-version: "stable"
release-test: true
+1 -1
View File
@@ -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@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- run: make build
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
outputs:
matrix: ${{ steps.discover.outputs.matrix }}
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- 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@v6
- uses: projectdiscovery/actions/setup/go@v1
with:
go-version-file: go.tool.mod
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
if: ${{ !endsWith(github.actor, '[bot]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/actions/setup/git@v1
- run: make syntax-docs
+1 -1
View File
@@ -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@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- run: |
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
env:
OUTPUT: "/tmp/results.sarif"
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
with:
go-version-file: go.tool.mod
+1 -1
View File
@@ -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@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/actions/setup/git@v1
- run: make memogen
+3 -3
View File
@@ -11,14 +11,14 @@ jobs:
env:
BENCH_OUT: "/tmp/bench.out"
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- run: make build-test
- run: ./bin/nuclei.test -test.run - -test.bench=. -test.benchmem ./cmd/nuclei/ | tee $BENCH_OUT
env:
DISABLE_STDOUT: "1"
- uses: actions/cache/restore@v6
- uses: actions/cache/restore@v5
with:
path: ./cache
key: ${{ runner.os }}-benchmark
@@ -32,7 +32,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
comment-on-alert: true
summary-always: true
- uses: actions/cache/save@v6
- uses: actions/cache/save@v5
if: github.event_name == 'push'
with:
path: ./cache
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
needs: ["pgo"]
runs-on: ubuntu-latest-16-cores
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/download-artifact@v8
-124
View File
@@ -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 }}
+1 -1
View File
@@ -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
+29 -84
View File
@@ -17,21 +17,14 @@ concurrency:
cancel-in-progress: true
jobs:
# lint gates every other job; none of them uses another's output, so they run
# in parallel once it passes instead of queueing behind the unit tests
lint:
name: "Lint"
if: ${{ !endsWith(github.actor, '[bot]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- uses: projectdiscovery/actions/golangci-lint/v2@v1
# go vet runs once here (not per-OS in the tests matrix): it is platform
# independent and folding it into the lint gate fails fast on a cheap
# runner before the heavy test/integration jobs start.
- run: make vet
tests:
name: "Tests"
@@ -44,45 +37,35 @@ jobs:
os: [ubuntu-latest, windows-latest, macOS-latest]
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
# 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)"
run: make test
if: ${{ matrix.os == 'ubuntu-latest' }}
env:
PDCP_API_KEY: "${{ secrets.PDCP_API_KEY }}"
- name: "Unit tests"
run: make test RACE=
if: ${{ matrix.os != 'ubuntu-latest' }}
- 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 vet
- run: make build
- run: make test
env:
PDCP_API_KEY: "${{ secrets.PDCP_API_KEY }}"
- run: go run -race . -l ${{ github.workspace }}/internal/tests/functional/testdata/targets.txt -id tech-detect,tls-version
if: ${{ matrix.os != 'windows-latest' }} # known issue: https://github.com/golang/go/issues/46099
working-directory: cmd/nuclei/
# Hermetic HTTP-engine scale regression: stands up many loopback hosts and
# asserts finding parity across a diverse template set (per-host pool,
# connection-reuse and http->https tracker + cookie-reuse). Race
# build runs on ubuntu only (same rationale as the unit tests above);
# other OSes run it plain. Large runs stay opt-in locally via
# NUCLEI_SCALE_HOSTS.
- name: "Scale regression (race)"
run: go test -tags=regression -race -timeout 15m ./lib/tests -run TestScaleRegression
if: ${{ matrix.os == 'ubuntu-latest' }}
- name: "Scale regression"
run: make regression
if: ${{ matrix.os != 'ubuntu-latest' }}
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@v6
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- name: "Simple"
@@ -98,7 +81,7 @@ jobs:
integration:
name: "Integration tests"
needs: ["lint"]
needs: ["tests"]
env:
GITHUB_TOKEN: "${{ github.token }}"
strategy:
@@ -107,22 +90,9 @@ jobs:
os: [ubuntu-latest, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- 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 +100,7 @@ jobs:
functional:
name: "Functional tests"
needs: ["lint"]
needs: ["tests"]
env:
GITHUB_TOKEN: "${{ github.token }}"
strategy:
@@ -139,10 +109,9 @@ jobs:
os: [ubuntu-latest, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- uses: projectdiscovery/actions/setup/python@v1
- uses: projectdiscovery/actions/setup/go@v1
- uses: projectdiscovery/nuclei-action/cache@v3
- uses: projectdiscovery/nuclei-action@v3
with:
version: latest
@@ -151,70 +120,46 @@ 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@v6
- 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@v6
- uses: github/codeql-action/init@v4
with:
languages: 'go'
- uses: github/codeql-action/autobuild@v4
- uses: github/codeql-action/analyze@v4
with:
output: sarif-results
upload: never
category: "/language:go"
# internal/fuzzplayground is a deliberately vulnerable mock server used to
# exercise nuclei's fuzzing templates (CMDI/SQLi/SSRF/etc. are by design),
# so its CodeQL alerts are expected false positives. paths-ignore has no
# effect for compiled languages, so drop those results from the SARIF
# before upload instead.
- name: "Filter fuzzplayground alerts from SARIF"
run: |
for sarif in sarif-results/*.sarif; do
[ -e "$sarif" ] || continue
jq '
.runs[].results |= map(
select(
[ .locations[]?.physicalLocation.artifactLocation.uri // "" ]
| any(startswith("internal/fuzzplayground/")) | not
)
)
' "$sarif" > "$sarif.filtered" && mv "$sarif.filtered" "$sarif"
done
- uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: sarif-results
category: "/language:go"
release:
name: "Release test"
needs: ["lint"]
needs: ["tests"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@v6
- 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:
+2 -2
View File
@@ -16,5 +16,5 @@ jobs:
if: ${{ !endsWith(github.actor, '[bot]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: crate-ci/[email protected]9.0
- uses: actions/checkout@v6
- uses: crate-ci/[email protected]7.2
-2
View File
@@ -33,8 +33,6 @@
# Templates
/*.yaml
!/.sops.yaml
!/.golangci.yaml
# Generated docs
/dsl.md
-1
View File
@@ -12,7 +12,6 @@ builds:
env:
- CGO_ENABLED=0
- GOEXPERIMENT=greenteagc
- GODEBUG=tlssha1=1
goos: [windows, linux, darwin]
goarch: [amd64, '386', arm, arm64]
ignore:
-4
View File
@@ -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)$
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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
+7 -20
View File
@@ -23,7 +23,7 @@ endif
.PHONY: all build build-stats clean devtools-all devtools-bindgen devtools-scrapefuncs fuzz fuzz-ci fuzz-tools
.PHONY: devtools-tsgen docs docgen dsl-docs functional go-build lint lint-strict fuzzplayground syntax-docs
.PHONY: integration integration-debug regression jsupdate-all jsupdate-bindgen jsupdate-tsgen memogen scan-charts test test-with-lint
.PHONY: integration integration-debug jsupdate-all jsupdate-bindgen jsupdate-tsgen memogen scan-charts test test-with-lint
.PHONY: tidy ts verify download vet template-validate build-fuzz discover-fuzz-packages
all: build
@@ -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
@@ -83,28 +82,16 @@ syntax-docs: docgen
syntax-docs:
./bin/docgen SYNTAX-REFERENCE.md nuclei-jsonschema.json
# RACE controls the data-race detector (on by default for local runs). CI builds
# the race variant on a single OS and passes RACE= elsewhere, since data races are
# OS-independent and the detector costs ~2-3x build+run time on every runner.
RACE ?= -race
test: GOFLAGS = $(RACE) -v -timeout 1h -count 1
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)
# Opt-in HTTP engine scale regression harness (not part of CI). Stands up many
# loopback hosts and asserts finding parity across a diverse template set.
# Override host count with NUCLEI_SCALE_HOSTS, e.g. NUCLEI_SCALE_HOSTS=500 make regression
regression:
$(GOTEST) -tags=regression -timeout 30m ./lib/tests -run TestScaleRegression -v
functional: build
@release_binary="$$(command -v nuclei.exe 2>/dev/null || command -v nuclei 2>/dev/null)"; \
if [ -z "$$release_binary" ]; then \
@@ -112,7 +99,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 +287,4 @@ template-validate:
./bin/nuclei -validate \
-w workflows \
-et http/technologies \
-ept code
-ept code
+6 -28
View File
@@ -64,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]
@@ -91,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>
@@ -140,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 4)
-dun, -disable-url-normalization keep targets exactly as supplied instead of collapsing urls that address the same resource
-mup, -max-urls-per-pattern int scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)
TARGET-FORMAT:
-im, -input-mode string mode of input file (list, burp, jsonl, yaml, openapi, swagger, http) (default "list")
-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
@@ -175,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) (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)
@@ -218,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -245,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -274,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")
@@ -294,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)
@@ -321,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:
@@ -339,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
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
+2 -19
View File
@@ -140,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, 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
@@ -173,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) (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)
@@ -216,7 +205,6 @@ 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
CONFIGURATIONS:
@@ -243,7 +231,7 @@ CONFIGURATIONS:
-ztls use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default
-sni string tls sni hostname to use (default: input domain name)
-dka, -dialer-keep-alive value keep-alive duration for network requests.
-lfa, -allow-local-file-access 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)
@@ -272,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")
@@ -292,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)
@@ -317,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:
-255
View File
@@ -1,255 +0,0 @@
<!-- Generated by securitycontext.dev. Source HEAD: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1. -->
# Security Context: projectdiscovery/nuclei @ dev
> Reference context for editing projectdiscovery/nuclei. Before changing any component below, **preserve the listed guards and do not reintroduce these fixed issues**. Each fingerprint is a regression check, and each shared attack surface is a guard that must keep holding on every path.
_Generated 2026-09-01 16:22 UTC by secc_
_Analyzed commit: 3e1e0e81f091c7817bbf7af64bb8eec6e8c2bac1_
**Remediation coverage: 56.7%**
_17 of 30 historical fixes have no remaining unguarded variant; 368 open remediation leads to verify. Measures fix consistency, not a vulnerability verdict._
- Commits analyzed: **4814**
- Commits flagged for review: **235**
- Confirmed historical security fixes: **32**
- Known published CVEs: **0**
## Summary
Nuclei's historical security posture is dominated by sandbox escapes and code execution via unsigned or unverified templates, with recurring SSRF, path traversal, and auth bypass issues across JS libraries, loaders, and workflow paths. The most critical theme is inconsistent enforcement of signature verification and local-file-access (-lfa) guards across direct, workflow, DAST, and JS-sandbox entry points, enabling variant attacks. A second theme is network policy (host allowlist) gaps in LDAP dialing and proxy handling. The security context also includes dependency upgrades (Alpine, go-git) and output file permission hardening, but the highest-value focus is on template trust boundaries and sandbox enforcement parity.
## Recurring Security Patterns
**15.6% of classified fixes repeat a previously fixed pattern** (5 recurring, 27 first-seen). A recurring pattern means this repository fixed a similar issue before — not that the code is currently vulnerable.
- **Code Execution:** [1c440e755a](https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645) → [9de96e4dda](https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76) — matched on `class:code execution`, `component:pkg/catalog/loader/loader.go`
- **Code Execution via Unsigned Template:** [fa56800fcc](https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de) → [fac0b7196d](https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37) — matched on `class:code execution via unsigned template`, `CWE-347`, `sink:loadtemplates`
- **SSRF:** [a167e6c57b](https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770) → [9def9af7e3](https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572) — matched on `class:ssrf`, `CWE-918`, `component:pkg/js/libs/ldap/ldap.go`
- **Dependency Vulnerability:** [d93b4a01df](https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe) → [fcd5c6b111](https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529) — matched on `class:dependency vulnerability`, `CWE-1104`, `component:dockerfile`
- **Sandbox Escape:** [96049a32fa](https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a) → [cf2a98b03d](https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91) — matched on `class:sandbox escape`, `CWE-284`, `component:v2/internal/runner/processor.go`, `sink:stdlib allmodulenames`, `sink:stdlib getmodulemap`
## Top Risks
### 1. Sandbox Escape: `pkg/js/compiler/pool.go, v2/pkg/protocols/javascript/js.go, v2/pkg/protocols/headless/engine/engine.go, v2/internal/runner/processor.go` (high)
Multiple sandbox escapes across JS require(), eval/ReadFile, headless browser flags, and tengo imports, indicating weak default sandbox boundaries that require consistent -lfa and safe-module enforcement.
Evidence: 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, df55f7a2eb591a7949b5c66a9c54db54aa28f450, cf2a98b03d76327c20fb0183b7f1c849eb692c91, 96049a32fa70aef0483d4180d386a57b3c2d973a
### 2. Code Execution via Unsigned Template: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go, pkg/templates/templates.go` (critical)
Unsigned templates can execute code through direct loading, workflows, and DAST branches; signature verification was bypassed via cache poisoning and missing guards on specific paths.
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de
### 3. Auth Bypass: `pkg/templates/compile.go, pkg/templates/signer/tmpl_signer.go, pkg/catalog/loader/loader.go, pkg/protocols/http/request.go` (high)
Signature verification can be bypassed via cached metadata, improper signature extraction, or missing checks on specific load paths, and cookie jar bypass via disabled cookie handling.
Evidence: 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b, a976ca8d3f6a18c9ca746c8ce73f28fdcd518642
### 4. SSRF: `pkg/js/libs/ldap/ldap.go, v2/internal/runner/proxy.go` (high)
SSRF via LDAP dialing to internal hosts and via proxy configuration; host allowlist checks were missing or applied to the wrong target (domain vs controller).
Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f
### 5. Path Traversal: `pkg/catalog/config/nucleiconfig.go, v2/internal/runner/update.go, v2/pkg/protocols/common/protocolstate/headless.go` (high)
Path traversal via lexical prefix checks, zip-slip in template extraction, and file:// scheme in headless mode; canonicalization and containment checks are required.
Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, d3928e080d819f25306bfbaecc399f03516db5aa
### 6. Code Execution: `pkg/catalog/loader/loader.go, pkg/templates/workflows.go` (high)
Code execution via cache poisoning and unverified code templates in workflows; requires -code flag and verified templates.
Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, e86f38299765b82ad724fdb701557e0eaff3884d
## Shared Attack Surfaces
Capabilities guarded by more than one past fix on different code paths. A guard enforced on one entry point is often missing on a sibling; verify each alternate path before assuming it's covered.
### 1. Template signature verification and unsigned-template opt-in enforcement across all load paths
- Guard that should hold everywhere: signature verification (template.Verified, Verify), DisableUnsignedTemplates flag, -code flag for code templates, cache invalidation on verification status/fingerprint/digest
- Entry points: `pkg/catalog/loader/loader.go (LoadTemplatesWithTags, DAST branch)`, `pkg/templates/workflows.go (loadTemplates, workflowExecutor)`, `pkg/templates/templates.go (loadTemplates)`, `pkg/templates/compile.go (template.Verified)`, `pkg/templates/signer/tmpl_signer.go (Verify)`
- Check: Inspect the DAST and workflow sub-loaders: they must enforce the same DisableUnsignedTemplates and -code checks as the direct loader, and must not rely on cached verification results without re-checking signatures.
- Evidence: 9de96e4dda5a03da963b9ae6582f03ea55791a76, 1c440e755a97471c56fb0276ee8a8c4132974645, fac0b7196d2fbca56c8af8ff8e1899c111041a37, e86f38299765b82ad724fdb701557e0eaff3884d, fa56800fcca3c8c8fe8e8dec28584210ca1527de, 4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4, 0da993afe6d41b4b1b814e8fad23a2acba13c60a, 9bd4db3f74aedd067af568df4f9198a8a090f68b
### 2. Local file access (read/write) outside the sandbox, gated by -lfa flag
- Guard that should hold everywhere: -lfa flag enforcement, path canonicalization and containment (sandbox directory), file:// scheme blocking, allowAllFiles stripping, require() registry rebuild
- Entry points: `pkg/js/libs/krbforge/krbforge.go (CreateTicket)`, `pkg/js/libs/mysql/mysql_private.go (sql.Open)`, `pkg/js/compiler/pool.go (require)`, `v2/pkg/protocols/javascript/js.go (ReadFile, eval)`, `v2/pkg/protocols/common/protocolstate/headless.go (file:)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
- Check: Verify that every JS library and protocol helper (krbforge, mysql, require, headless file://, payload loaders) enforces the -lfa flag and path containment; a sibling path that skips the -lfa check is a sandbox escape.
- Evidence: 3a8b65cb45f121fcdf418f60e5177ed9f250c122, 9611926a9a1ca99e8ed554d1b26fbeff60e06368, 6f2ade6a9b427c284c15a43445f9c7f055e60e5d, c35162c8ef061a96dd74b54c8f022c1a41cf5833, d3928e080d819f25306bfbaecc399f03516db5aa, e7fb40a41308f6c41267e858d1781eb67f7e0572
### 3. Network policy (host allowlist) enforcement before outbound connections
- Guard that should hold everywhere: IsHostAllowed check on the actual target host (controller, not domain), proxy validation and connectivity check, ProxyInternal gate for HTTP_PROXY env
- Entry points: `pkg/js/libs/ldap/ldap.go (Dial, newLdapSession)`, `v2/internal/runner/proxy.go (loadProxyServers, os.Setenv HTTP_PROXY)`
- Check: Inspect other dialing paths (e.g., HTTP, headless) to ensure they apply the same host allowlist check on the resolved IP/host, not just a user-supplied domain, and that proxy settings cannot redirect traffic to internal hosts.
- Evidence: 9def9af7e3f7ebcf5cd561c37b623b9999ee7572, a167e6c57b80f5dc7be231231b62d7c594ee2770, 0e46d3e0410f89e4af41099f5cd38fa7f54ae54f, 3f5c055aca1499642caf4f40e8cf55ad32bd4da9
### 4. Path traversal in file extraction and template path trust boundaries
- Guard that should hold everywhere: canonical path containment check (filepath.Rel, prefix), zip-slip validation against template directory, sandbox path prefix validation
- Entry points: `pkg/catalog/config/nucleiconfig.go (IsCustomTemplate)`, `v2/internal/runner/update.go (os.OpenFile)`, `v2/pkg/protocols/common/generators/load.go (loadPayloadsFromFile)`
- Check: Audit all file-write and file-read paths that accept template-controlled paths (e.g., template extraction, payload loading) to ensure they use canonical containment, not lexical prefix checks, and apply the same sandbox directory restriction.
- Evidence: 20cbf0bd73893c1270582c9778b6b26a5cb11206, f9c214a66f37a00fc29fe64232b690e6285ec59c, e7fb40a41308f6c41267e858d1781eb67f7e0572
## Historical Vulnerability Classes
| Class | Count |
|---|---|
| Sandbox Escape | 7 |
| Auth Bypass | 4 |
| Code Execution | 3 |
| Dependency Vulnerability | 3 |
| Path Traversal | 3 |
| SSRF | 3 |
| Code Execution via Unsigned Template | 2 |
| Arbitrary File Read | 1 |
| Cryptographic Weakness | 1 |
| Deserialization of Untrusted Data | 1 |
| Information Disclosure | 1 |
| Insecure File Permissions | 1 |
| Race Condition | 1 |
| Security Misconfiguration | 1 |
## Fixed-Issue Fingerprints
These are real security fixes mined from git history. Use them as regression checks.
- **Auth Bypass** (high) [CWE-287]: Honors disable-cookie by preventing the shared cookie jar from being applied when cookie handling is disabled, closing a path where cookies could be reused across requests.
- sink: `SetCookieJar(input.CookieJar)`
- fix: Adds !request.DisableCookie check before applying shared cookie jar
- commit: https://github.com/projectdiscovery/nuclei/commit/a976ca8d3f6a18c9ca746c8ce73f28fdcd518642 (2026-08-25)
- **Code Execution** (high) [CWE-347]: This commit fixes a cache poisoning vulnerability where unsigned templates could bypass signature verification and execute malicious code during compilation or request execution.
- sink: `template compilation and execution of JavaScript`
- fix: cache invalidation based on verification status, verifier fingerprint, and content digest
- commit: https://github.com/projectdiscovery/nuclei/commit/9de96e4dda5a03da963b9ae6582f03ea55791a76 (2026-08-04)
- **Code Execution via Unsigned Template** (high) [CWE-347]: Enforces signature verification on JavaScript templates, preventing execution of unsigned templates that could lead to arbitrary code execution.
- sink: `loading unsigned javascript templates into execution paths`
- fix: Require signature verification for javascript templates in direct loading and workflows when signing is enforced
- commit: https://github.com/projectdiscovery/nuclei/commit/fac0b7196d2fbca56c8af8ff8e1899c111041a37 (2026-07-03)
- **Sandbox Escape** (high) [CWE-22]: Enforces nuclei's local file access policy on krbforge output paths, preventing arbitrary file writes outside the sandbox.
- sink: `gpkrb.CreateTicket(cfg) with cfg.OutputFile`
- fix: Path normalization into sandbox and denial of outside paths unless -lfa
- commit: https://github.com/projectdiscovery/nuclei/commit/3a8b65cb45f121fcdf418f60e5177ed9f250c122 (2026-06-24)
- **SSRF** (high) [CWE-918]: Adds network policy host allowlist checks before LDAP dialing, preventing SSRF to restricted internal hosts.
- sink: `dialers.Fastdialer.Dial`
- fix: host allowlist check via IsHostAllowed before dialing
- commit: https://github.com/projectdiscovery/nuclei/commit/9def9af7e3f7ebcf5cd561c37b623b9999ee7572 (2026-06-24)
- **Arbitrary File Read** (high) [CWE-22]: Gates MySQL's allowAllFiles option behind the -lfa flag, preventing arbitrary local file reads via LOAD DATA LOCAL INFILE.
- sink: `sql.Open("mysql", dsn) with AllowAllFiles=true`
- fix: DSN sandbox that strips allowAllFiles unless -lfa is set
- commit: https://github.com/projectdiscovery/nuclei/commit/9611926a9a1ca99e8ed554d1b26fbeff60e06368 (2026-06-19)
- **Code Execution** (critical) [CWE-94]: Moves the unsigned code-template guard ahead of the DAST branch, preventing unsigned code templates from being loaded and executed under DAST, which previously bypassed the signature check.
- sink: `Template loading and execution`
- fix: Hoisting unsigned code-template guard before DAST branch to ensure it applies on all load paths
- commit: https://github.com/projectdiscovery/nuclei/commit/1c440e755a97471c56fb0276ee8a8c4132974645 (2026-06-19)
- **Path Traversal** (high) [CWE-22]: This commit replaces lexical prefix checks with canonical path containment checks across multiple filesystem trust boundaries, preventing path traversal where sibling paths were incorrectly treated as trusted.
- sink: `IsCustomTemplate`
- fix: path canonicalization and containment check
- commit: https://github.com/projectdiscovery/nuclei/commit/20cbf0bd73893c1270582c9778b6b26a5cb11206 (2026-06-04)
- **Information Disclosure** (medium) [CWE-200]: This commit fixes a data leakage issue in workflows where sibling state could leak across concurrent executions, and race requests ignored dynamic values, by syncing context and cloning inputs.
- sink: `syncWorkflowInputToTemplateCtx`
- fix: context synchronization and input cloning
- commit: https://github.com/projectdiscovery/nuclei/commit/805bcfe76f3629dbe71021795d4ce865406c1555 (2026-05-08)
- **Sandbox Escape** (high) [CWE-284]: Fixes sandbox escape where require() bypassed local file access restrictions and reused privileged module cache across executions.
- sink: `require() using default host filesystem loader`
- fix: Rebuild require registry per execution and enforce allow-local-file-access sandbox rules
- commit: https://github.com/projectdiscovery/nuclei/commit/6f2ade6a9b427c284c15a43445f9c7f055e60e5d (2026-04-10)
- **Cryptographic Weakness** (medium) [CWE-338]: Replaces predictable math/rand PRNG with cryptographically secure crypto/rand in JS helper functions.
- sink: `rand.Intn(255) / rand.Int63()`
- fix: Replace predictable math/rand with crypto/rand
- commit: https://github.com/projectdiscovery/nuclei/commit/44ef9ce34ffcfdfca3a96626abc73d99dd0e83ab (2026-03-15)
- **Auth Bypass** (high) [CWE-347]: Caching signature verification results without cryptographic re-check allows a metadata index compromise to bypass template signature verification.
- sink: `template.Verified, template.TemplateVerifier`
- fix: cached verification data reuse
- commit: https://github.com/projectdiscovery/nuclei/commit/4534e9cb3034ebe5cc7b0cecabe399b6fb9ce0e4 (2026-01-21)
- **Race Condition** (medium) [CWE-362]: Fixes a race condition in workflow execution caused by shared context callbacks, which could lead to data corruption or crashes; the primary impact is reliability, not a direct security breach.
- sink: `shared context callbacks in workflow execution`
- fix: fix race condition by making workflow execution sequential
- commit: https://github.com/projectdiscovery/nuclei/commit/24d1f58682027e23cb21a299a170abd2bdf20dc2 (2025-12-05)
- **Auth Bypass** (high) [CWE-347]: Fixes template signature verification by centralizing signature extraction and requiring the signature to be at the end of the template, preventing bypass.
- sink: `Verify`
- fix: signature extraction and verification
- commit: https://github.com/projectdiscovery/nuclei/commit/0da993afe6d41b4b1b814e8fad23a2acba13c60a (2024-08-19)
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.5 to 3.18.6 to fix known security vulnerabilities in bundled packages.
- sink: `FROM alpine:3.18.5`
- fix: Upgrade base image to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/fcd5c6b111ddcb746525f88b7d0bf4b4abbc2529 (2024-03-15)
- **Dependency Vulnerability** (medium) [CWE-1104]: Upgrades Alpine base image from 3.18.2 to 3.18.5 to fix known security vulnerabilities in bundled packages.
- sink: `FROM alpine:3.18.2`
- fix: Upgrade base image to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/d93b4a01df9c218a36dd2fccfb1c2c1f96b181fe (2024-03-15)
- **Dependency Vulnerability** (high) [CWE-1035]: Updates go-git to fix known vulnerabilities (CVE-2023-49569, CVE-2023-49568), which could be exploited when processing untrusted git data.
- sink: `go-git library usage`
- fix: dependency upgrade to patched version
- commit: https://github.com/projectdiscovery/nuclei/commit/1700cdf4e23128a358066364062246b58c1c013f (2024-03-14)
- **Code Execution via Unsigned Template** (high) [CWE-347]: Introduces a flag to disable unsigned templates, preventing execution of unverified templates that could lead to arbitrary code execution.
- sink: `loading unsigned templates when -dut flag is set`
- fix: Adds -disable-unsigned-templates flag to skip unsigned templates and report them separately
- commit: https://github.com/projectdiscovery/nuclei/commit/fa56800fcca3c8c8fe8e8dec28584210ca1527de (2024-03-14)
- **Auth Bypass** (medium) [CWE-347]: This commit introduces a flag to skip unsigned templates, closing a path where an attacker could execute untrusted templates by bypassing signature verification.
- sink: `parsed.Verified`
- fix: skip unverified templates when DisableUnsignedTemplates is set
- commit: https://github.com/projectdiscovery/nuclei/commit/9bd4db3f74aedd067af568df4f9198a8a090f68b (2024-03-10)
- **Code Execution** (high) [CWE-94]: Prevents execution of unverified code templates in workflows, mitigating code execution risk.
- sink: `template.RequestsCode`
- fix: require -code flag and verified templates for code templates in workflows
- commit: https://github.com/projectdiscovery/nuclei/commit/e86f38299765b82ad724fdb701557e0eaff3884d (2024-03-01)
- **SSRF** (high) [CWE-918]: Fixes SSRF by validating the actual LDAP controller host against the network policy instead of the domain name.
- sink: `newLdapSession`
- fix: host allowlist check on controller instead of domain
- commit: https://github.com/projectdiscovery/nuclei/commit/a167e6c57b80f5dc7be231231b62d7c594ee2770 (2024-01-20)
- **Sandbox Escape** (high) [CWE-94]: Disables eval and tightens file read validation in the JavaScript sandbox, preventing template-driven code execution and file access outside the sandbox.
- sink: `eval function and file read in sandbox mode`
- fix: Disable eval function and rewrite file validation in sandbox mode
- commit: https://github.com/projectdiscovery/nuclei/commit/c35162c8ef061a96dd74b54c8f022c1a41cf5833 (2023-10-13)
- **Path Traversal** (high) [CWE-22]: Adds network policy and blocks file:// protocol in headless mode unless -lfa is set, preventing local file disclosure.
- sink: `file:// protocol access in headless browser`
- fix: Network policy validation and file:// scheme blocking when local file access is disabled
- commit: https://github.com/projectdiscovery/nuclei/commit/d3928e080d819f25306bfbaecc399f03516db5aa (2023-08-25)
- **Deserialization of Untrusted Data** (medium) [CWE-502]: Fixes JSON deserialization by adding explicit UnmarshalJSON methods and correcting tags, preventing type confusion and potential logic bypasses.
- sink: `UnmarshalJSON on user-controlled fields like Type and Name`
- fix: Adds proper UnmarshalJSON implementations and corrects json tags to prevent type confusion
- commit: https://github.com/projectdiscovery/nuclei/commit/85090b75317977bb9301444c9dfd8e4c2dba667a (2023-02-07)
- **Sandbox Escape** (medium) [CWE-22]: The commit adds sandbox mode that restricts payload file paths to the template directory, preventing arbitrary file reads when sandbox is enabled.
- sink: `loadPayloadsFromFile`
- fix: path prefix validation against template directory and template path directory
- commit: https://github.com/projectdiscovery/nuclei/commit/e7fb40a41308f6c41267e858d1781eb67f7e0572 (2022-11-16)
- **Insecure File Permissions** (low) [CWE-732]: Fixes overly permissive file permissions on debug output files by changing from os.ModePerm (0777) to 0644.
- sink: `os.OpenFile`
- fix: restrict file mode to 0644
- commit: https://github.com/projectdiscovery/nuclei/commit/af4854f90de728bb08689d5cae3dec4acfdb6b9a (2022-06-20)
- **Security Misconfiguration** (medium) [CWE-16]: Prevents setting HTTP_PROXY environment variable unless explicitly enabled, avoiding potential proxy hijacking.
- sink: `os.Setenv(types.HTTP_PROXY_ENV, proxyURL.String())`
- fix: Added check for options.ProxyInternal before setting HTTP_PROXY environment variable
- commit: https://github.com/projectdiscovery/nuclei/commit/3f5c055aca1499642caf4f40e8cf55ad32bd4da9 (2022-03-09)
- **Sandbox Escape** (high) [CWE-250]: The commit restricts the use of the --no-sandbox flag to Linux root users, preventing sandbox escape vulnerabilities in other environments.
- sink: `NoSandbox(true)`
- fix: Conditionally disable sandbox only on Linux as root
- commit: https://github.com/projectdiscovery/nuclei/commit/df55f7a2eb591a7949b5c66a9c54db54aa28f450 (2021-12-05)
- **Path Traversal** (high) [CWE-22]: This commit fixes a zip-slip path traversal vulnerability by validating the extracted file path against the configured template directory.
- sink: `os.OpenFile`
- fix: path canonicalization and prefix check
- commit: https://github.com/projectdiscovery/nuclei/commit/f9c214a66f37a00fc29fe64232b690e6285ec59c (2021-11-25)
- **SSRF** (high) [CWE-918]: Adds validation and connectivity checks for SOCKS5 proxies before launching a scan, preventing SSRF through proxy configuration.
- sink: `Proxy connection validation and loading`
- fix: Proxy server validation and connectivity check before scan launch
- commit: https://github.com/projectdiscovery/nuclei/commit/0e46d3e0410f89e4af41099f5cd38fa7f54ae54f (2021-11-10)
- **Sandbox Escape** (high) [CWE-284]: This commit adds a sandbox flag that, when enabled, restricts the tengo scripting engine to safe modules, preventing sandbox escape and arbitrary command execution.
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
- fix: Added sandbox flag to restrict imports to safe modules when sandbox is enabled
- commit: https://github.com/projectdiscovery/nuclei/commit/cf2a98b03d76327c20fb0183b7f1c849eb692c91 (2020-12-13)
- **Sandbox Escape** (high) [CWE-284]: This commit restricts the tengo scripting engine to a safe module list, preventing sandbox escape and arbitrary command execution via workflow templates.
- sink: `stdlib.GetModuleMap(stdlib.AllModuleNames()...)`
- fix: Restricted tengo script imports to a safe module list
- commit: https://github.com/projectdiscovery/nuclei/commit/96049a32fa70aef0483d4180d386a57b3c2d973a (2020-12-13)
---
_Fixed-issue fingerprints and CVE matches are derived from real data; LLM-written prose is advisory and should be verified against the cited commits._
-262
View File
@@ -1009,26 +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>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 />
@@ -1477,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>
@@ -2475,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 />
@@ -2730,8 +2567,6 @@ Enum Values:
- <code>dsl</code>
- <code>xpath</code>
- <code>llm</code>
</div>
<hr />
@@ -2771,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
@@ -3064,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 />
@@ -3247,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 />
@@ -3361,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
@@ -3754,7 +3501,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>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)
<hr />
@@ -4135,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
@@ -4335,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
@@ -4510,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">
-1
View File
@@ -25,7 +25,6 @@ extend-exclude = [
[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
-72
View File
@@ -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)
}
})
}
}
+278 -209
View File
@@ -2,7 +2,6 @@ package main
import (
"bufio"
"flag"
"fmt"
"io/fs"
"os"
@@ -12,7 +11,6 @@ import (
"runtime/pprof"
"runtime/trace"
"strings"
"sync"
"time"
"github.com/projectdiscovery/gologger"
@@ -22,11 +20,11 @@ import (
_ "github.com/projectdiscovery/utils/pprof"
stringsutil "github.com/projectdiscovery/utils/strings"
"github.com/rs/xid"
"gopkg.in/yaml.v2"
"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,22 +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
)
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()
@@ -80,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
@@ -119,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
}
@@ -133,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()
@@ -166,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
@@ -184,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...")
}
@@ -210,7 +213,6 @@ func main() {
options.Logger.Info().Msgf("CTRL+C pressed: Exiting\n")
if options.DASTServer {
nucleiRunner.Close()
cleanupInlineSecretsDirs()
os.Exit(1)
}
@@ -226,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()
@@ -245,12 +249,10 @@ func main() {
}
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
flagSet := goflags.NewFlagSet()
@@ -258,9 +260,6 @@ func readConfig() *goflags.FlagSet {
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
*/
@@ -273,8 +272,6 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
flagSet.BoolVarP(&options.ScanAllIPs, "scan-all-ips", "sa", false, "scan all the IP's associated with dns record"),
flagSet.StringSliceVarP(&options.IPVersion, "ip-version", "iv", nil, "IP version to scan of hostname (4,6) - (default 4)", goflags.CommaSeparatedStringSliceOptions),
flagSet.BoolVarP(&options.DisableURLNormalization, "disable-url-normalization", "dun", false, "keep targets exactly as supplied instead of collapsing urls that address the same resource"),
flagSet.IntVarP(&options.MaxURLsPerPattern, "max-urls-per-pattern", "mup", 0, "scan at most this many urls sharing one structural shape, e.g. /user/1 and /user/2 (0 to scan all)"),
)
flagSet.CreateGroup("target-format", "Target-Format",
@@ -306,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)"),
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",
@@ -356,8 +345,8 @@ on extensive configurability, massive extensibility and ease of use.`)
)
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"),
@@ -379,14 +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.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.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"),
)
@@ -410,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)"),
@@ -432,9 +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"),
flagSet.IntVarP(&options.TemplateThreads, "concurrency", "c", 25, "maximum number of templates to be executed in parallel"),
@@ -463,9 +446,7 @@ on extensive configurability, massive extensibility and ease of use.`)
}),
flagSet.DurationVarP(&options.InputReadTimeout, "input-read-timeout", "irt", time.Duration(3*time.Minute), "timeout on input read"),
flagSet.BoolVarP(&options.DisableHTTPProbe, "no-httpx", "nh", false, "disable httpx probing for non-url input"),
flagSet.BoolVar(&options.PreflightPortScan, "preflight-portscan", false, "run preflight resolve + TCP portscan and filter targets before scanning (disabled by default)"),
flagSet.BoolVar(&options.DisableStdin, "no-stdin", false, "disable stdin processing"),
flagSet.BoolVar(&options.EnableHTTPCache, "http-cache", false, "enable HTTP cache (RFC 9111) for HTTP requests"),
)
flagSet.CreateGroup("headless", "Headless",
@@ -484,7 +465,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"),
@@ -504,7 +484,7 @@ on extensive configurability, massive extensibility and ease of use.`)
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.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",
@@ -522,7 +502,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)"),
@@ -555,32 +535,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 {
@@ -594,13 +554,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)
}
}
@@ -611,7 +571,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")
}
}
@@ -630,34 +590,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-",
@@ -665,10 +734,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)
}
@@ -692,155 +797,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
}
+5 -21
View File
@@ -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 {
-90
View File
@@ -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())
}
-25
View File
@@ -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)
}
}
-184
View File
@@ -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
View File
@@ -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
+162 -197
View File
@@ -1,173 +1,155 @@
module github.com/projectdiscovery/nuclei/v3
go 1.27.1
go 1.25.7
require (
github.com/andygrunwald/go-jira v1.16.1
github.com/antchfx/htmlquery v1.3.5
github.com/bluele/gcache v0.0.2
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/google/go-github v17.0.0+incompatible
github.com/invopop/jsonschema v0.13.0
github.com/itchyny/gojq v0.12.17
github.com/json-iterator/go v1.1.12
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.10
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.14
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/valyala/fasttemplate v1.2.2
github.com/weppos/publicsuffix-go v0.50.3
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
gopkg.in/yaml.v2 v2.4.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
github.com/DataDog/gostackparse v0.7.0
github.com/FalconOpsLLC/goexec v0.3.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687
github.com/adrg/xdg v0.5.3
github.com/alecthomas/chroma v0.10.0
github.com/alexsnet/go-vnc v0.1.0
github.com/alitto/pond v1.9.2
github.com/andygrunwald/go-jira v1.16.1
github.com/antchfx/htmlquery v1.3.5
github.com/antchfx/xmlquery v1.4.4
github.com/antchfx/xpath v1.3.6
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
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/elazarl/goproxy v1.7.2
github.com/fatih/structs v1.1.0
github.com/fullstorydev/grpcurl v1.9.3
github.com/getkin/kin-openapi v0.144.0
github.com/getsops/sops/v3 v3.9.2
github.com/go-echarts/go-echarts/v2 v2.6.0
github.com/go-git/go-git/v5 v5.19.2
github.com/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/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/invopop/yaml v0.3.1
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/labstack/echo/v4 v4.13.4
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/ory/dockertest/v3 v3.12.0
github.com/pkg/errors v0.9.1
github.com/praetorian-inc/fingerprintx v1.1.15
github.com/projectdiscovery/clistats v0.1.7
github.com/projectdiscovery/dsl v0.8.24
github.com/projectdiscovery/fastdialer v0.5.26
github.com/projectdiscovery/dsl v0.8.19
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/goja_nodejs v0.0.0-20260618132410-8519f75f703d
github.com/projectdiscovery/gologger v1.1.73
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/gologger v1.1.70
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
github.com/projectdiscovery/govaluate v0.0.0-20260504230327-80320480bb6e
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.55
github.com/projectdiscovery/ratelimit v0.0.94
github.com/projectdiscovery/rawhttp v0.2.0
github.com/projectdiscovery/networkpolicy v0.1.40
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.11-0.20261020211236-cde16c1381ec
github.com/projectdiscovery/wappalyzergo v0.3.5
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/redis/go-redis/v9 v9.19.0
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/xid v1.6.0
github.com/projectdiscovery/useragent v0.0.108
github.com/projectdiscovery/utils v0.11.1
github.com/projectdiscovery/wappalyzergo v0.2.84
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/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/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/mongodb v0.42.0
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/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.3.2 // 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
@@ -176,39 +158,35 @@ require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/andybalholm/cascadia v1.3.3 // 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
@@ -219,94 +197,71 @@ 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/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // 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.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/go-github/v30 v30.1.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.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/hashicorp/golang-lru/v2 v2.0.7 // 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
@@ -318,84 +273,82 @@ 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.7 // 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/labstack/gommon v0.4.2 // 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/magiconair/properties v1.8.10 // 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/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.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
github.com/olekukonko/ll v0.0.9 // indirect
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/opencontainers/runc v1.2.8 // 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.39 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/jarm-go v0.0.0-20260910160638-430e2bae86f6 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/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/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/tidwall/btree v1.8.1 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@@ -411,14 +364,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
@@ -430,45 +381,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.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.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
golang.org/x/sync v0.20.0 // indirect
mellium.im/sasl v0.3.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // 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.51.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
gopkg.in/ini.v1 v1.67.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
mellium.im/sasl v0.3.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
)
// https://go.dev/ref/mod#go-mod-file-retract
+238 -346
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
module nuclei-tools
go 1.27.1
go 1.26.1
tool (
github.com/dvyukov/go-fuzz/go-fuzz
-198
View File
@@ -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
}
-443
View File
@@ -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)
}
}
-67
View File
@@ -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)
}
}
-187
View File
@@ -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
}
+86 -163
View File
@@ -2,67 +2,39 @@
package fuzzplayground
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os/exec"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/projectdiscovery/retryablehttp-go"
)
// PlaygroundServer wraps the fuzz playground handler with the lifecycle methods
// used by the integration tests and the standalone playground command.
type PlaygroundServer struct {
handler http.Handler
server *http.Server
}
func GetPlaygroundServer() *echo.Echo {
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.Logger())
func GetPlaygroundServer() *PlaygroundServer {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", indexHandler)
mux.HandleFunc("GET /info", infoHandler)
mux.HandleFunc("GET /redirect", redirectHandler)
mux.HandleFunc("GET /request", requestHandler)
mux.HandleFunc("GET /email", emailHandler)
mux.HandleFunc("GET /permissions", permissionsHandler)
e.GET("/", indexHandler)
e.GET("/info", infoHandler)
e.GET("/redirect", redirectHandler)
e.GET("/request", requestHandler)
e.GET("/email", emailHandler)
e.GET("/permissions", permissionsHandler)
mux.HandleFunc("GET /blog/post", numIdorHandler) // for num based idors like ?id=44
mux.HandleFunc("POST /reset-password", resetPasswordHandler)
mux.HandleFunc("GET /host-header-lab", hostHeaderLabHandler)
mux.HandleFunc("GET /user/{id}/profile", userProfileHandler)
mux.HandleFunc("POST /user", patchUnsanitizedUserHandler)
mux.HandleFunc("GET /blog/posts", getPostsHandler)
handler := recoverPlaygroundRequest(logPlaygroundRequest(mux))
return &PlaygroundServer{handler: handler}
}
func (s *PlaygroundServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handler.ServeHTTP(w, r)
}
func (s *PlaygroundServer) Start(addr string) error {
s.server = &http.Server{
Addr: addr,
Handler: s.handler,
}
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (s *PlaygroundServer) Close() error {
if s.server == nil {
return nil
}
return s.server.Close()
e.GET("/blog/post", numIdorHandler) // for num based idors like ?id=44
e.POST("/reset-password", resetPasswordHandler)
e.GET("/host-header-lab", hostHeaderLabHandler)
e.GET("/user/:id/profile", userProfileHandler)
e.POST("/user", patchUnsanitizedUserHandler)
e.GET("/blog/posts", getPostsHandler)
return e
}
var bodyTemplate = `<html>
@@ -74,8 +46,8 @@ var bodyTemplate = `<html>
</body>
</html>`
func indexHandler(w http.ResponseWriter, _ *http.Request) {
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, `<h1>Fuzzing Playground</h1><hr>
func indexHandler(ctx echo.Context) error {
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, `<h1>Fuzzing Playground</h1><hr>
<ul>
<li><a href="/info?name=test&another=value&random=data">Info Page XSS</a></li>
@@ -93,172 +65,154 @@ func indexHandler(w http.ResponseWriter, _ *http.Request) {
`))
}
func infoHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, fmt.Sprintf("Name of user: %s%s%s", query.Get("name"), query.Get("another"), query.Get("random"))))
func infoHandler(ctx echo.Context) error {
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, fmt.Sprintf("Name of user: %s%s%s", ctx.QueryParam("name"), ctx.QueryParam("another"), ctx.QueryParam("random"))))
}
func redirectHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.URL.Query().Get("redirect_url"), http.StatusFound)
func redirectHandler(ctx echo.Context) error {
url := ctx.QueryParam("redirect_url")
return ctx.Redirect(302, url)
}
func requestHandler(w http.ResponseWriter, r *http.Request) {
requestURL := r.URL.Query().Get("url")
data, err := retryablehttp.DefaultClient().Get(requestURL)
func requestHandler(ctx echo.Context) error {
url := ctx.QueryParam("url")
data, err := retryablehttp.DefaultClient().Get(url)
if err != nil {
writeHTML(w, http.StatusInternalServerError, err.Error())
return
return ctx.HTML(500, err.Error())
}
defer func() {
_ = data.Body.Close()
}()
body, _ := io.ReadAll(data.Body)
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, string(body)))
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, string(body)))
}
func emailHandler(w http.ResponseWriter, r *http.Request) {
text := r.URL.Query().Get("text")
func emailHandler(ctx echo.Context) error {
text := ctx.QueryParam("text")
if strings.Contains(text, "{{") {
trimmed := strings.SplitN(strings.Trim(text[strings.Index(text, "{"):], "{}"), "*", 2)
if len(trimmed) < 2 {
writeHTML(w, http.StatusInternalServerError, "invalid template")
return
return ctx.HTML(500, "invalid template")
}
first, _ := strconv.Atoi(trimmed[0])
second, _ := strconv.Atoi(trimmed[1])
text = strconv.Itoa(first * second)
}
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, fmt.Sprintf("Text: %s", text)))
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, fmt.Sprintf("Text: %s", text)))
}
func permissionsHandler(w http.ResponseWriter, r *http.Request) {
command := r.URL.Query().Get("cmd")
func permissionsHandler(ctx echo.Context) error {
command := ctx.QueryParam("cmd")
fields := strings.Fields(command)
cmd := exec.Command(fields[0], fields[1:]...)
data, _ := cmd.CombinedOutput()
writeHTML(w, http.StatusOK, fmt.Sprintf(bodyTemplate, string(data)))
return ctx.HTML(200, fmt.Sprintf(bodyTemplate, string(data)))
}
func numIdorHandler(w http.ResponseWriter, r *http.Request) {
func numIdorHandler(ctx echo.Context) error {
// validate if any numerical query param is present
// if not, return 400 if so, return 200
for k := range r.URL.Query() {
value := r.URL.Query().Get(k)
if _, err := strconv.Atoi(value); err == nil {
writeJSON(w, http.StatusOK, "Profile Info for user with id "+value)
return
for k := range ctx.QueryParams() {
if _, err := strconv.Atoi(ctx.QueryParam(k)); err == nil {
return ctx.JSON(200, "Profile Info for user with id "+ctx.QueryParam(k))
}
}
writeJSON(w, http.StatusBadRequest, "No numerical query param found")
return ctx.JSON(400, "No numerical query param found")
}
func patchUnsanitizedUserHandler(w http.ResponseWriter, r *http.Request) {
func patchUnsanitizedUserHandler(ctx echo.Context) error {
var user User
contentType := r.Header.Get("Content-Type")
contentType := ctx.Request().Header.Get("Content-Type")
// manually handle unmarshalling data
if strings.Contains(contentType, "application/json") {
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
writeJSON(w, http.StatusInternalServerError, "Invalid JSON data")
return
err := ctx.Bind(&user)
if err != nil {
return ctx.JSON(500, "Invalid JSON data")
}
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
user.Name = r.FormValue("name")
user.Age, _ = strconv.Atoi(r.FormValue("age"))
user.Role = r.FormValue("role")
user.ID, _ = strconv.Atoi(r.FormValue("id"))
user.Name = ctx.FormValue("name")
user.Age, _ = strconv.Atoi(ctx.FormValue("age"))
user.Role = ctx.FormValue("role")
user.ID, _ = strconv.Atoi(ctx.FormValue("id"))
} else if strings.Contains(contentType, "application/xml") {
bin, _ := io.ReadAll(r.Body)
bin, _ := io.ReadAll(ctx.Request().Body)
err := xml.Unmarshal(bin, &user)
if err != nil {
writeJSON(w, http.StatusInternalServerError, "Invalid XML data")
return
return ctx.JSON(500, "Invalid XML data")
}
} else if strings.Contains(contentType, "multipart/form-data") {
user.Name = r.FormValue("name")
user.Age, _ = strconv.Atoi(r.FormValue("age"))
user.Role = r.FormValue("role")
user.ID, _ = strconv.Atoi(r.FormValue("id"))
user.Name = ctx.FormValue("name")
user.Age, _ = strconv.Atoi(ctx.FormValue("age"))
user.Role = ctx.FormValue("role")
user.ID, _ = strconv.Atoi(ctx.FormValue("id"))
} else {
writeJSON(w, http.StatusInternalServerError, "Invalid Content-Type")
return
return ctx.JSON(500, "Invalid Content-Type")
}
err := patchUnsanitizedUser(db, user)
if err != nil {
writeJSON(w, http.StatusInternalServerError, err.Error())
return
return ctx.JSON(500, err.Error())
}
writeJSON(w, http.StatusOK, "User updated successfully")
return ctx.JSON(200, "User updated successfully")
}
// resetPassword mock
func resetPasswordHandler(w http.ResponseWriter, r *http.Request) {
func resetPasswordHandler(c echo.Context) error {
var m map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
writeJSON(w, http.StatusInternalServerError, "Something went wrong")
return
if err := c.Bind(&m); err != nil {
return c.JSON(500, "Something went wrong")
}
host := r.Header.Get("X-Forwarded-For")
host := c.Request().Header.Get("X-Forwarded-For")
if host == "" {
writeJSON(w, http.StatusInternalServerError, "Something went wrong")
return
return c.JSON(500, "Something went wrong")
}
password, ok := m["password"].(string)
if !ok {
writeJSON(w, http.StatusInternalServerError, "Something went wrong")
return
}
resp, err := http.Get("http://internal." + host + "/update?user=1337&pass=" + password)
resp, err := http.Get("http://internal." + host + "/update?user=1337&pass=" + m["password"].(string))
if err != nil {
writeJSON(w, http.StatusInternalServerError, "Something went wrong")
return
return c.JSON(500, "Something went wrong")
}
defer func() {
_ = resp.Body.Close()
}()
writeJSON(w, http.StatusOK, "Password reset successfully")
return c.JSON(200, "Password reset successfully")
}
func hostHeaderLabHandler(w http.ResponseWriter, r *http.Request) {
func hostHeaderLabHandler(c echo.Context) error {
// vulnerable app has custom routing and trusts x-forwarded-host
// to route to internal services
if r.Header.Get("X-Forwarded-Host") != "" {
resp, err := http.Get("http://" + r.Header.Get("X-Forwarded-Host"))
if c.Request().Header.Get("X-Forwarded-Host") != "" {
resp, err := http.Get("http://" + c.Request().Header.Get("X-Forwarded-Host"))
if err != nil {
writeJSON(w, http.StatusInternalServerError, "Something went wrong")
return
return c.JSON(500, "Something went wrong")
}
defer func() {
_ = resp.Body.Close()
}()
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
w.WriteHeader(resp.StatusCode)
_, err = io.Copy(w, resp.Body)
c.Response().Header().Set("Content-Type", resp.Header.Get("Content-Type"))
c.Response().WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Response().Writer, resp.Body)
if err != nil {
return
return c.JSON(500, "Something went wrong")
}
return
}
writeJSON(w, http.StatusOK, "Not a Teapot")
return c.JSON(200, "Not a Teapot")
}
func userProfileHandler(w http.ResponseWriter, r *http.Request) {
val, _ := url.PathUnescape(r.PathValue("id"))
func userProfileHandler(ctx echo.Context) error {
val, _ := url.PathUnescape(ctx.Param("id"))
fmt.Printf("Unescaped: %s\n", val)
user, err := getUnsanitizedUser(db, val)
if err != nil {
writeJSON(w, http.StatusInternalServerError, err.Error())
return
return ctx.JSON(500, err.Error())
}
writeJSON(w, http.StatusOK, user)
return ctx.JSON(200, user)
}
func getPostsHandler(w http.ResponseWriter, r *http.Request) {
lang, err := r.Cookie("lang")
func getPostsHandler(c echo.Context) error {
lang, err := c.Cookie("lang")
if err != nil {
// If the language cookie is missing, default to English
lang = new(http.Cookie)
@@ -266,38 +220,7 @@ func getPostsHandler(w http.ResponseWriter, r *http.Request) {
}
posts, err := getUnsanitizedPostsByLang(db, lang.Value)
if err != nil {
writeJSON(w, http.StatusInternalServerError, err.Error())
return
return c.JSON(http.StatusInternalServerError, err.Error())
}
writeJSON(w, http.StatusOK, posts)
}
func writeHTML(w http.ResponseWriter, statusCode int, value string) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
w.WriteHeader(statusCode)
_, _ = io.WriteString(w, value)
}
func writeJSON(w http.ResponseWriter, statusCode int, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(value)
}
func recoverPlaygroundRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recovered := recover(); recovered != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func logPlaygroundRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
})
return c.JSON(http.StatusOK, posts)
}
-52
View File
@@ -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)
}
}
-54
View File
@@ -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)
}
+1 -27
View File
@@ -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()
-73
View File
@@ -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)
}
-47
View File
@@ -15,9 +15,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"
@@ -81,8 +79,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()
@@ -153,46 +149,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]
}
}
}
-27
View File
@@ -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")
})
}
-56
View File
@@ -1,56 +0,0 @@
package runner
import (
"context"
"time"
llmclient "github.com/projectdiscovery/nuclei/v3/pkg/operators/common/llm"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
utilsllm "github.com/projectdiscovery/utils/llm"
)
// llmMatcherClient adapts the shared provider layer to the minimal interface
// the matchers package needs, keeping matchers free of any provider SDK import.
type llmMatcherClient struct {
client *utilsllm.Client
model string
}
// Model names the model behind a verdict so it can be recorded in the finding.
func (a *llmMatcherClient) Model() string {
return a.model
}
func (a *llmMatcherClient) Complete(ctx context.Context, prompt string, asJSON bool) (string, error) {
return a.client.Complete(ctx, utilsllm.Request{
Prompt: prompt,
Format: utilsllm.Format{JSON: asJSON},
})
}
// configureLLM builds the scan's llm client from options. It returns nil unless
// -llm is set, so nothing reaches a model until the user opts in. The API key is
// read from the environment by the provider layer, never from options.
//
// The client is returned rather than installed globally so two scans in one
// process cannot overwrite each other's provider, cache, budget, or timeout.
func configureLLM(options *types.Options) (llmclient.Client, error) {
if !options.EnableLLM {
return nil, nil
}
client, err := utilsllm.New(utilsllm.Config{
Provider: options.LLMProvider,
BaseURL: options.LLMBaseURL,
Model: options.LLMModel,
Timeout: time.Duration(options.LLMTimeout) * time.Second,
Cache: options.LLMCache,
MaxCalls: options.LLMMaxCalls,
MaxConcurrency: options.LLMConcurrency,
})
if err != nil {
return nil, err
}
return &llmMatcherClient{client: client, model: options.LLMModel}, nil
}
+2 -19
View File
@@ -18,7 +18,6 @@ import (
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/formatter"
"github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/nuclei/v3/internal/server/proxy"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
@@ -56,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)
@@ -179,9 +170,6 @@ func ValidateOptions(options *types.Options) error {
if err := loadProxyServers(options); err != nil {
return err
}
if err := validateReplayProxy(options.ReplayProxy); err != nil {
return err
}
if options.Validate {
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
}
@@ -308,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
}
+1 -22
View File
@@ -49,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)
}
@@ -59,24 +59,3 @@ func TestParseHeadlessOptionalArguments(t *testing.T) {
})
}
}
func TestReadsTargetsFromStdin(t *testing.T) {
for _, tc := range []struct {
name string
options *types.Options
stdinIsPipe bool
want bool
}{
{"piped stdin alone", &types.Options{}, true, true},
{"no pipe", &types.Options{}, false, false},
{"disabled", &types.Options{DisableStdin: true}, true, false},
{"targets given with -u", &types.Options{Targets: goflags.StringSlice{"example.com"}}, true, false},
{"targets given with -l", &types.Options{TargetsFilePath: "targets.txt"}, true, false},
{"uncover query", &types.Options{UncoverQuery: goflags.StringSlice{"example"}}, true, false},
{"uncover enabled", &types.Options{Uncover: true}, true, false},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, readsTargetsFromStdin(tc.options, tc.stdinIsPipe))
})
}
}
-654
View File
@@ -1,654 +0,0 @@
package runner
import (
"context"
"fmt"
"net"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
inputtypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/utils/errkit"
iputil "github.com/projectdiscovery/utils/ip"
mapsutil "github.com/projectdiscovery/utils/maps"
sliceutil "github.com/projectdiscovery/utils/slice"
stringsutil "github.com/projectdiscovery/utils/strings"
syncutil "github.com/projectdiscovery/utils/sync"
urlutil "github.com/projectdiscovery/utils/url"
)
const preflightWorkers = 100
// preflightDialTimeout is intentionally short: this is a coarse filter to skip
// obviously-dead targets before the real scan (which will use full timeouts).
const preflightDialTimeout = 750 * time.Millisecond
type filteringInputProvider struct {
base provider.InputProvider
allowed *mapsutil.SyncLockMap[string, struct{}]
allowCnt int64
execID string
}
func (f *filteringInputProvider) Count() int64 { return f.allowCnt }
func (f *filteringInputProvider) InputType() string { return f.base.InputType() }
func (f *filteringInputProvider) Close() { f.base.Close() }
func (f *filteringInputProvider) Set(executionId string, value string) {
f.base.Set(executionId, value)
}
func (f *filteringInputProvider) SetWithProbe(executionId string, value string, probe inputtypes.InputLivenessProbe) error {
return f.base.SetWithProbe(executionId, value, probe)
}
func (f *filteringInputProvider) SetWithExclusions(executionId string, value string) error {
return f.base.SetWithExclusions(executionId, value)
}
func (f *filteringInputProvider) Iterate(callback func(value *contextargs.MetaInput) bool) {
f.base.Iterate(func(mi *contextargs.MetaInput) bool {
key, err := mi.MarshalString()
if err != nil {
return callback(mi)
}
if _, ok := f.allowed.Get(key); !ok {
return true
}
return callback(mi)
})
}
// preflightResolveAndPortScan resolves hostname targets and performs a TCP connect scan for ports
// required by loaded templates. Targets that are non-resolvable hostnames or have no relevant open
// ports are filtered out from the input provider.
func (r *Runner) preflightResolveAndPortScan(store *loader.Store) error {
if r.inputProvider == nil {
return nil
}
// MultiFormat inputs may represent complete requests; skip preflight for now.
if r.inputProvider.InputType() == provider.MultiFormatInputProvider {
return nil
}
finalTemplates := []*templates.Template{}
finalTemplates = append(finalTemplates, store.Templates()...)
finalTemplates = append(finalTemplates, store.Workflows()...)
if len(finalTemplates) == 0 {
return nil
}
dialers := protocolstate.GetDialersWithId(r.options.ExecutionId)
if dialers == nil || dialers.Fastdialer == nil {
return fmt.Errorf("dialers not initialized for %s", r.options.ExecutionId)
}
portsPopularity := portsPopularityFromTemplates(finalTemplates)
// Also include ports explicitly present in input list (ip:port or URL with port),
// so that a user-provided port isn't dropped even if templates didn't specify it.
var inputs []preflightTarget
portsFromInputs := map[string]struct{}{}
var totalTargets atomic.Int64
r.inputProvider.Iterate(func(mi *contextargs.MetaInput) bool {
totalTargets.Add(1)
key, err := mi.MarshalString()
if err != nil {
return true
}
// Use Target()/CustomIP so preflight resolves the same address the real
// scan would connect to (honors ReqResp-backed targets and CustomIP),
// instead of only the raw Input which can mismatch routing state.
inputs = append(inputs, preflightTarget{key: key, target: mi.Target(), customIP: mi.CustomIP})
extractPortsFromInput(portsFromInputs, mi.Target())
return true
})
portsToScan := sliceutil.Dedupe(append(keysOfPopularity(portsPopularity), keysOf(portsFromInputs)...))
portsToScan = filterValidPorts(portsToScan)
// Sort by "likely-open" order (nmap-ish/common ports first), then numeric for determinism.
likelyRank := likelyOpenPortRank()
sort.Slice(portsToScan, func(i, j int) bool {
pi, pj := portsToScan[i], portsToScan[j]
ri, okRi := likelyRank[pi]
rj, okRj := likelyRank[pj]
// ranked ports first
if okRi != okRj {
return okRi
}
// among ranked ports, lower rank wins
if okRi && okRj && ri != rj {
return ri < rj
}
// numeric asc
ni, _ := strconv.Atoi(pi)
nj, _ := strconv.Atoi(pj)
return ni < nj
})
// If no ports were found, nothing to scan -> keep all.
if len(portsToScan) == 0 {
return nil
}
if !r.options.Silent {
r.Logger.Info().Msgf("Running preflight portscan (workers=%d, ports=%d, targets=%d)", preflightWorkers, len(portsToScan), totalTargets.Load())
}
swg, err := syncutil.New(syncutil.WithSize(preflightWorkers))
if err != nil {
return err
}
// Resolve all targets (once) up-front so we can optionally run a single batched TCP dial scan.
// Map original input key -> resolved IPs (deduped).
// SyncLockMap requires comparable values; store resolved IPs as a comma-separated string.
resolvedIPsByKey := mapsutil.NewSyncLockMap[string, string]()
allIPsSet := mapsutil.NewSyncLockMap[string, struct{}]()
var resolveProcessed atomic.Int64
var resolveDNSFail atomic.Int64
for _, t := range inputs {
swg.Add()
go func(t preflightTarget) {
defer swg.Done()
host, _, _, _ := hostForResolveAndScan(t.target)
if t.customIP != "" {
host = t.customIP
}
if host == "" {
resolveDNSFail.Add(1)
resolveProcessed.Add(1)
return
}
ips := []string{}
if iputil.IsIP(host) {
ips = append(ips, host)
} else {
dns, err := dialers.Fastdialer.GetDNSData(host)
if err != nil || (len(dns.A) == 0 && len(dns.AAAA) == 0) {
resolveDNSFail.Add(1)
resolveProcessed.Add(1)
return
}
ips = append(ips, dns.A...)
ips = append(ips, dns.AAAA...)
}
ips = sliceutil.Dedupe(ips)
if len(ips) == 0 {
resolveDNSFail.Add(1)
resolveProcessed.Add(1)
return
}
// store
// (small contention; acceptable at preflight scale)
_ = resolvedIPsByKey.Set(t.key, strings.Join(ips, ","))
for _, ip := range ips {
_ = allIPsSet.Set(ip, struct{}{})
}
resolveProcessed.Add(1)
}(t)
}
swg.Wait()
// Prepare list of all IPs for scan.
allIPsMap := allIPsSet.GetAll()
allIPs := make([]string, 0, len(allIPsMap))
for ip := range allIPsMap {
allIPs = append(allIPs, ip)
}
sort.Strings(allIPs)
// we do fast TCP dial scanning against resolved IPs.
if !r.options.Silent {
r.Logger.Info().Msgf("Preflight resolution: total=%d resolvable=%d unresolvable=%d", totalTargets.Load(), int64(len(resolvedIPsByKey.GetAll())), resolveDNSFail.Load())
}
allowed := mapsutil.NewSyncLockMap[string, struct{}]()
var dnsFail atomic.Int64
var portFail atomic.Int64
var kept atomic.Int64
var processed atomic.Int64
perPortOpen := mapsutil.NewSyncLockMap[string, *atomic.Int64]()
// Periodic progress logging
// Always enabled unless running in silent mode.
debugProgress := true
stopProgress := make(chan struct{})
if debugProgress && !r.options.Silent {
start := time.Now()
go func() {
t := time.NewTicker(1 * time.Second)
defer t.Stop()
var lastProcessed int64
for {
select {
case <-t.C:
p := processed.Load()
if p == lastProcessed {
continue
}
lastProcessed = p
total := totalTargets.Load()
k := kept.Load()
df := dnsFail.Load()
pf := portFail.Load()
dropped := p - k
r.Logger.Info().Msgf("Preflight progress: %d/%d processed (kept=%d dropped=%d dns_fail=%d port_fail=%d elapsed=%s)",
p, total, k, dropped, df, pf, time.Since(start).Truncate(time.Second))
case <-stopProgress:
return
}
}
}()
}
for _, t := range inputs {
swg.Add()
go func(t preflightTarget) {
defer swg.Done()
ok, openPort, reason := r.preflightOneResolved(t.key, t.target, portsToScan, resolvedIPsByKey)
processed.Add(1)
if ok {
_ = allowed.Set(t.key, struct{}{})
kept.Add(1)
if openPort != "" {
counter, _ := perPortOpen.Get(openPort)
if counter == nil {
counter = &atomic.Int64{}
_ = perPortOpen.Set(openPort, counter)
}
counter.Add(1)
}
return
}
switch reason {
case preflightReasonDNS:
dnsFail.Add(1)
case preflightReasonPorts:
portFail.Add(1)
}
}(t)
}
swg.Wait()
close(stopProgress)
// Apply filtering wrapper
allowedAll := allowed.GetAll()
r.inputProvider = &filteringInputProvider{
base: r.inputProvider,
allowed: allowed,
allowCnt: int64(len(allowedAll)),
execID: r.options.ExecutionId,
}
// Summary
if !r.options.Silent {
dropped := totalTargets.Load() - kept.Load()
r.Logger.Info().Msgf("Preflight summary: total=%d kept=%d filtered_dns=%d filtered_ports=%d",
totalTargets.Load(), kept.Load(), dnsFail.Load(), portFail.Load())
r.Logger.Info().Msgf("Preflight targets: dropped=%d left=%d", dropped, kept.Load())
perPortOpenAll := perPortOpen.GetAll()
if len(perPortOpenAll) > 0 {
type kv struct {
port string
count int64
}
kvs := make([]kv, 0, len(perPortOpenAll))
for p, c := range perPortOpenAll {
if c == nil {
continue
}
kvs = append(kvs, kv{port: p, count: c.Load()})
}
sort.Slice(kvs, func(i, j int) bool {
if kvs[i].count == kvs[j].count {
return kvs[i].port < kvs[j].port
}
return kvs[i].count > kvs[j].count
})
parts := make([]string, 0, len(kvs))
for _, item := range kvs {
parts = append(parts, fmt.Sprintf("%s=%d", item.port, item.count))
}
r.Logger.Info().Msgf("Preflight open-port distribution: %s", strings.Join(parts, " "))
}
}
_ = gologger.DefaultLogger // ensure logger imported even when silent builds vary
return nil
}
type preflightTarget struct {
key string
target string
customIP string
}
type preflightReason int
const (
preflightReasonNone preflightReason = iota
preflightReasonDNS
preflightReasonPorts
)
func (r *Runner) preflightOneResolved(key string, raw string, ports []string, resolved *mapsutil.SyncLockMap[string, string]) (ok bool, openPort string, reason preflightReason) {
resolvedIPsCSV, _ := resolved.Get(key)
if resolvedIPsCSV == "" {
return false, "", preflightReasonDNS
}
ips := strings.Split(resolvedIPsCSV, ",")
// TCP dial scan using resolved IPs
host, schemePort, hasSchemePort, _ := hostForResolveAndScan(raw)
ordered := ports
if hasSchemePort && schemePort != "" {
ordered = append([]string{schemePort}, ports...)
ordered = sliceutil.Dedupe(ordered)
}
timeout := preflightDialTimeout
if r.options.Timeout > 0 {
t := time.Duration(r.options.Timeout) * time.Second
if t > 0 && t < timeout {
timeout = t
}
}
// Use net.Dialer directly for strict timeout enforcement.
// We already resolved IPs, so we don't need fastdialer DNS behavior here.
// This avoids rare cases where proxy dialers / custom dial stacks may not respect ctx cancellation promptly.
d := &net.Dialer{Timeout: timeout}
// Per-host parallelism: probe up to 3 ports concurrently, stop on first success.
ctx, cancelAll := context.WithCancel(context.Background())
defer cancelAll()
type hit struct {
port string
}
resultCh := make(chan hit, 1)
portsCh := make(chan string)
worker := func() {
for p := range portsCh {
// Stop quickly if someone already found an open port.
select {
case <-ctx.Done():
return
default:
}
for _, ip := range ips {
_ = host // keep for debugging parity
dctx, cancel := context.WithTimeout(ctx, timeout)
conn, err := d.DialContext(dctx, "tcp", net.JoinHostPort(ip, p))
cancel()
if err == nil {
_ = conn.Close()
// Best-effort: first hit wins.
select {
case resultCh <- hit{port: p}:
cancelAll()
default:
}
return
}
// If ctx cancelled (other worker won), stop early.
select {
case <-ctx.Done():
return
default:
}
}
}
}
var wg sync.WaitGroup
workers := 3
if len(ordered) < workers {
workers = len(ordered)
}
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker()
}()
}
go func() {
defer close(portsCh)
for _, p := range ordered {
select {
case <-ctx.Done():
return
case portsCh <- p:
}
}
}()
// Wait for either a hit or all workers to finish.
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case h := <-resultCh:
<-done
return true, h.port, preflightReasonNone
case <-done:
return false, "", preflightReasonPorts
}
}
func portsPopularityFromTemplates(tpls []*templates.Template) map[string]int {
out := map[string]int{}
for _, tpl := range tpls {
// HTTP templates imply 80/443 for preflight.
if len(tpl.RequestsHTTP) > 0 || len(tpl.RequestsWithHTTP) > 0 || len(tpl.RequestsHeadless) > 0 {
out["80"]++
out["443"]++
}
// Network templates declare ports directly.
for _, req := range tpl.RequestsNetwork {
for _, p := range splitPorts(req.Port) {
out[p]++
}
}
for _, req := range tpl.RequestsWithTCP {
for _, p := range splitPorts(req.Port) {
out[p]++
}
}
// Javascript templates may include args.Port (comma-separated).
for _, req := range tpl.RequestsJavascript {
for _, p := range extractPortsFromJSArgs(req.Args) {
out[p]++
}
}
}
return out
}
func keysOfPopularity(m map[string]int) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// likelyOpenPortRank returns a heuristic "most likely to be open" ranking for common TCP ports.
// This is intentionally static (fast + deterministic) and loosely aligns with what scanners like nmap
// tend to prioritize (common services first).
func likelyOpenPortRank() map[string]int {
// Lower index = higher priority.
// Keep this list small-ish but useful; anything not in here falls back to template popularity + numeric.
common := []string{
"80", "443",
"22", "21", "23",
"25", "110", "143", "465", "587", "993", "995",
"53",
"3389",
"445", "139",
"135",
"3306", "5432", "1433", "1521",
"6379", "27017",
"9200", "9300",
"8080", "8443", "8000", "8008", "8081", "8888",
"9201",
"161", "162",
"389", "636",
"5900",
"11211",
"69", "123",
"1194",
"500", "4500",
}
rank := make(map[string]int, len(common))
for i, p := range common {
// do not overwrite if duplicates
if _, ok := rank[p]; !ok {
rank[p] = i
}
}
return rank
}
func extractPortsFromJSArgs(args map[string]interface{}) []string {
if args == nil {
return nil
}
for k, v := range args {
if strings.EqualFold(k, "Port") {
s := fmt.Sprint(v)
return splitPorts(s)
}
}
return nil
}
func splitPorts(s string) []string {
if s == "" {
return nil
}
var out []string
for p := range strings.SplitSeq(s, ",") {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func extractPortsFromInput(dst map[string]struct{}, input string) {
if dst == nil {
return
}
in := strings.TrimSpace(input)
if in == "" {
return
}
low := strings.ToLower(in)
if strings.HasPrefix(low, "http://") {
dst["80"] = struct{}{}
}
if strings.HasPrefix(low, "https://") {
dst["443"] = struct{}{}
}
// URL parsing (best effort)
if u, err := urlutil.Parse(in); err == nil && u != nil {
if p := u.Port(); p != "" {
dst[p] = struct{}{}
} else if u.Scheme == "http" {
dst["80"] = struct{}{}
} else if u.Scheme == "https" {
dst["443"] = struct{}{}
}
return
}
// host:port
_, p, err := net.SplitHostPort(in)
if err == nil && p != "" {
dst[p] = struct{}{}
}
}
func hostForResolveAndScan(raw string) (host string, schemeDefaultPort string, hasSchemePort bool, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", "", false, errkit.New("empty input")
}
// If it looks like URL, parse and extract hostname.
if stringsutil.ContainsAny(raw, "://") {
u, perr := urlutil.ParseAbsoluteURL(raw, false)
if perr == nil && u != nil {
host = u.Hostname()
if u.Port() != "" {
return host, "", false, nil
}
switch strings.ToLower(u.Scheme) {
case "http":
return host, "80", true, nil
case "https":
return host, "443", true, nil
}
return host, "", false, nil
}
}
// Try host:port form
h, _, serr := net.SplitHostPort(raw)
if serr == nil && h != "" {
return h, "", false, nil
}
// Bare host/ip
return raw, "", false, nil
}
func keysOf(m map[string]struct{}) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func filterValidPorts(ports []string) []string {
out := make([]string, 0, len(ports))
for _, p := range ports {
if p == "" {
continue
}
// allow numeric only
if !isNumeric(p) {
continue
}
i, err := strconv.Atoi(p)
if err != nil || i < 1 || i > 65535 {
continue
}
out = append(out, p)
}
return sliceutil.Dedupe(out)
}
func isNumeric(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
-18
View File
@@ -65,21 +65,3 @@ func loadProxyServers(options *types.Options) error {
}
return nil
}
// validateReplayProxy rejects a -replay-proxy value the replay client cannot dial
func validateReplayProxy(raw string) error {
if raw == "" {
return nil
}
proxyURL, err := url.Parse(raw)
if err != nil {
return errkit.Wrapf(err, "invalid replay proxy %q", raw)
}
switch proxyURL.Scheme {
case proxyutils.HTTP, proxyutils.HTTPS, proxyutils.SOCKS5:
if proxyURL.Host != "" {
return nil
}
}
return fmt.Errorf("invalid replay proxy %q: want an http, https or socks5 URL", raw)
}
-16
View File
@@ -1,16 +0,0 @@
package runner
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestValidateReplayProxy(t *testing.T) {
for _, valid := range []string{"", "http://127.0.0.1:8080", "https://proxy.example:8443", "socks5://127.0.0.1:1080"} {
require.NoError(t, validateReplayProxy(valid), valid)
}
for _, invalid := range []string{"127.0.0.1:8080", "ftp://127.0.0.1:21", "http://", "://bad"} {
require.Error(t, validateReplayProxy(invalid), invalid)
}
}
+51 -229
View File
@@ -6,8 +6,6 @@ import (
"os"
"path/filepath"
"reflect"
"slices"
"sort"
"strings"
"sync/atomic"
"time"
@@ -15,7 +13,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"
@@ -47,7 +44,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"
@@ -56,11 +52,9 @@ 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"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/uncover"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
@@ -112,7 +106,6 @@ type Runner struct {
httpApiEndpoint *httpapi.Server
fuzzStats *fuzzStats.Tracker
dastServer *server.DASTServer
llmClient llmclient.Client
}
// New creates a new client for running the enumeration process.
@@ -122,31 +115,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)
}
}
@@ -163,15 +141,15 @@ func New(options *types.Options) (*Runner, error) {
DisablePublicTemplates: options.PublicTemplateDisableDownload,
}
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)
}
}
@@ -181,7 +159,6 @@ func New(options *types.Options) (*Runner, error) {
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())
@@ -198,25 +175,21 @@ 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
}
yaml.StrictSyntax = !options.NoStrictSyntax
yaml.AllowLocalFileAccess = options.AllowLocalFileAccess
if options.Headless {
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
}
@@ -225,8 +198,7 @@ func New(options *types.Options) (*Runner, error) {
var httpclient *retryablehttp.Client
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
var err error
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{}, "")
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{})
if err != nil {
return nil, err
}
@@ -235,12 +207,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
}
@@ -250,7 +220,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
}
@@ -313,12 +282,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 {
@@ -329,11 +296,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
@@ -359,30 +324,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
}
}
@@ -392,7 +352,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,
@@ -430,7 +389,6 @@ func New(options *types.Options) (*Runner, error) {
// 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)
@@ -443,21 +401,13 @@ 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)
} else {
runner.rateLimiter = utils.GetRateLimiter(context.Background(), options.RateLimit, options.RateLimitDuration)
}
runner.rateLimiter = utils.GetRateLimiter(context.Background(), options.RateLimit, options.RateLimitDuration)
// Initialization successful, disable cleanup on error
cleanupOnError = false
return runner, nil
}
@@ -466,7 +416,6 @@ func (r *Runner) runStandardEnumeration(executerOpts *protocols.ExecutorOptions,
if r.options.AutomaticScan {
return r.executeSmartWorkflowInput(executerOpts, store, engine)
}
return r.executeTemplatesInput(store, engine)
}
@@ -478,29 +427,6 @@ func (r *Runner) Close() {
if r.httpStats != nil {
r.httpStats.DisplayTopStats(r.options.NoColor)
}
if newConns, reusedConns := httpclientpool.GetConnectionStats(); newConns+reusedConns > 0 {
total := newConns + reusedConns
ratio := float64(reusedConns) / float64(total) * 100
gologger.Info().Msgf("HTTP connections: %d total, %d new, %d reused (%.1f%%)", total, newConns, reusedConns, ratio)
// Per-host breakdown is opt-in (verbose) since large scans touch many hosts.
if r.options.Verbose {
perHost := httpclientpool.GetPerHostConnectionStats()
sort.Slice(perHost, func(i, j int) bool {
return (perHost[i].New + perHost[i].Reused) > (perHost[j].New + perHost[j].Reused)
})
const maxPerHostLines = 20
for i, s := range perHost {
if i >= maxPerHostLines {
gologger.Info().Msgf("HTTP connections: ... and %d more host(s)", len(perHost)-maxPerHostLines)
break
}
hostTotal := s.New + s.Reused
hostRatio := float64(s.Reused) / float64(hostTotal) * 100
gologger.Info().Msgf("HTTP connections [%s]: %d total, %d new, %d reused (%.1f%%)", s.Host, hostTotal, s.New, s.Reused, hostRatio)
}
}
}
// dump hosterrors cache
if r.hostErrors != nil {
r.hostErrors.Close()
@@ -581,14 +507,9 @@ func (r *Runner) setupPDCPUpload(writer output.Writer) output.Writer {
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() error {
// Reset connection-reuse counters so the summary logged on Close()
// reflects only this run, not totals accumulated across multiple
// in-process executions (e.g. SDK / embedded usage).
httpclientpool.ResetConnectionStats()
// 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,
@@ -605,7 +526,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,
@@ -614,28 +535,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()
}
@@ -645,18 +549,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)
@@ -667,7 +569,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,
@@ -728,10 +629,6 @@ func (r *Runner) RunEnumeration() error {
executorOpts.HostErrorsCache = cache
}
if r.options.HostBackoff {
executorOpts.HostBackoff = hostbackoff.New(hostbackoff.Config{Max: r.options.HostBackoffMax})
}
executorEngine := core.New(r.options)
executorEngine.SetExecuterOptions(executorOpts)
@@ -743,15 +640,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 (!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)
@@ -784,7 +674,7 @@ func (r *Runner) RunEnumeration() error {
if err := store.ValidateTemplates(); err != nil {
return err
}
if stats.GetValue(templates.TemplateSyntaxErrorStats) == 0 && stats.GetValue(templates.TemplateSyntaxWarningStats) == 0 && stats.GetValue(templates.TemplateRuntimeWarningStats) == 0 {
if stats.GetValue(templates.SyntaxErrorStats) == 0 && stats.GetValue(templates.SyntaxWarningStats) == 0 && stats.GetValue(templates.RuntimeWarningsStats) == 0 {
r.Logger.Info().Msgf("All templates validated successfully")
} else {
return errors.New("encountered errors while performing template validation")
@@ -794,10 +684,6 @@ func (r *Runner) RunEnumeration() error {
if err := store.Load(); err != nil {
return err
}
if profiles != nil {
profiles.Prepare(slices.Concat(store.Templates(), store.Workflows()))
executorOpts.TargetScope = profiles
}
// TODO: remove below functions after v3 or update warning messages
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
@@ -815,15 +701,6 @@ func (r *Runner) RunEnumeration() error {
_ = r.inputProvider.SetWithExclusions(r.options.ExecutionId, host)
}
}
// Preflight: resolve hosts + portscan for ports required by loaded templates, then filter inputs.
// This reduces time spent on non-resolvable targets or targets with no relevant open ports.
// Preflight is a best-effort optimization: on failure we log and continue with the full input set.
if r.options.PreflightPortScan {
if err := r.preflightResolveAndPortScan(store); err != nil {
gologger.Warning().Msgf("preflight resolve/portscan failed, continuing without input filtering: %s", err)
}
}
// display execution info like version , templates used etc
r.displayExecutionInfo(store)
@@ -846,15 +723,13 @@ func (r *Runner) RunEnumeration() error {
executorOpts.InputHelper.InputsHTTP = inputHelpers
}
inputCount := int(r.inputProvider.Count())
// initialize stats worker ( this is no-op unless nuclei is built with stats build tag)
// during execution a directory with 2 files will be created in the current directory
// config.json - containing below info
// events.jsonl - containing all start and end times of all templates
events.InitWithConfig(&events.ScanConfig{
Name: "nuclei-stats", // make this configurable
TargetCount: inputCount,
TargetCount: int(r.inputProvider.Count()),
TemplatesCount: len(store.Templates()) + len(store.Workflows()),
TemplateConcurrency: r.options.TemplateThreads,
PayloadConcurrency: r.options.PayloadConcurrency,
@@ -896,25 +771,6 @@ func (r *Runner) RunEnumeration() error {
r.progress.Stop()
timeTaken := time.Since(now)
// Print pool/tracker stats if available (single dialers lookup, reads under lock)
if dialers := protocolstate.GetDialersWithId(r.options.ExecutionId); dialers != nil {
dialers.Lock()
perHostRateLimitPool := dialers.PerHostRateLimitPool
httpToHTTPSPortTracker := dialers.HTTPToHTTPSPortTracker
dialers.Unlock()
if pool, ok := perHostRateLimitPool.(interface{ PrintStats() }); ok {
pool.PrintStats()
}
if pool, ok := perHostRateLimitPool.(interface{ PrintPerHostPPSStats() }); ok {
pool.PrintPerHostPPSStats()
}
if tracker, ok := httpToHTTPSPortTracker.(interface{ PrintStats() }); ok {
tracker.PrintStats()
}
}
// todo: error propagation without canonical straight error check is required by cloud?
// use safe dereferencing to avoid potential panics in case of previous unchecked errors
if v := ptrutil.Safe(results); !v.Load() {
@@ -932,24 +788,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()
@@ -1027,44 +865,49 @@ func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine)
return results, nil
}
// displayExecutionInfo prints parser stats, version info, and scan counts.
// displayExecutionInfo displays misc info about the nuclei engine execution
func (r *Runner) displayExecutionInfo(store *loader.Store) {
// Display parser stats for templates loaded into the store.
stats.Display(templates.TemplateSyntaxWarningStats)
stats.Display(templates.TemplateSyntaxErrorStats)
stats.Display(templates.TemplateRuntimeWarningStats)
// Display stats for any loaded templates' syntax warnings or errors
stats.Display(templates.SyntaxWarningStats)
stats.Display(templates.SyntaxErrorStats)
stats.Display(templates.RuntimeWarningsStats)
tmplCount := len(store.Templates())
workflowCount := len(store.Workflows())
if r.options.Verbose || (tmplCount == 0 && workflowCount == 0) {
// Excluded-template stats are noisy during normal scans, but useful in verbose mode
// and when no runnable templates remain.
for _, capability := range templates.AllCapabilities() {
stats.ForceDisplayWarning(capability.Stat())
}
stats.ForceDisplayWarning(templates.ExcludedWeakMatcherTemplateStats)
// only print these stats in verbose mode
stats.ForceDisplayWarning(templates.ExcludedHeadlessTmplStats)
stats.ForceDisplayWarning(templates.ExcludedCodeTmplStats)
stats.ForceDisplayWarning(templates.ExcludedDastTmplStats)
stats.ForceDisplayWarning(templates.TemplatesExcludedStats)
stats.ForceDisplayWarning(templates.ExcludedFileStats)
stats.ForceDisplayWarning(templates.ExcludedSelfContainedStats)
}
if tmplCount == 0 && workflowCount == 0 {
// if dast flag is used print explicit warning
if r.options.DAST {
r.Logger.Warning().Msg("No DAST templates found")
}
stats.ForceDisplayWarning(templates.SkippedUnverifiedCodeTemplateStats)
stats.ForceDisplayWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
stats.ForceDisplayWarning(templates.SkippedCodeTmplTamperedStats)
} else {
stats.DisplayAsWarning(templates.SkippedUnverifiedCodeTemplateStats)
stats.DisplayAsWarning(templates.SkippedUnverifiedJavascriptTemplateStats)
stats.DisplayAsWarning(templates.SkippedCodeTmplTamperedStats)
}
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
stats.ForceDisplayWarning(templates.SkippedUnverifiedTemplateStats)
stats.ForceDisplayWarning(templates.SkippedRequestSignatureTemplateStats)
stats.ForceDisplayWarning(templates.SkippedUnsignedStats)
stats.ForceDisplayWarning(templates.SkippedRequestSignatureStats)
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)
@@ -1098,40 +941,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
-17
View File
@@ -1,17 +0,0 @@
package runner
import (
"github.com/projectdiscovery/nuclei/v3/pkg/input/targetprofile"
)
// targetProfiles returns the per-target profiles read from input, or nil when
// no target has one.
func (r *Runner) targetProfiles() *targetprofile.Registry {
provider, ok := r.inputProvider.(interface {
TargetProfiles() *targetprofile.Registry
})
if !ok || !provider.TargetProfiles().Scoped() {
return nil
}
return provider.TargetProfiles()
}
+2 -2
View File
@@ -7,13 +7,13 @@ import (
"strings"
"github.com/alecthomas/chroma/quick"
jsoniter "github.com/json-iterator/go"
"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/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
)
// log available templates for verbose (-vv)
@@ -99,7 +99,7 @@ func (r *Runner) listAvailableTags(tagsMap map[string]int) {
for _, tag := range tagsList {
if r.options.JSONL {
marshalled, _ := json.Marshal(tag)
marshalled, _ := jsoniter.Marshal(tag)
r.Logger.Print().Msgf("%s", string(marshalled))
} else {
r.Logger.Print().Msgf("%s (%d)", tag.Key, tag.Value)
-45
View File
@@ -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)
}
}
+3 -11
View File
@@ -28,7 +28,6 @@ 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"
@@ -107,10 +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})
}
executorEngine := core.New(opts.Options)
executorEngine.SetExecuterOptions(executorOpts)
@@ -122,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)
@@ -195,13 +190,10 @@ func (n *nucleiExecutor) ExecuteScan(target PostRequestsHandlerRequest) error {
}
func (n *nucleiExecutor) Close() {
if n == nil || n.executorOpts == nil {
return
}
if n.executorOpts.FuzzStatsDB != nil {
n.executorOpts.FuzzStatsDB.Close()
}
if n.options != nil && n.options.Interactsh != nil {
if n.options.Interactsh != nil {
_ = n.options.Interactsh.Close()
}
if n.executorOpts.InputHelper != nil {
-194
View File
@@ -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
}
-90
View File
@@ -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)
}
-452
View File
@@ -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
}
-349
View File
@@ -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)
}
-12
View File
@@ -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
}
-18
View File
@@ -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))
}
+52 -293
View File
@@ -2,11 +2,8 @@ package server
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"strings"
@@ -14,8 +11,9 @@ import (
"time"
"github.com/alitto/pond"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"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"
@@ -27,12 +25,11 @@ import (
// DASTServer is a server that performs execution of fuzzing templates
// on user input passed to the API.
type DASTServer struct {
httpServer *http.Server
echo *echo.Echo
options *Options
tasksPool *pond.WorkerPool
deduplicator *requestDeduplicator
scopeManager *scope.Manager
proxy *proxy.Proxy
startTime time.Time
// metrics
@@ -53,21 +50,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,73 +98,19 @@ 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{},
nucleiExecutor: &nucleiExecutor{
executorOpts: &protocols.ExecutorOptions{
FuzzStatsDB: fuzzStatsDB,
@@ -196,147 +124,91 @@ 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()
}
if s.httpServer != nil {
_ = s.httpServer.Close()
}
if s.tasksPool != nil {
s.tasksPool.StopAndWaitFor(1 * time.Minute)
}
s.nucleiExecutor.Close()
_ = s.echo.Close()
s.tasksPool.StopAndWaitFor(1 * time.Minute)
}
func (s *DASTServer) buildURL(endpoint string) string {
values := make(url.Values)
opts := s.optionsOrDefault()
if opts.Token != "" {
values.Set("token", opts.Token)
if s.options.Token != "" {
values.Set("token", s.options.Token)
}
// Use url.URL struct to safely construct the URL
u := &url.URL{
Scheme: "http",
Host: opts.Address,
Host: s.options.Address,
Path: endpoint,
RawQuery: values.Encode(),
}
return u.String()
}
func (s *DASTServer) optionsOrDefault() *Options {
if s.options != nil {
return s.options
}
return &Options{}
}
func (s *DASTServer) setupHandlers(onlyStats bool) {
opts := s.optionsOrDefault()
mux := http.NewServeMux()
if !onlyStats && opts.EnableFuzzAPI {
mux.HandleFunc("POST /fuzz", s.handleRequest)
e := echo.New()
e.Use(middleware.Recover())
if s.options.Verbose {
cfg := middleware.DefaultLoggerConfig
cfg.Skipper = func(c echo.Context) bool {
// Skip /stats and /stats.json
return c.Request().URL.Path == "/stats" || c.Request().URL.Path == "/stats.json"
}
e.Use(middleware.LoggerWithConfig(cfg))
}
mux.HandleFunc("GET /stats", s.handleStats)
mux.HandleFunc("GET /stats.json", s.handleStatsJSON)
if !onlyStats && opts.ProxyAddress != "" {
mux.HandleFunc("GET /ca", s.handleProxyCA)
e.Use(middleware.CORS())
if s.options.Token != "" {
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
KeyLookup: "query:token",
Validator: func(key string, c echo.Context) (bool, error) {
return key == s.options.Token, nil
},
}))
}
handler := http.Handler(mux)
if opts.Token != "" {
handler = s.tokenAuthMiddleware(handler)
e.HideBanner = true
// POST /fuzz - Queue a request for fuzzing
if !onlyStats {
e.POST("/fuzz", s.handleRequest)
}
handler = corsMiddleware(handler)
if opts.Verbose {
handler = requestLoggerMiddleware(handler)
}
handler = recoverMiddleware(handler)
e.GET("/stats", s.handleStats)
e.GET("/stats.json", s.handleStatsJSON)
s.httpServer = &http.Server{Handler: handler}
s.echo = e
}
func (s *DASTServer) Start() error {
if s.httpServer == nil {
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.echo.Start(s.options.Address); 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"`
URL string `json:"url"`
}
func (s *DASTServer) handleRequest(w http.ResponseWriter, r *http.Request) {
func (s *DASTServer) handleRequest(c echo.Context) error {
var req PostRequestsHandlerRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err := c.Bind(&req); err != nil {
fmt.Printf("Error binding request: %s\n", err)
writeServerJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
return err
}
// Validate the request
if req.RawHTTP == "" || req.URL == "" {
fmt.Printf("Missing required fields\n")
writeServerJSON(w, http.StatusBadRequest, map[string]string{"error": "missing required fields"})
return
return c.JSON(400, map[string]string{"error": "missing required fields"})
}
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)
})
return c.NoContent(200)
}
type StatsResponse struct {
@@ -353,8 +225,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 +237,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 +256,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
@@ -422,116 +274,23 @@ func (s *DASTServer) getStats() (StatsResponse, error) {
//go:embed templates/index.html
var indexTemplate string
func (s *DASTServer) handleStats(w http.ResponseWriter, _ *http.Request) {
func (s *DASTServer) handleStats(c echo.Context) error {
stats, err := s.getStats()
if err != nil {
writeServerJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
return c.JSON(500, map[string]string{"error": err.Error()})
}
tmpl, err := template.New("index").Parse(indexTemplate)
if err != nil {
writeServerJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if err := tmpl.Execute(w, stats); err != nil {
writeServerJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return c.JSON(500, map[string]string{"error": err.Error()})
}
return tmpl.Execute(c.Response().Writer, stats)
}
func (s *DASTServer) handleStatsJSON(w http.ResponseWriter, _ *http.Request) {
func (s *DASTServer) handleStatsJSON(c echo.Context) error {
resp, err := s.getStats()
if err != nil {
writeServerJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
return c.JSON(500, map[string]string{"error": err.Error()})
}
writeServerJSONPretty(w, http.StatusOK, resp)
}
func (s *DASTServer) tokenAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
writeServerJSON(w, http.StatusBadRequest, map[string]string{"message": "missing key in the query string"})
return
}
if token != s.optionsOrDefault().Token {
writeServerJSON(w, http.StatusUnauthorized, map[string]string{"message": "Unauthorized"})
return
}
next.ServeHTTP(w, r)
})
}
func corsMiddleware(next http.Handler) http.Handler {
const allowMethods = "GET,HEAD,PUT,PATCH,POST,DELETE"
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
w.Header().Add("Vary", "Origin")
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
if r.Method == http.MethodOptions {
if origin != "" {
w.Header().Add("Vary", "Access-Control-Request-Method")
w.Header().Add("Vary", "Access-Control-Request-Headers")
w.Header().Set("Access-Control-Allow-Methods", allowMethods)
if headers := r.Header.Get("Access-Control-Request-Headers"); headers != "" {
w.Header().Set("Access-Control-Allow-Headers", headers)
}
}
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func requestLoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/stats" || r.URL.Path == "/stats.json" {
next.ServeHTTP(w, r)
return
}
recorder := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
start := time.Now()
next.ServeHTTP(recorder, r)
fmt.Printf("%s %s %d %s\n", r.Method, r.URL.RequestURI(), recorder.statusCode, time.Since(start))
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if recovered := recover(); recovered != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
type statusRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *statusRecorder) WriteHeader(statusCode int) {
r.statusCode = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
func writeServerJSON(w http.ResponseWriter, statusCode int, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(value)
}
func writeServerJSONPretty(w http.ResponseWriter, statusCode int, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(statusCode)
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
_ = encoder.Encode(value)
return c.JSONPretty(200, resp, " ")
}
-135
View File
@@ -1,135 +0,0 @@
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"
)
func TestDASTServerTokenAuthRejectsMissingAndInvalidTokens(t *testing.T) {
server := &DASTServer{options: &Options{Token: "secret"}}
server.setupHandlers(false)
tests := []struct {
name string
target string
statusCode int
}{
{
name: "missing token",
target: "/stats",
statusCode: http.StatusBadRequest,
},
{
name: "invalid token",
target: "/stats?token=wrong",
statusCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, tt.target, nil)
response := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(response, request)
require.Equal(t, tt.statusCode, response.Code)
})
}
}
func TestDASTServerCORSPreflightBypassesTokenAuth(t *testing.T) {
server := &DASTServer{options: &Options{Token: "secret"}}
server.setupHandlers(false)
request := httptest.NewRequest(http.MethodOptions, "/stats", nil)
request.Header.Set("Origin", "https://example.com")
request.Header.Set("Access-Control-Request-Headers", "X-Test")
response := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(response, request)
require.Equal(t, http.StatusNoContent, response.Code)
require.Equal(t, "*", response.Header().Get("Access-Control-Allow-Origin"))
require.Equal(t, "GET,HEAD,PUT,PATCH,POST,DELETE", response.Header().Get("Access-Control-Allow-Methods"))
require.Equal(t, "X-Test", response.Header().Get("Access-Control-Allow-Headers"))
}
func TestStatsServerCloseDoesNotRequireFullExecutorOptions(t *testing.T) {
server, err := NewStatsServer(nil)
require.NoError(t, err)
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)
}
-13
View File
@@ -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">
+10 -53
View File
@@ -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
View File
@@ -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
+17 -52
View File
@@ -17,8 +17,8 @@ import (
)
var isCodeDisabled = func() bool { return osutils.IsWindows() && os.Getenv("CI") == "true" }
var signedIntegrationTemplates sync.Once
var signedIntegrationTemplatesErr error
var signedCodeTemplates sync.Once
var signedCodeTemplatesErr error
func hasAnyExecutable(names ...string) bool {
for _, name := range names {
@@ -34,7 +34,6 @@ var codeTestCases = []integrationCase{
{Path: "protocols/code/py-file.yaml", TestCase: &codeFile{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/py-env-var.yaml", TestCase: &codeEnvVar{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/unsigned.yaml", TestCase: &unsignedCode{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/dast-unsigned.yaml", TestCase: &dastUnsignedCode{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/py-nosig.yaml", TestCase: &codePyNoSig{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/py-interactsh.yaml", TestCase: &codeSnippet{}, DisableOn: isCodeDisabled},
{Path: "protocols/code/ps1-snippet.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsWindows() || isCodeDisabled() }},
@@ -51,20 +50,23 @@ const (
var testcertpath = ""
func ensureSignedIntegrationTemplates() error {
signedIntegrationTemplates.Do(func() {
func ensureSignedCodeTemplates() error {
if isCodeDisabled() {
return nil
}
signedCodeTemplates.Do(func() {
previousWD, err := os.Getwd()
if err != nil {
signedIntegrationTemplatesErr = err
signedCodeTemplatesErr = err
return
}
if err := os.Chdir(suite.fixturesDir); err != nil {
signedIntegrationTemplatesErr = err
signedCodeTemplatesErr = err
return
}
defer func() {
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedIntegrationTemplatesErr == nil {
signedIntegrationTemplatesErr = chdirErr
if chdirErr := os.Chdir(previousWD); chdirErr != nil && signedCodeTemplatesErr == nil {
signedCodeTemplatesErr = chdirErr
}
}()
@@ -76,16 +78,13 @@ func ensureSignedIntegrationTemplates() error {
tsigner, err := signer.NewTemplateSignerFromFiles(certPath, keyPath)
if err != nil {
signedIntegrationTemplatesErr = err
signedCodeTemplatesErr = err
return
}
var templatesToSign []string
if !isCodeDisabled() {
templatesToSign = append(templatesToSign,
"workflow/code-template-1.yaml",
"workflow/code-template-2.yaml",
)
templatesToSign := []string{
"workflow/code-template-1.yaml",
"workflow/code-template-2.yaml",
}
for _, v := range codeTestCases {
if v.DisableOn != nil && v.DisableOn() {
@@ -97,25 +96,16 @@ func ensureSignedIntegrationTemplates() error {
if _, ok := v.TestCase.(*codePyNoSig); ok {
continue
}
if _, ok := v.TestCase.(*dastUnsignedCode); ok {
continue
}
templatesToSign = append(templatesToSign, v.Path)
}
for _, v := range jsTestcases {
if v.DisableOn != nil && v.DisableOn() {
continue
}
templatesToSign = append(templatesToSign, v.Path)
}
for _, templatePath := range templatesToSign {
if err := templates.SignTemplate(tsigner, fixturePath(templatePath)); err != nil {
signedIntegrationTemplatesErr = err
signedCodeTemplatesErr = err
return
}
}
})
return signedIntegrationTemplatesErr
return signedCodeTemplatesErr
}
func getEnvValues() []string {
@@ -124,15 +114,6 @@ func getEnvValues() []string {
}
}
func runSignedNucleiTemplateAndGetResults(template, url string, debug bool, extra ...string) ([]string, error) {
if err := ensureSignedIntegrationTemplates(); err != nil {
return nil, err
}
args := []string{"-t", template, "-target", url}
args = append(args, extra...)
return testutils.RunNucleiBareArgsAndGetResults(debug, getEnvValues(), args...)
}
type codeSnippet struct{}
// Execute executes a test case and returns an error if occurred
@@ -197,22 +178,6 @@ func (h *unsignedCode) Execute(filePath string) error {
return errors.Join(expectResultsCount(results, 1), errors.New("unsigned template was executed"))
}
type dastUnsignedCode struct{}
// Execute runs an unsigned template that pairs a fuzzable request with a code
// block under -dast, which must not bypass the code-signing check.
func (h *dastUnsignedCode) Execute(filePath string) error {
results, err := testutils.RunNucleiArgsWithEnvAndGetResults(debug, getEnvValues(), "-t", filePath, "-u", "input", "-dast")
// should error out
if err != nil {
return nil
}
// this point should never be reached
return errors.Join(expectResultsCount(results, 1), errors.New("unsigned template was executed"))
}
type codePyNoSig struct{}
// Execute executes a test case and returns an error if occurred
@@ -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)
}
}
})
}
+11 -25
View File
@@ -9,26 +9,12 @@ import (
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
)
func expectPublicDNSResultsCount(results []string, expectedNumbers ...int) error {
return expectResultsCount(results, append([]int{0}, expectedNumbers...)...)
}
func TestDNS(t *testing.T) {
t.Run("A", func(t *testing.T) {
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/dns/a.yaml", "one.one.one.one", suite.debug)
if err != nil {
t.Fatalf("dns A request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
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)
}
@@ -39,7 +25,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns AAAA request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -49,7 +35,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns CNAME request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -59,7 +45,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns SRV request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -69,7 +55,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns NS request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -79,7 +65,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns TXT request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -89,7 +75,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns PTR request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -99,7 +85,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns CAA request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -119,7 +105,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns variables request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -129,7 +115,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns payload request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1, 2, 3); err != nil {
if err := expectResultsCount(results, 3); err != nil {
t.Fatal(err)
}
@@ -137,7 +123,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns payload request with CLI override failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1, 2, 3, 4); err != nil {
if err := expectResultsCount(results, 4); err != nil {
t.Fatal(err)
}
})
@@ -147,7 +133,7 @@ func TestDNS(t *testing.T) {
if err != nil {
t.Fatalf("dns DSL matcher variable request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -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
+16 -41
View File
@@ -6,26 +6,22 @@ package integration_test
import (
"context"
"fmt"
"net"
"log"
"time"
"github.com/ory/dockertest/v3"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
"github.com/testcontainers/testcontainers-go"
mongocontainer "github.com/testcontainers/testcontainers-go/modules/mongodb"
osutil "github.com/projectdiscovery/utils/os"
mongoclient "go.mongodb.org/mongo-driver/mongo"
mongooptions "go.mongodb.org/mongo-driver/mongo/options"
)
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"
dbPort = "27017/tcp"
mongoDatabaseReadyTimeout = 3 * time.Minute
mongoServerSelectionDelay = time.Second
dbName = "test"
dbImage = "mongo:8"
)
var exportersTestCases = []integrationCase{
@@ -40,43 +36,22 @@ func (m *mongoExporter) Execute(filepath string) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
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)
}
pool.MaxWait = mongoDatabaseReadyTimeout
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: dbRepository,
Tag: dbTag,
ExposedPorts: []string{dbPort},
})
// Start a MongoDB container
mongodbContainer, err := mongocontainer.Run(ctx, dbImage)
defer func() {
if err := testcontainers.TerminateContainer(mongodbContainer); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
return fmt.Errorf("failed to start container: %w", err)
}
defer purge(pool, resource)
mappedPort := resource.GetPort(dbPort)
if mappedPort == "" {
return fmt.Errorf("missing mapped port for %s", dbPort)
}
connString := fmt.Sprintf("mongodb://%s/%s", net.JoinHostPort("127.0.0.1", mappedPort), dbName)
err = pool.Retry(func() error {
clientOptions := mongooptions.Client().ApplyURI(connString).SetServerSelectionTimeout(mongoServerSelectionDelay)
client, err := mongoclient.Connect(ctx, clientOptions)
if err != nil {
return err
}
defer client.Disconnect(ctx)
return client.Ping(ctx, nil)
})
connString, err := mongodbContainer.ConnectionString(ctx)
if err != nil {
return fmt.Errorf("failed to wait for MongoDB container: %w", err)
return fmt.Errorf("failed to get connection string for MongoDB container: %s", err)
}
connString = connString + dbName
// Create a MongoDB exporter and write a test result to the database
opts := mongo.Options{
+2 -2
View File
@@ -20,7 +20,7 @@ func TestFlow(t *testing.T) {
if err != nil {
t.Fatalf("conditional flow request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1); err != nil {
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
@@ -77,7 +77,7 @@ func TestFlow(t *testing.T) {
if err != nil {
t.Fatalf("dns ns probe flow request failed: %v", err)
}
if err := expectPublicDNSResultsCount(results, 1, 2); err != nil {
if err := expectResultsCount(results, 2); err != nil {
t.Fatal(err)
}
})
-53
View File
@@ -5,13 +5,9 @@ package integration_test
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
@@ -40,55 +36,6 @@ 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{}},
}
// 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{}
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)
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)
}()
results, err := testutils.RunNucleiWithArgsAndGetResults(debug, "-t", filePath, "-l", targetPath, "-im", "http", "-dast")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
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{}},
+7 -120
View File
@@ -16,13 +16,13 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/julienschmidt/httprouter"
"gopkg.in/yaml.v2"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/projectdiscovery/utils/errkit"
logutil "github.com/projectdiscovery/utils/log"
@@ -44,7 +44,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{}},
@@ -77,7 +76,6 @@ 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{}},
@@ -97,8 +95,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{}},
}
type httpMultiVarSharing struct{}
@@ -834,7 +830,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 {
@@ -882,7 +878,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 {
@@ -906,29 +902,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()
@@ -1422,7 +1395,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 {
@@ -1459,28 +1432,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
@@ -1813,7 +1764,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 {
@@ -1838,7 +1789,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 {
@@ -1852,67 +1803,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
}
@@ -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)
}
@@ -19,6 +19,8 @@ import (
"github.com/jcmturner/gokrb5/v8/iana/nametype"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/types"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
)
// javascriptASRepRoast exercises templates/ad/asrep-roast.yaml end-to-end
@@ -44,7 +46,7 @@ func (j *javascriptASRepRoast) Execute(filePath string) error {
}
defer kdc.Close()
results, err := runSignedNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, kdc.Address(), debug)
if err != nil {
return err
}
@@ -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...)
}
+20 -86
View File
@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net"
"os"
"time"
"github.com/go-pg/pg/v10"
@@ -25,29 +24,15 @@ func javascriptDockerDisabled() bool {
return !osutils.IsLinux() || !hasAnyExecutable("docker", "podman")
}
func javascriptGoExecSambaDisabled() bool {
return javascriptDockerDisabled() || os.Getenv("RUN_GOEXEC_SAMBA_LOCAL") != "1"
}
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/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/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},
@@ -55,7 +40,7 @@ var jsTestcases = []integrationCase{
{Path: "protocols/javascript/wmi-command.yaml", TestCase: &javascriptWMICommand{}},
{Path: "protocols/javascript/goexec-redaction.yaml", TestCase: &javascriptGoExecRedaction{}},
{Path: "protocols/javascript/goexec-modules.yaml", TestCase: &javascriptGoExecModules{}},
{Path: "protocols/javascript/goexec-samba-ntlm.yaml", TestCase: &javascriptGoExecSambaNTLM{}, DisableOn: javascriptGoExecSambaDisabled, Serial: true},
{Path: "protocols/javascript/goexec-samba-ntlm.yaml", TestCase: &javascriptGoExecSambaNTLM{}, DisableOn: javascriptDockerDisabled, Serial: true},
}
var (
@@ -63,6 +48,7 @@ var (
)
const (
javascriptContainerTTLSeconds = 300
javascriptDatabaseReadyTimeout = 3 * time.Minute
javascriptServiceReadyTimeout = 45 * time.Second
javascriptRetryDelay = 500 * time.Millisecond
@@ -104,7 +90,7 @@ func newJavascriptDockerSpec(port string, options *dockertest.RunOptions, readyT
type javascriptNetHttps struct{}
func (j *javascriptNetHttps) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
@@ -121,16 +107,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 {
@@ -183,62 +159,11 @@ 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 {
// use scanme.sh as target to ensure we match on the 2nd default port 22
results, err := runSignedNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
if err != nil {
return err
}
@@ -248,7 +173,7 @@ func (j *javascriptMultiPortsSSH) Execute(filePath string) error {
type javascriptNoPortArgs struct{}
func (j *javascriptNoPortArgs) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "yo.dawg", debug)
if err != nil {
return err
}
@@ -263,7 +188,7 @@ func (j *javascriptWMICommand) Execute(filePath string) error {
// the exclude list and short-circuits before any dial, so the JSON result
// must contain "ok":false plus "network policy" in the error while not
// leaking the password.
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
@@ -275,7 +200,7 @@ type javascriptGoExecRedaction struct{}
func (j *javascriptGoExecRedaction) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
@@ -287,7 +212,7 @@ type javascriptGoExecModules struct{}
func (j *javascriptGoExecModules) Execute(filePath string) error {
listener := newGoExecCloseListener()
defer listener.Close()
results, err := runSignedNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, listener.host, debug, "-V", "RPCEndpoint="+listener.binding)
if err != nil {
return err
}
@@ -332,6 +257,10 @@ func (j *javascriptGoExecSambaNTLM) Execute(filePath string) error {
}
defer purge(pool, resource)
if err := resource.Expire(javascriptContainerTTLSeconds); err != nil {
return fmt.Errorf("could not expire samba: %w", err)
}
targetAddress := "127.0.0.1:445"
if err := waitForTCPService(targetAddress, javascriptServiceReadyTimeout); err != nil {
return err
@@ -346,7 +275,7 @@ func (j *javascriptGoExecSambaNTLM) Execute(filePath string) error {
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug)
if err == nil {
if countErr := expectResultsCount(results, 1); countErr == nil {
return nil
@@ -454,7 +383,7 @@ func (j *networkMultiStep) Execute(filePath string) error {
})
defer server.Close()
results, err := runSignedNucleiTemplateAndGetResults(filePath, server.URL, debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, server.URL, debug)
if err != nil {
return err
}
@@ -490,6 +419,10 @@ func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expecte
}
defer purge(pool, resource)
if err := resource.Expire(javascriptContainerTTLSeconds); err != nil {
return fmt.Errorf("could not expire resource for %s: %w", filePath, err)
}
mappedPort := resource.GetPort(spec.port)
if mappedPort == "" {
return fmt.Errorf("missing mapped port for %s", spec.port)
@@ -510,7 +443,7 @@ func runJavascriptDockerCase(filePath string, spec javascriptDockerSpec, expecte
errS := make([]error, 0, defaultRetry)
for attempt := 1; attempt <= defaultRetry; attempt++ {
results, err := runSignedNucleiTemplateAndGetResults(filePath, targetAddress, debug)
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, targetAddress, debug)
if err == nil {
if countErr := expectResultsCount(results, expectedNumbers...); countErr == nil {
return nil
@@ -653,6 +586,7 @@ func purge(pool *dockertest.Pool, resource *dockertest.Resource) {
return
}
containerName := resource.Container.Name
_ = pool.Client.StopContainer(resource.Container.ID, 0)
_ = pool.Purge(resource)
_ = pool.RemoveContainerByName(containerName)
}
@@ -1,74 +0,0 @@
//go:build integration
// +build integration
package integration_test
import (
"fmt"
"net"
"net/http"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
// startWebSocketEchoServer starts a WebSocket server on a random loopback port
// that accepts the "chat" subprotocol, greets, then echoes every message.
func startWebSocketEchoServer() (string, func(), error) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", nil, fmt.Errorf("websocket listen: %w", err)
}
srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upgrader := ws.HTTPUpgrader{Protocol: func(protocol string) bool { return protocol == "chat" }}
conn, _, _, err := upgrader.Upgrade(r, w)
if err != nil {
return
}
defer func() { _ = conn.Close() }()
if err := wsutil.WriteServerText(conn, []byte("welcome")); err != nil {
return
}
for {
payload, op, err := wsutil.ReadClientData(conn)
if err != nil {
return
}
if err := wsutil.WriteServerMessage(conn, op, payload); err != nil {
return
}
}
})}
go func() { _ = srv.Serve(lis) }()
return lis.Addr().String(), func() { _ = srv.Close() }, nil
}
// javascriptWebSocketEcho exercises the nuclei/websocket library end to end
// against a real WebSocket server.
type javascriptWebSocketEcho struct{}
func (j *javascriptWebSocketEcho) Execute(filePath string) error {
address, stop, err := startWebSocketEchoServer()
if err != nil {
return err
}
defer stop()
results, err := runSignedNucleiTemplateAndGetResults(filePath, address, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
// javascriptWebSocketDenied asserts the nuclei/websocket library refuses a host
// on the exclude list (network-policy enforcement) before connecting.
type javascriptWebSocketDenied struct{}
func (j *javascriptWebSocketDenied) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "127.0.0.1", debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
+1 -9
View File
@@ -9,7 +9,6 @@ import (
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"time"
@@ -93,14 +92,7 @@ func executeNucleiAsLibrary(templatePath, templateURL string) ([]string, error)
actualTemplatePath = fixturePath(actualTemplatePath)
}
defaultOpts.Templates = goflags.StringSlice{actualTemplatePath}
ignoreFile, err := config.ReadIgnoreFile()
if errors.Is(err, os.ErrNotExist) {
defaultOpts.Logger.Warning().Msgf("%s; continuing without ignore exclusions\n", err)
} else if err != nil {
return nil, err
} else {
defaultOpts.ExcludeTags = ignoreFile.Tags
}
defaultOpts.ExcludeTags = config.ReadIgnoreFile().Tags
outputWriter := testutils.NewMockOutputWriter(defaultOpts.OmitTemplate)
var results []string
@@ -79,7 +79,7 @@ type javascriptNoAccess struct{}
// Execute executes a test case and returns an error if occurred
func (h *javascriptNoAccess) Execute(filePath string) error {
results, err := runSignedNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
results, err := testutils.RunNucleiTemplateAndGetResults(filePath, "trust_me_bro.real", debug, "-ms", "-j")
if err != nil {
return err
}
+1 -1
View File
@@ -17,7 +17,7 @@ type multiProtoDynamicExtractor struct{}
// Execute executes a test case and returns an error if occurred
func (h *multiProtoDynamicExtractor) Execute(templatePath string) error {
results, err := testutils.RunNucleiTemplateAndGetResults(templatePath, "https://docs.projectdiscovery.io/", debug)
results, err := testutils.RunNucleiTemplateAndGetResults(templatePath, "docs.projectdiscovery.io", debug)
if err != nil {
return err
}
+3 -57
View File
@@ -6,6 +6,7 @@ package integration_test
import (
"fmt"
"net"
"strings"
"testing"
"time"
@@ -116,46 +117,6 @@ func TestNetwork(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
errState := &capturedError{}
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 5, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "FIRST" {
time.Sleep(integrationDurationObservationDelay)
_, _ = conn.Write([]byte("PING"))
}
data, err = reader.ConnReadNWithTimeout(conn, 6, 5*time.Second)
if err != nil {
errState.Set(err)
return
}
if string(data) == "SECOND" {
time.Sleep(integrationDurationObservationDelay)
_, _ = conn.Write([]byte("PONG"))
}
_, _ = conn.Write([]byte("NUCLEI"))
})
defer server.Close()
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/network/duration.yaml", server.URL, suite.debug)
if err != nil {
t.Fatalf("duration network request failed: %v", err)
}
if err := errState.Err(); err != nil {
t.Fatal(err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("SelfContained", func(t *testing.T) {
server := testutils.NewTCPServer(nil, 0, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
@@ -252,24 +213,9 @@ func TestNetwork(t *testing.T) {
t.Fatal(err)
}
// An explicitly specified reserved port (e.g. host:8081) must be preserved
// and not overridden by the template port (regression test for #7323).
// 8081 is a reserved port but is used here on an unprivileged port so the
// listener binds without root. The server is started on 8081 and the
// template (port 23846) must dial the operator-specified 8081 to match.
serverReserved := testutils.NewTCPServer(nil, 8081, func(conn net.Conn) {
defer func() { _ = conn.Close() }()
data, err := reader.ConnReadNWithTimeout(conn, 4, 5*time.Second)
if err == nil && string(data) == "PING" {
_, _ = conn.Write([]byte("PONG"))
}
})
defer serverReserved.Close()
results, err = testutils.RunNucleiTemplateAndGetResults("protocols/network/network-port.yaml", serverReserved.URL, suite.debug)
results, err = testutils.RunNucleiTemplateAndGetResults("protocols/network/network-port.yaml", strings.ReplaceAll(server.URL, "23846", "443"), suite.debug)
if err != nil {
t.Fatalf("network-port template failed with explicit reserved input port: %v", err)
t.Fatalf("network-port template failed with overridden input port: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
+6 -14
View File
@@ -4,7 +4,6 @@
package integration_test
import (
"errors"
"flag"
"fmt"
"os"
@@ -107,7 +106,7 @@ func integrationFamilies() []integrationFamily {
{Name: "headless", Cases: headlessTestcases},
{Name: "whois", Cases: whoisTestCases},
{Name: "library", Cases: libraryTestcases},
{Name: "template-path", Cases: templatesPathTestCases()},
{Name: "template-path", Cases: templatesPathTestCases},
{Name: "offline-http", Cases: offlineHttpTestcases},
{Name: "fuzz", Cases: fuzzingTestCases},
{Name: "generic", Cases: genericTestcases},
@@ -160,9 +159,9 @@ func executeIntegrationCase(testCase integrationCase) error {
if testCase.DisableOn != nil && testCase.DisableOn() {
return nil
}
if needsSignedIntegrationTemplates(testCase.Path) {
if err := ensureSignedIntegrationTemplates(); err != nil {
return fmt.Errorf("failed to sign integration templates: %w", err)
if needsSignedCodeTemplates(testCase.Path) {
if err := ensureSignedCodeTemplates(); err != nil {
return fmt.Errorf("failed to sign code templates: %w", err)
}
}
@@ -175,11 +174,6 @@ func executeIntegrationCase(testCase integrationCase) error {
if err == nil {
return nil
}
// a killed nuclei is not a flake; retrying it just spends the package
// timeout that every remaining case shares
if errors.Is(err, testutils.ErrCommandTimeout) {
return err
}
}
if retries > 1 {
return fmt.Errorf("test %s failed after %d attempts: %w", testCase.Path, retries, err)
@@ -197,10 +191,8 @@ func parallelism() int {
return parallelism
}
func needsSignedIntegrationTemplates(path string) bool {
return strings.HasPrefix(path, "protocols/code/") ||
strings.Contains(path, "workflow/code-") ||
strings.HasPrefix(path, "protocols/javascript/")
func needsSignedCodeTemplates(path string) bool {
return strings.HasPrefix(path, "protocols/code/") || strings.Contains(path, "workflow/code-")
}
func testNameForPath(path string) string {
-11
View File
@@ -23,17 +23,6 @@ func TestSSL(t *testing.T) {
}
})
t.Run("Duration", func(t *testing.T) {
server := newTLSServer(t, &tls.Config{})
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/ssl/duration.yaml", server.URL, suite.debug)
if err != nil {
t.Fatalf("duration ssl request failed: %v", err)
}
if err := expectResultsCount(results, 1); err != nil {
t.Fatal(err)
}
})
t.Run("BasicZTLS", func(t *testing.T) {
server := newTLSServer(t, &tls.Config{})
results, err := testutils.RunNucleiTemplateAndGetResults("protocols/ssl/basic-ztls.yaml", server.URL, suite.debug, "-ztls")
@@ -5,24 +5,25 @@ package integration_test
import (
"fmt"
"path/filepath"
"strings"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
)
func templatesPathTestCases() []integrationCase {
return []integrationCase{
//template folder path issue
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
//cwd
{Path: "./dns/detect-dangling-cname.yaml", TestCase: &cwdTemplateTest{}},
//relative path
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
//absolute path
{Path: filepath.Join(config.DefaultConfig.TemplatesDirectory, "dns", "dns-saas-service-detection.yaml"), TestCase: &absolutePathTemplateTest{}},
}
func getTemplatePath() string {
return config.DefaultConfig.TemplatesDirectory
}
var templatesPathTestCases = []integrationCase{
//template folder path issue
{Path: "protocols/http/get.yaml", TestCase: &folderPathTemplateTest{}},
//cwd
{Path: "./dns/detect-dangling-cname.yaml", TestCase: &cwdTemplateTest{}},
//relative path
{Path: "dns/dns-saas-service-detection.yaml", TestCase: &relativePathTemplateTest{}},
//absolute path
{Path: fmt.Sprintf("%v/dns/dns-saas-service-detection.yaml", getTemplatePath()), TestCase: &absolutePathTemplateTest{}},
}
type cwdTemplateTest struct{}
@@ -33,7 +34,7 @@ func (h *cwdTemplateTest) Execute(filePath string) error {
if err != nil {
return err
}
return expectPublicDNSResultsCount(results, 1)
return expectResultsCount(results, 1)
}
type relativePathTemplateTest struct{}
@@ -44,7 +45,7 @@ func (h *relativePathTemplateTest) Execute(filePath string) error {
if err != nil {
return err
}
return expectPublicDNSResultsCount(results, 1)
return expectResultsCount(results, 1)
}
type absolutePathTemplateTest struct{}
@@ -55,7 +56,7 @@ func (h *absolutePathTemplateTest) Execute(filePath string) error {
if err != nil {
return err
}
return expectPublicDNSResultsCount(results, 1)
return expectResultsCount(results, 1)
}
type folderPathTemplateTest struct{}
@@ -1,34 +0,0 @@
id: raw-http-input-body-fuzz
info:
name: fuzzing json body seeded from a raw http request file
author: pdteam
severity: info
description: |
This template fuzzes the json body of a base request seeded from a raw HTTP
request file (-im http), covering the shortest path from a single request to
a DAST scan.
http:
- pre-condition:
- type: dsl
dsl:
- method == "POST"
- contains(content_type, "application/json")
condition: and
payloads:
injection:
- "'"
fuzzing:
- part: body
type: postfix
mode: single
fuzz:
- '{{injection}}'
matchers:
- type: word
words:
- "unrecognized token:"
@@ -1,33 +0,0 @@
id: dast-unsigned-code
info:
name: dast-unsigned-code
author: pdteam
severity: info
tags: dast,code
description: |
unsigned code paired with a fuzzable request must not execute under -dast
http:
- method: GET
path:
- "{{BaseURL}}/?x=1"
fuzzing:
- part: query
type: replace
mode: single
keys: ["x"]
fuzz:
- "test"
code:
- engine:
- py
- python3
source: |
print("unsigned code")
matchers:
- type: word
words:
- "unsigned code"
@@ -1,23 +0,0 @@
id: dns-duration
info:
name: DNS Duration
author: pdteam
severity: info
flow: dns("duration_dns")
dns:
- id: duration_dns
name: "{{FQDN}}"
type: A
class: inet
recursion: true
retries: 3
matchers:
- type: dsl
condition: and
dsl:
- "duration > 0 && duration < 60"
- "duration_dns_duration > 0 && duration_dns_duration < 60"
- "duration == duration_dns_duration"
Loaded 100 of 671 files, more files were not shown because too many files have changed in this diff. Show more