mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 17:39:09 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeedf6cce6 |
No files matched your search
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(make:*)",
|
||||
"Bash(go:*)",
|
||||
"Bash(golangci-lint:*)",
|
||||
"Bash(git merge:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git fetch:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"WebFetch(*)",
|
||||
"Write(*)",
|
||||
"WebSearch(*)",
|
||||
"MultiEdit(*)",
|
||||
"Edit(*)",
|
||||
"Bash(gh:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(./nuclei:*)",
|
||||
"WebFetch(domain:github.com)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(make run:*)",
|
||||
"Bash(./bin/nuclei:*)"
|
||||
],
|
||||
"defaultMode": "acceptEdits"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
|
||||
- name: 🐛 Report a Bug (Start with Discussion)
|
||||
url: https://github.com/orgs/projectdiscovery/discussions/new?category=q-a
|
||||
about: Start by reporting your issue in discussions for proper triage. Issues will be created after review to avoid duplicate/invalid reports.
|
||||
|
||||
- name: 💡 Request a Feature (Start with Discussion)
|
||||
url: https://github.com/orgs/projectdiscovery/discussions/new?category=ideas
|
||||
about: Share your feature idea in discussions first. This helps validate and refine the request before creating an issue.
|
||||
|
||||
File renamed without changes.
@@ -2,7 +2,6 @@ addReviewers: true
|
||||
reviewers:
|
||||
- dogancanbakir
|
||||
- dwisiswant0
|
||||
- mzack9999
|
||||
|
||||
numberOfReviewers: 1
|
||||
skipKeywords:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.DEPENDABOT_PAT }}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go/compat-checks@v1
|
||||
with:
|
||||
release-test: true
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
if: "${{ !endsWith(github.actor, '[bot]') }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/setup/git@v1
|
||||
- run: make syntax-docs
|
||||
|
||||
@@ -28,10 +28,9 @@ jobs:
|
||||
LIST_FILE: "/tmp/targets-${{ matrix.targets }}.txt"
|
||||
PROFILE_MEM: "/tmp/nuclei-profile-${{ matrix.targets }}-targets"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/git@v1
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- name: Generate list
|
||||
run: for i in {1..${{ matrix.targets }}}; do echo "https://honey.scanme.sh/?_=${i}" >> "${LIST_FILE}"; done
|
||||
# NOTE(dwisiswant0): use `-no-mhe` flag to get better samples.
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
files: "${PGO_FILE}"
|
||||
message: "build: update PGO profile :robot:"
|
||||
- run: git push origin $GITHUB_REF
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "pgo"
|
||||
path: "${{ env.PGO_FILE }}"
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
env:
|
||||
OUTPUT: "/tmp/results.sarif"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- run: govulncheck -scan package -format sarif ./... > $OUTPUT
|
||||
- uses: github/codeql-action/upload-sarif@v4
|
||||
- uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: "${{ env.OUTPUT }}"
|
||||
category: "govulncheck"
|
||||
@@ -11,10 +11,8 @@ jobs:
|
||||
env:
|
||||
BENCH_OUT: "/tmp/bench.out"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- run: make build-test
|
||||
- run: ./bin/nuclei.test -test.run - -test.bench=. -test.benchmem ./cmd/nuclei/ | tee $BENCH_OUT
|
||||
env:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
LIST_FILE: "/tmp/targets-${{ matrix.count }}.txt"
|
||||
PROFILE_MEM: "/tmp/nuclei-perf-test-${{ matrix.count }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- run: make verify
|
||||
- name: Generate list
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest-16-cores
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-stale: 90
|
||||
days-before-close: 7
|
||||
@@ -37,4 +37,5 @@ jobs:
|
||||
it, please comment or feel free to reopen it.
|
||||
close-issue-label: "Status: Abandoned"
|
||||
close-pr-label: "Status: Abandoned"
|
||||
exempt-issue-labels: "Type: Enhancement"
|
||||
exempt-issue-labels: "Status: Abandoned"
|
||||
exempt-pr-labels: "Status: Abandoned"
|
||||
@@ -22,9 +22,8 @@ jobs:
|
||||
if: "${{ !endsWith(github.actor, '[bot]') }}"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- uses: projectdiscovery/actions/golangci-lint/v2@v1
|
||||
|
||||
tests:
|
||||
@@ -36,19 +35,8 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- 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
|
||||
@@ -64,10 +52,8 @@ jobs:
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- name: "Simple"
|
||||
run: go run .
|
||||
working-directory: examples/simple/
|
||||
@@ -88,11 +74,9 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- uses: projectdiscovery/actions/setup/python@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- run: bash run.sh "${{ matrix.os }}"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -109,11 +93,9 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/nuclei@v1
|
||||
- uses: projectdiscovery/actions/setup/python@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- run: bash run.sh
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -124,9 +106,8 @@ jobs:
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/cache/go-rod-browser@v1
|
||||
- run: make template-validate
|
||||
|
||||
codeql:
|
||||
@@ -138,19 +119,19 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: github/codeql-action/init@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: 'go'
|
||||
- uses: github/codeql-action/autobuild@v4
|
||||
- uses: github/codeql-action/analyze@v4
|
||||
- uses: github/codeql-action/autobuild@v3
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
|
||||
release:
|
||||
name: "Release test"
|
||||
needs: ["tests"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: projectdiscovery/actions/setup/go@v1
|
||||
- uses: projectdiscovery/actions/goreleaser@v1
|
||||
|
||||
@@ -162,7 +143,7 @@ jobs:
|
||||
TARGET_URL: "http://scanme.sh/a/?b=c"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- run: make build
|
||||
- name: "Setup environment (push)"
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
.gitignore
|
||||
.idea
|
||||
.vscode
|
||||
.claude/settings.local.json
|
||||
|
||||
# Binaries
|
||||
/bin/*
|
||||
/bindgen
|
||||
|
||||
@@ -40,8 +40,6 @@ Nuclei was built with some environment variables in mind to help with debugging.
|
||||
| `SHOW_DSL_ERRORS=true` | Enables Printing DSL Errors (that are hidden by default) |
|
||||
| `HIDE_TEMPLATE_SIG_WARNING=true` | Hides Template Signature Verification Warnings |
|
||||
| `NUCLEI_LOG_ALL=true` | Log All Events that were skipped in verbose mode |
|
||||
| `NUCLEI_CONFIG_DIR` | Sets custom configuration directory path |
|
||||
| `NUCLEI_TEMPLATES_DIR` | Sets custom nuclei templates directory path |
|
||||
|
||||
|
||||
|
||||
@@ -147,6 +147,8 @@ template-validate: build
|
||||
template-validate:
|
||||
./bin/nuclei -ut
|
||||
./bin/nuclei -validate \
|
||||
-et .github/ \
|
||||
-et helpers/payloads/ \
|
||||
-et http/technologies \
|
||||
-t dns \
|
||||
-t ssl \
|
||||
@@ -155,5 +157,7 @@ template-validate:
|
||||
-ept code
|
||||
./bin/nuclei -validate \
|
||||
-w workflows \
|
||||
-et .github/ \
|
||||
-et helpers/payloads/ \
|
||||
-et http/technologies \
|
||||
-ept code
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
<p align="center">
|
||||
|
||||
<img src="https://img.shields.io/badge/go-1.22-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white">
|
||||
|
||||
<a href="https://docs.projectdiscovery.io/tools/nuclei/overview?utm_source=github&utm_medium=web&utm_campaign=nuclei_readme"><img src="https://img.shields.io/badge/Documentation-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1ib29rLW9wZW4iPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjwvc3ZnPg==&logoColor=white"></a>
|
||||
|
||||
<a href="https://github.com/projectdiscovery/nuclei-templates"><img src="https://img.shields.io/badge/Templates Library-%23000000.svg?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNoaWVsZCI+PHBhdGggZD0iTTIwIDEzYzAgNS0zLjUgNy41LTcuNjYgOC45NWExIDEgMCAwIDEtLjY3LS4wMUM3LjUgMjAuNSA0IDE4IDQgMTNWNmExIDEgMCAwIDEgMS0xYzIgMCA0LjUtMS4yIDYuMjQtMi43MmExLjE3IDEuMTcgMCAwIDEgMS41MiAwQzE0LjUxIDMuODEgMTcgNSAxOSA1YTEgMSAwIDAgMSAxIDF6Ii8+PC9zdmc+&logoColor=white"></a>
|
||||
@@ -109,7 +111,7 @@ Browse the full Nuclei [**`documentation here`**](https://docs.projectdiscovery.
|
||||
|
||||
### Installation
|
||||
|
||||
`nuclei` requires **go >= 1.24.1** to install successfully. Run the following command to get the repo:
|
||||
`nuclei` requires **go1.23** to install successfully. Run the following command to get the repo:
|
||||
|
||||
```sh
|
||||
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
|
||||
@@ -140,7 +142,7 @@ 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)
|
||||
-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)
|
||||
-resume string resume scan using resume.cfg (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)
|
||||
|
||||
@@ -161,7 +163,7 @@ TEMPLATES:
|
||||
-validate validate the passed templates to nuclei
|
||||
-nss, -no-strict-syntax disable strict syntax check on templates
|
||||
-td, -template-display displays the templates content
|
||||
-tl list all templates matching current filters
|
||||
-tl list all available templates
|
||||
-tgl list all available tags
|
||||
-sign signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable
|
||||
-code enable loading code protocol-based templates
|
||||
@@ -284,7 +286,6 @@ RATE-LIMIT:
|
||||
-jsc, -js-concurrency int maximum number of javascript runtimes to be executed in parallel (default 120)
|
||||
-pc, -payload-concurrency int max payload concurrency for each template (default 25)
|
||||
-prc, -probe-concurrency int http probe concurrency with httpx (default 50)
|
||||
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int time to wait in seconds before timeout (default 10)
|
||||
@@ -355,7 +356,6 @@ CLOUD:
|
||||
AUTHENTICATION:
|
||||
-sf, -secret-file string[] path to config file containing secrets for nuclei authenticated scan
|
||||
-ps, -prefetch-secrets prefetch secrets from the secrets file
|
||||
# NOTE: Headers in secrets files preserve exact casing (useful for case-sensitive APIs)
|
||||
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
+1
-2
@@ -119,7 +119,7 @@ Nuclei是一款注重于可配置性、可扩展性和易用性的基于模板
|
||||
目标:
|
||||
-u, -target string[] 指定扫描的目标URL/主机(多个目标则指定多个-u参数)
|
||||
-l, -list string 指定包含要扫描的目标URL/主机列表的文件路径(一行一个)
|
||||
-resume string 从指定文件恢复扫描并保存到指定文件(将禁用请求聚类)
|
||||
-resume string 使用指定的resume.cfg文件恢复扫描(将禁用请求聚类)
|
||||
-sa, -scan-all-ips 扫描由目标解析出来的所有IP(针对域名对应多个IP的情况)
|
||||
-iv, -ip-version string[] 要扫描的主机名的IP版本(4,6)-(默认为4)
|
||||
|
||||
@@ -238,7 +238,6 @@ UNCOVER引擎:
|
||||
-c, -concurrency int 并行执行的最大模板数量(默认:25)
|
||||
-hbs, -headless-bulk-size int 每个模板并行运行的无头主机最大数量(默认:10)
|
||||
-headc, -headless-concurrency int 并行指定无头主机最大数量(默认:10)
|
||||
-tlc, -template-loading-concurrency int 最大并发模板加载操作数(默认:50)
|
||||
|
||||
|
||||
优化:
|
||||
|
||||
+1
-2
@@ -118,7 +118,7 @@ TARGET:
|
||||
-u, -target string[] URLs/hosts a escanear
|
||||
-l, -list string ruta al archivo que contiene la lista de URLs/hosts a escanear (uno por línea)
|
||||
-eh, -exclude-hosts string[] hosts a excluir para escanear de la lista de entrada (ip, cidr, hostname)
|
||||
-resume string reanudar el escaneo desde y guardar en el archivo especificado (la clusterización quedará inhabilitada)
|
||||
-resume string reanudar el escaneo usando resume.cfg (la clusterización quedará inhabilitada)
|
||||
-sa, -scan-all-ips escanear todas las IP asociadas al registro dns
|
||||
-iv, -ip-version string[] versión IP a escanear del nombre de host (4,6) - (por defecto 4)
|
||||
|
||||
@@ -244,7 +244,6 @@ RATE-LIMIT:
|
||||
-headc, -headless-concurrency int número máximo de plantillas headless a ejecutar en paralelo (por defecto 10)
|
||||
-jsc, -js-concurrency int número máximo de entornos de ejecución de JavaScript a ejecutar en paralelo (por defecto 120)
|
||||
-pc, -payload-concurrency int concurrencia máxima de carga útil para cada plantilla (por defecto 25)
|
||||
-tlc, -template-loading-concurrency int número máximo de operaciones de carga de plantillas concurrentes (por defecto 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int tiempo de espera en segundos (por defecto 10)
|
||||
|
||||
+1
-2
@@ -98,7 +98,7 @@ 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)
|
||||
-resume string resume scan from and save to specified file (clustering will be disabled)
|
||||
-resume string resume scan using resume.cfg (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)
|
||||
|
||||
@@ -211,7 +211,6 @@ RATE-LIMIT:
|
||||
-c, -concurrency int maximum number of templates to be executed in parallel (default 25)
|
||||
-hbs, -headless-bulk-size int maximum number of headless hosts to be analyzed in parallel per template (default 10)
|
||||
-headc, -headless-concurrency int maximum number of headless templates to be executed in parallel (default 10)
|
||||
-tlc, -template-loading-concurrency int maximum number of concurrent template loading operations (default 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int time to wait in seconds before timeout (default 10)
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ Nucleiは、広範な設定可能性、大規模な拡張性、および使い
|
||||
ターゲット:
|
||||
-u, -target string[] スキャンする対象のURL/ホスト
|
||||
-l, -list string スキャンする対象のURL/ホストのリストが含まれているファイルへのパス(1行に1つ)
|
||||
-resume string 指定されたファイルからスキャンを再開し、指定されたファイルに保存(クラスタリングは無効になります)
|
||||
-resume string resume.cfgを使用してスキャンを再開(クラスタリングは無効になります)
|
||||
-sa, -scan-all-ips DNSレコードに関連付けられているすべてのIPをスキャン
|
||||
-iv, -ip-version string[] ホスト名のスキャンするIPバージョン(4,6)-(デフォルトは4)
|
||||
|
||||
|
||||
+1
-2
@@ -96,7 +96,7 @@ Nuclei는 빠르고, 템플릿 기반의 취약점 스캐너로
|
||||
TARGET:
|
||||
-u, -target string[] 스캔할 대상 URL/호스트
|
||||
-l, -list string 스캔할 대상 URL/호스트 목록이 있는 파일 경로 (한 줄에 하나씩)
|
||||
-resume string 지정된 파일에서 스캔을 재개하고 지정된 파일에 저장 (클러스터링은 비활성화됨)
|
||||
-resume string resume.cfg를 사용하여 스캔 재개 (클러스터링은 비활성화됨)
|
||||
-sa, -scan-all-ips dns 레코드와 관련된 모든 IP 스캔
|
||||
-iv, -ip-version string[] 스캔할 호스트의 IP 버전 (4,6) - (기본값 4)
|
||||
|
||||
@@ -209,7 +209,6 @@ RATE-LIMIT:
|
||||
-c, -concurrency int 병렬로 실행할 최대 템플릿 수 (기본값 25)
|
||||
-hbs, -headless-bulk-size int 템플릿당 병렬로 분석할 최대 headless 호스트 수 (기본값 10)
|
||||
-headc, -headless-concurrency int 병렬로 실행할 최대 headless 템플릿 수 (기본값 10)
|
||||
-tlc, -template-loading-concurrency int 최대 동시 템플릿 로딩 작업 수 (기본값 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int 타임아웃 전에 기다릴 초 수 (기본값 10)
|
||||
|
||||
+1
-2
@@ -118,7 +118,7 @@ TARGET:
|
||||
-u, -target string[] URLs/hosts a serem escaneados
|
||||
-l, -list string caminho do arquivo contendo a lista de URLs/hosts a serem escaneados (um por linha)
|
||||
-eh, -exclude-hosts string[] hosts a serem excluídos do escaneamento na lista de entrada (ip, cidr, hostname)
|
||||
-resume string retomar o escaneamento a partir de e salvar no arquivo especificado (a clusterização será desabilitada)
|
||||
-resume string retomar o escaneamento usando resume.cfg (a clusterização será desabilitada)
|
||||
-sa, -scan-all-ips escanear todos os IPs associados ao registro DNS
|
||||
-iv, -ip-version string[] versão de IP a escanear do nome do host (4,6) - (padrão 4)
|
||||
|
||||
@@ -244,7 +244,6 @@ RATE-LIMIT:
|
||||
-headc, -headless-concurrency int número máximo de templates headless a serem executados em paralelo (padrão 10)
|
||||
-jsc, -js-concurrency int número máximo de ambientes de execução de JavaScript a serem executados em paralelo (padrão 120)
|
||||
-pc, -payload-concurrency int concorrência máxima de payload para cada template (padrão 25)
|
||||
-tlc, -template-loading-concurrency int número máximo de operações de carregamento de templates concorrentes (padrão 50)
|
||||
|
||||
OPTIMIZATIONS:
|
||||
-timeout int tempo limite em segundos (padrão 10)
|
||||
|
||||
+1
-1
@@ -3218,7 +3218,7 @@ Inputs contains inputs for the network socket
|
||||
<div class="dt">
|
||||
|
||||
description: |
|
||||
Port is the port to send network requests to. this acts as default port but is overridden if target/input contains
|
||||
Port is the port to send network requests to. this acts as default port but is overriden if target/input contains
|
||||
non-http(s) ports like 80,8080,8081 etc
|
||||
|
||||
</div>
|
||||
|
||||
@@ -36,24 +36,24 @@
|
||||
{{binary}} -tags cve,dos,fuzz
|
||||
{{binary}} -tags cve -include-tags dos,fuzz
|
||||
{{binary}} -tags cve -exclude-tags cve2020
|
||||
{{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/
|
||||
{{binary}} -t http/cves/
|
||||
{{binary}} -t http/cves/ -t http/exposures/
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config,ssrf
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical -author geeknik,pdteam
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -exclude-templates http/cves/2021/
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -exclude-templates http/cves/2017/CVE-2017-7269.yaml
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -include-templates http/cves/2017/CVE-2017-7269.yaml
|
||||
{{binary}} -tags cve -exclude-templates cves/2020/
|
||||
{{binary}} -tags cve -exclude-templates cves/2020/CVE-2020-9757.yaml
|
||||
{{binary}} -tags cve -exclude-templates cves/2020/CVE-2020-9757.yaml -exclude-templates cves/2021/
|
||||
{{binary}} -t cves/
|
||||
{{binary}} -t cves/ -t exposures/
|
||||
{{binary}} -t cves/ -t exposures/ -tags config
|
||||
{{binary}} -t cves/ -t exposures/ -tags config,ssrf
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical -author geeknik,pdteam
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -exclude-templates cves/2021/
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -exclude-templates cves/2017/CVE-2017-7269.yaml
|
||||
{{binary}} -t cves/ -t exposures/ -tags config -severity high,critical -author geeknik,pdteam -etags sqli -include-templates cves/2017/CVE-2017-7269.yaml
|
||||
|
||||
# Advanced Filtering
|
||||
{{binary}} -tags cve -author geeknik,pdteam -tc severity=='high'
|
||||
{{binary}} -tc contains(authors,'pdteam')
|
||||
{{binary}} -t http/cves/ -t http/exposures/ -tc contains(tags,'cve') -exclude-templates http/cves/2020/CVE-2020-9757.yaml
|
||||
{{binary}} -t cves/ -t exposures/ -tc contains(tags,'cve') -exclude-templates cves/2020/CVE-2020-9757.yaml
|
||||
{{binary}} -tc protocol=='dns'
|
||||
{{binary}} -tc contains(http_method,'GET')
|
||||
{{binary}} -tc len(body)>0
|
||||
@@ -65,7 +65,7 @@
|
||||
{{binary}} -w workflows
|
||||
{{binary}} -w workflows -author geeknik,pdteam
|
||||
{{binary}} -w workflows -severity high,critical
|
||||
{{binary}} -w workflows -author geeknik,pdteam -severity high,critical
|
||||
{{binary}} -w workflows -author geeknik,pdteam -severity high,critical
|
||||
|
||||
# Input Types
|
||||
# http protocol
|
||||
|
||||
@@ -24,8 +24,6 @@ var codeTestCases = []TestCaseInfo{
|
||||
{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() }},
|
||||
{Path: "protocols/code/pre-condition.yaml", TestCase: &codePreCondition{}, DisableOn: isCodeDisabled},
|
||||
{Path: "protocols/code/sh-virtual.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsLinux() || isCodeDisabled() }},
|
||||
{Path: "protocols/code/py-virtual.yaml", TestCase: &codeSnippet{}, DisableOn: func() bool { return !osutils.IsLinux() || isCodeDisabled() }},
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/kitabisa/go-ci"
|
||||
@@ -25,7 +24,7 @@ type TestCaseInfo struct {
|
||||
}
|
||||
|
||||
var (
|
||||
debug = isDebugMode()
|
||||
debug = os.Getenv("DEBUG") == "true"
|
||||
customTests = os.Getenv("TESTS")
|
||||
protocol = os.Getenv("PROTO")
|
||||
|
||||
@@ -47,7 +46,6 @@ var (
|
||||
"library": libraryTestcases,
|
||||
"templatesPath": templatesPathTestCases,
|
||||
"templatesDir": templatesDirTestCases,
|
||||
"env_vars": templatesDirEnvTestCases,
|
||||
"file": fileTestcases,
|
||||
"offlineHttp": offlineHttpTestcases,
|
||||
"customConfigDir": customConfigDirTestCases,
|
||||
@@ -61,7 +59,6 @@ var (
|
||||
"matcher-status": matcherStatusTestcases,
|
||||
"exporters": exportersTestCases,
|
||||
}
|
||||
|
||||
// flakyTests are run with a retry count of 3
|
||||
flakyTests = map[string]bool{
|
||||
"protocols/http/self-contained-file-input.yaml": true,
|
||||
@@ -92,12 +89,11 @@ func main() {
|
||||
}
|
||||
|
||||
// start fuzz playground server
|
||||
defer fuzzplayground.Cleanup()
|
||||
server := fuzzplayground.GetPlaygroundServer()
|
||||
defer func() {
|
||||
fuzzplayground.Cleanup()
|
||||
_ = server.Close()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := server.Start("localhost:8082"); err != nil {
|
||||
if !strings.Contains(err.Error(), "Server closed") {
|
||||
@@ -107,6 +103,7 @@ func main() {
|
||||
}()
|
||||
|
||||
customTestsList := normalizeSplit(customTests)
|
||||
|
||||
failedTestTemplatePaths := runTests(customTestsList)
|
||||
|
||||
if len(failedTestTemplatePaths) > 0 {
|
||||
@@ -133,27 +130,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// isDebugMode checks if debug mode is enabled via any of the supported debug
|
||||
// environment variables.
|
||||
func isDebugMode() bool {
|
||||
debugEnvVars := []string{
|
||||
"DEBUG",
|
||||
"ACTIONS_RUNNER_DEBUG", // GitHub Actions runner debug
|
||||
// Add more debug environment variables here as needed
|
||||
}
|
||||
|
||||
truthyValues := []string{"true", "1", "yes", "on", "enabled"}
|
||||
|
||||
for _, envVar := range debugEnvVars {
|
||||
envValue := strings.ToLower(strings.TrimSpace(os.Getenv(envVar)))
|
||||
if slices.Contains(truthyValues, envValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// execute a testcase with retry and consider best of N
|
||||
// intended for flaky tests like interactsh
|
||||
func executeWithRetry(testCase testutils.TestCase, templatePath string, retryCount int) (string, error) {
|
||||
|
||||
@@ -15,18 +15,13 @@ var jsTestcases = []TestCaseInfo{
|
||||
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
|
||||
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
|
||||
{Path: "protocols/javascript/oracle-auth-test.yaml", TestCase: &javascriptOracleAuthTest{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
|
||||
{Path: "protocols/javascript/vnc-pass-brute.yaml", TestCase: &javascriptVncPassBrute{}},
|
||||
{Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}},
|
||||
}
|
||||
|
||||
var (
|
||||
redisResource *dockertest.Resource
|
||||
sshResource *dockertest.Resource
|
||||
oracleResource *dockertest.Resource
|
||||
vncResource *dockertest.Resource
|
||||
pool *dockertest.Pool
|
||||
defaultRetry = 3
|
||||
redisResource *dockertest.Resource
|
||||
sshResource *dockertest.Resource
|
||||
pool *dockertest.Pool
|
||||
defaultRetry = 3
|
||||
)
|
||||
|
||||
type javascriptNetHttps struct{}
|
||||
@@ -103,82 +98,6 @@ func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
|
||||
type javascriptOracleAuthTest struct{}
|
||||
|
||||
func (j *javascriptOracleAuthTest) Execute(filePath string) error {
|
||||
if oracleResource == nil || pool == nil {
|
||||
// skip test as oracle is not running
|
||||
return nil
|
||||
}
|
||||
tempPort := oracleResource.GetPort("1521/tcp")
|
||||
finalURL := "localhost:" + tempPort
|
||||
defer purge(oracleResource)
|
||||
|
||||
errs := []error{}
|
||||
for i := 0; i < defaultRetry; i++ {
|
||||
results := []string{}
|
||||
var err error
|
||||
_ = pool.Retry(func() error {
|
||||
//let ssh server start
|
||||
time.Sleep(3 * time.Second)
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return multierr.Combine(errs...)
|
||||
}
|
||||
|
||||
type javascriptVncPassBrute struct{}
|
||||
|
||||
func (j *javascriptVncPassBrute) Execute(filePath string) error {
|
||||
if vncResource == nil || pool == nil {
|
||||
// skip test as vnc is not running
|
||||
return nil
|
||||
}
|
||||
tempPort := vncResource.GetPort("5900/tcp")
|
||||
finalURL := "localhost:" + tempPort
|
||||
defer purge(vncResource)
|
||||
errs := []error{}
|
||||
for i := 0; i < defaultRetry; i++ {
|
||||
results := []string{}
|
||||
var err error
|
||||
_ = pool.Retry(func() error {
|
||||
//let ssh server start
|
||||
time.Sleep(3 * time.Second)
|
||||
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectResultsCount(results, 1); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
errs = append(errs, 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 := testutils.RunNucleiTemplateAndGetResults(filePath, "scanme.sh", debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// purge any given resource if it is not nil
|
||||
func purge(resource *dockertest.Resource) {
|
||||
if resource != nil && pool != nil {
|
||||
@@ -244,41 +163,4 @@ func init() {
|
||||
if err := sshResource.Expire(30); err != nil {
|
||||
log.Printf("Could not expire resource: %s", err)
|
||||
}
|
||||
|
||||
// setup a temporary oracle instance
|
||||
oracleResource, err = pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "gvenzl/oracle-xe",
|
||||
Tag: "latest",
|
||||
Env: []string{
|
||||
"ORACLE_PASSWORD=mysecret",
|
||||
},
|
||||
Platform: "linux/amd64",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Could not start Oracle resource: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// by default expire after 30 sec
|
||||
if err := oracleResource.Expire(30); err != nil {
|
||||
log.Printf("Could not expire Oracle resource: %s", err)
|
||||
}
|
||||
|
||||
// setup a temporary vnc server
|
||||
vncResource, err = pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "dorowu/ubuntu-desktop-lxde-vnc",
|
||||
Tag: "latest",
|
||||
Env: []string{
|
||||
"VNC_PASSWORD=mysecret",
|
||||
},
|
||||
Platform: "linux/amd64",
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Could not start resource: %s", err)
|
||||
return
|
||||
}
|
||||
// by default expire after 30 sec
|
||||
if err := vncResource.Expire(30); err != nil {
|
||||
log.Printf("Could not expire resource: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
@@ -71,7 +70,6 @@ func executeNucleiAsLibrary(templatePath, templateURL string) ([]string, error)
|
||||
|
||||
defaultOpts := types.DefaultOptions()
|
||||
defaultOpts.ExecutionId = "test"
|
||||
defaultOpts.Logger = gologger.DefaultLogger
|
||||
|
||||
mockProgress := &testutils.MockProgressClient{}
|
||||
reportingClient, err := reporting.New(&reporting.Options{ExecutionId: defaultOpts.ExecutionId}, "", false)
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
osutils "github.com/projectdiscovery/utils/os"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
)
|
||||
|
||||
// isNotLinux returns true if not running on Linux (used to skip tests on non-Linux OS)
|
||||
var isNotLinux = func() bool { return !osutils.IsLinux() }
|
||||
|
||||
var templatesDirEnvTestCases = []TestCaseInfo{
|
||||
{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvBasicTest{}, DisableOn: isNotLinux},
|
||||
{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvAbsolutePathTest{}, DisableOn: isNotLinux},
|
||||
{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvRelativePathTest{}, DisableOn: isNotLinux},
|
||||
{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvPrecedenceTest{}, DisableOn: isNotLinux},
|
||||
{Path: "protocols/dns/cname-fingerprint.yaml", TestCase: &templatesDirEnvCustomTemplatesTest{}, DisableOn: isNotLinux},
|
||||
}
|
||||
|
||||
// copyTemplateToDir copies a template file to a destination directory, preserving the directory structure
|
||||
func copyTemplateToDir(templatePath, destDir string) error {
|
||||
// Read the template file
|
||||
templateData, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to read template file")
|
||||
}
|
||||
|
||||
// Create the destination path preserving directory structure
|
||||
destPath := filepath.Join(destDir, templatePath)
|
||||
destDirPath := filepath.Dir(destPath)
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
if err := os.MkdirAll(destDirPath, 0755); err != nil {
|
||||
return errkit.Wrap(err, "failed to create destination directory")
|
||||
}
|
||||
|
||||
// Write the template file
|
||||
if err := os.WriteFile(destPath, templateData, 0644); err != nil {
|
||||
return errkit.Wrap(err, "failed to write template file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// templatesDirEnvBasicTest tests basic functionality of NUCLEI_TEMPLATES_DIR
|
||||
type templatesDirEnvBasicTest struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *templatesDirEnvBasicTest) Execute(filePath string) error {
|
||||
tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-env-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempdir)
|
||||
}()
|
||||
|
||||
// Copy template to temp directory
|
||||
if err := copyTemplateToDir(filePath, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set NUCLEI_TEMPLATES_DIR and run nuclei
|
||||
envVars := []string{"NUCLEI_TEMPLATES_DIR=" + tempdir}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// templatesDirEnvAbsolutePathTest tests that absolute paths work correctly
|
||||
type templatesDirEnvAbsolutePathTest struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *templatesDirEnvAbsolutePathTest) Execute(filePath string) error {
|
||||
tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-env-abs-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempdir)
|
||||
}()
|
||||
|
||||
// Get absolute path
|
||||
absTempDir, err := filepath.Abs(tempdir)
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to get absolute path")
|
||||
}
|
||||
|
||||
// Copy template to temp directory
|
||||
if err := copyTemplateToDir(filePath, absTempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set NUCLEI_TEMPLATES_DIR with absolute path and run nuclei
|
||||
envVars := []string{"NUCLEI_TEMPLATES_DIR=" + absTempDir}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// templatesDirEnvRelativePathTest tests that relative paths are resolved correctly
|
||||
type templatesDirEnvRelativePathTest struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *templatesDirEnvRelativePathTest) Execute(filePath string) error {
|
||||
// Create temp directory in current working directory
|
||||
tempdir, err := os.MkdirTemp(".", "nuclei-templates-dir-env-rel-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempdir)
|
||||
}()
|
||||
|
||||
// Get relative path (just the directory name)
|
||||
relPath := filepath.Base(tempdir)
|
||||
|
||||
// Copy template to temp directory
|
||||
if err := copyTemplateToDir(filePath, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set NUCLEI_TEMPLATES_DIR with relative path and run nuclei
|
||||
// Note: The implementation should convert relative paths to absolute
|
||||
envVars := []string{"NUCLEI_TEMPLATES_DIR=" + relPath}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// templatesDirEnvPrecedenceTest tests that -ud flag takes precedence over NUCLEI_TEMPLATES_DIR
|
||||
type templatesDirEnvPrecedenceTest struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *templatesDirEnvPrecedenceTest) Execute(filePath string) error {
|
||||
// Create two temp directories
|
||||
envTempDir, err := os.MkdirTemp("", "nuclei-templates-dir-env-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create env temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(envTempDir)
|
||||
}()
|
||||
|
||||
flagTempDir, err := os.MkdirTemp("", "nuclei-templates-dir-flag-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create flag temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(flagTempDir)
|
||||
}()
|
||||
|
||||
// Copy template to flag temp directory (this should be used due to precedence)
|
||||
if err := copyTemplateToDir(filePath, flagTempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set NUCLEI_TEMPLATES_DIR to envTempDir (should be ignored due to -ud flag)
|
||||
envVars := []string{"NUCLEI_TEMPLATES_DIR=" + envTempDir}
|
||||
// Use -ud flag which should take precedence
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com", "-ud", flagTempDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
|
||||
// templatesDirEnvCustomTemplatesTest tests that custom template subdirectories are correctly set
|
||||
type templatesDirEnvCustomTemplatesTest struct{}
|
||||
|
||||
// Execute executes a test case and returns an error if occurred
|
||||
func (h *templatesDirEnvCustomTemplatesTest) Execute(filePath string) error {
|
||||
tempdir, err := os.MkdirTemp("", "nuclei-templates-dir-custom-*")
|
||||
if err != nil {
|
||||
return errkit.Wrap(err, "failed to create temp dir")
|
||||
}
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempdir)
|
||||
}()
|
||||
|
||||
// Create custom template subdirectories structure
|
||||
customDirs := []string{"github", "s3", "gitlab", "azure"}
|
||||
for _, dir := range customDirs {
|
||||
customDirPath := filepath.Join(tempdir, dir)
|
||||
if err := os.MkdirAll(customDirPath, 0755); err != nil {
|
||||
return errkit.Wrap(err, "failed to create custom template directory")
|
||||
}
|
||||
}
|
||||
|
||||
// Copy template to temp directory
|
||||
if err := copyTemplateToDir(filePath, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set NUCLEI_TEMPLATES_DIR and run nuclei
|
||||
envVars := []string{"NUCLEI_TEMPLATES_DIR=" + tempdir}
|
||||
results, err := testutils.RunNucleiBareArgsAndGetResults(debug, envVars, "-t", filePath, "-u", "8x8exch02.8x8.com")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return expectResultsCount(results, 1)
|
||||
}
|
||||
+5
-53
@@ -20,7 +20,6 @@ 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"
|
||||
@@ -194,11 +193,8 @@ func main() {
|
||||
})
|
||||
}
|
||||
|
||||
// Setup filename for graceful exits
|
||||
// Setup graceful exits
|
||||
resumeFileName := types.DefaultResumeFilePath()
|
||||
if options.Resume == "" {
|
||||
resumeFileName = options.Resume
|
||||
}
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
@@ -258,7 +254,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringSliceVarP(&options.Targets, "target", "u", nil, "target URLs/hosts to scan", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVarP(&options.TargetsFilePath, "list", "l", "", "path to file containing a list of target URLs/hosts to scan (one per line)"),
|
||||
flagSet.StringSliceVarP(&options.ExcludeTargets, "exclude-hosts", "eh", nil, "hosts to exclude to scan from the input list (ip, cidr, hostname)", goflags.FileCommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan from and save to specified file (clustering will be disabled)"),
|
||||
flagSet.StringVar(&options.Resume, "resume", "", "resume scan using resume.cfg (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),
|
||||
)
|
||||
@@ -267,8 +263,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.StringVarP(&options.InputFileMode, "input-mode", "im", "list", fmt.Sprintf("mode of input file (%v)", provider.SupportedInputFormats())),
|
||||
flagSet.BoolVarP(&options.FormatUseRequiredOnly, "required-only", "ro", false, "use only required fields in input format when generating requests"),
|
||||
flagSet.BoolVarP(&options.SkipFormatValidation, "skip-format-validation", "sfv", false, "skip format validation (like missing vars) when parsing input file"),
|
||||
flagSet.BoolVarP(&options.VarsTextTemplating, "vars-text-templating", "vtt", false, "enable text templating for vars in input file (only for yaml input mode)"),
|
||||
flagSet.StringSliceVarP(&options.VarsFilePaths, "var-file-paths", "vfp", nil, "list of yaml file contained vars to inject into yaml input", goflags.CommaSeparatedStringSliceOptions),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("templates", "Templates",
|
||||
@@ -283,7 +277,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.BoolVar(&options.Validate, "validate", false, "validate the passed templates to nuclei"),
|
||||
flagSet.BoolVarP(&options.NoStrictSyntax, "no-strict-syntax", "nss", false, "disable strict syntax check on templates"),
|
||||
flagSet.BoolVarP(&options.TemplateDisplay, "template-display", "td", false, "displays the templates content"),
|
||||
flagSet.BoolVar(&options.TemplateList, "tl", false, "list all templates matching current filters"),
|
||||
flagSet.BoolVar(&options.TemplateList, "tl", false, "list all available templates"),
|
||||
flagSet.BoolVar(&options.TagList, "tgl", false, "list all available tags"),
|
||||
flagSet.StringSliceVarConfigOnly(&options.RemoteTemplateDomainList, "remote-template-domain", []string{"cloud.projectdiscovery.io"}, "allowed domain list to load remote templates from"),
|
||||
flagSet.BoolVar(&options.SignTemplates, "sign", false, "signs the templates with the private key defined in NUCLEI_SIGNATURE_PRIVATE_KEY env variable"),
|
||||
@@ -414,7 +408,6 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.IntVarP(&options.JsConcurrency, "js-concurrency", "jsc", 120, "maximum number of javascript runtimes to be executed in parallel"),
|
||||
flagSet.IntVarP(&options.PayloadConcurrency, "payload-concurrency", "pc", 25, "max payload concurrency for each template"),
|
||||
flagSet.IntVarP(&options.ProbeConcurrency, "probe-concurrency", "prc", 50, "http probe concurrency with httpx"),
|
||||
flagSet.IntVarP(&options.TemplateLoadingConcurrency, "template-loading-concurrency", "tlc", types.DefaultTemplateLoadingConcurrency, "maximum number of concurrent template loading operations"),
|
||||
)
|
||||
flagSet.CreateGroup("optimization", "Optimizations",
|
||||
flagSet.IntVar(&options.Timeout, "timeout", 10, "time to wait in seconds before timeout"),
|
||||
@@ -578,7 +571,6 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
|
||||
config.DefaultConfig.SetConfigDir(customConfigDir)
|
||||
readFlagsConfig(flagSet)
|
||||
}
|
||||
|
||||
if cfgFile != "" {
|
||||
if !fileutil.FileExists(cfgFile) {
|
||||
options.Logger.Fatal().Msgf("given config file '%s' does not exist", cfgFile)
|
||||
@@ -587,49 +579,9 @@ Additional documentation is available at: https://docs.nuclei.sh/getting-started
|
||||
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)
|
||||
if options.NewTemplatesDirectory != "" {
|
||||
config.DefaultConfig.SetTemplatesDir(options.NewTemplatesDirectory)
|
||||
}
|
||||
|
||||
defaultProfilesPath := filepath.Join(config.DefaultConfig.GetTemplateDir(), "profiles")
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -52,31 +48,6 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if _, err := os.Stat(basePath); os.IsNotExist(err) {
|
||||
return basePath
|
||||
}
|
||||
|
||||
lastDot := strings.LastIndex(basePath, ".")
|
||||
var name, ext string
|
||||
if lastDot != -1 {
|
||||
name = basePath[:lastDot]
|
||||
ext = basePath[lastDot:]
|
||||
} else {
|
||||
name = basePath
|
||||
ext = ""
|
||||
}
|
||||
|
||||
for i := 1; ; i++ {
|
||||
newPath := fmt.Sprintf("%s.%d%s", name, i, ext)
|
||||
if _, err := os.Stat(newPath); os.IsNotExist(err) {
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultOptions() *types.Options {
|
||||
return &types.Options{
|
||||
RemoteTemplateDomainList: []string{"cloud.projectdiscovery.io"},
|
||||
@@ -135,55 +106,24 @@ func runEnumBenchmark(b *testing.B, options *types.Options) {
|
||||
}
|
||||
defer nucleiRunner.Close()
|
||||
|
||||
benchNameSlug := strings.ReplaceAll(b.Name(), "/", "-")
|
||||
|
||||
// Start CPU profiling
|
||||
cpuProfileBase := fmt.Sprintf("%s.cpu.prof", benchNameSlug)
|
||||
cpuProfilePath := getUniqFilename(cpuProfileBase)
|
||||
cpuProfile, err := os.Create(cpuProfilePath)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create CPU profile: %s", err)
|
||||
}
|
||||
defer func() { _ = cpuProfile.Close() }()
|
||||
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
b.Fatalf("failed to start CPU profile: %s", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := nucleiRunner.RunEnumeration(); err != nil {
|
||||
b.Fatalf("%s failed: %s", b.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
// Write heap profile
|
||||
heapProfileBase := fmt.Sprintf("%s.heap.prof", benchNameSlug)
|
||||
heapProfilePath := getUniqFilename(heapProfileBase)
|
||||
heapProfile, err := os.Create(heapProfilePath)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create heap profile: %s", err)
|
||||
}
|
||||
defer func() { _ = heapProfile.Close() }()
|
||||
|
||||
runtime.GC() // Force GC before heap profile
|
||||
if err := pprof.WriteHeapProfile(heapProfile); err != nil {
|
||||
b.Fatalf("failed to write heap profile: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRunEnumeration(b *testing.B) {
|
||||
// Default case: run enumeration with default options == all nuclei-templates
|
||||
b.Run("Default", func(b *testing.B) {
|
||||
options := getDefaultOptions()
|
||||
options.Targets = []string{targetURL}
|
||||
// b.Run("Default", func(b *testing.B) {
|
||||
// options := getDefaultOptions()
|
||||
// options.Targets = []string{targetURL}
|
||||
|
||||
runEnumBenchmark(b, options)
|
||||
})
|
||||
// runEnumBenchmark(b, options)
|
||||
// })
|
||||
|
||||
// Case: https://github.com/projectdiscovery/nuclei/pull/6258
|
||||
b.Run("Multiproto", func(b *testing.B) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
module github.com/projectdiscovery/nuclei/v3
|
||||
|
||||
go 1.24.2
|
||||
|
||||
toolchain go1.24.4
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible
|
||||
github.com/andygrunwald/go-jira v1.16.1
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/andygrunwald/go-jira v1.16.0
|
||||
github.com/antchfx/htmlquery v1.3.4
|
||||
github.com/bluele/gcache v0.0.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-rod/rod v0.116.2
|
||||
@@ -18,16 +16,16 @@ require (
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/miekg/dns v1.1.68
|
||||
github.com/miekg/dns v1.1.66
|
||||
github.com/olekukonko/tablewriter v1.0.8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectdiscovery/clistats v0.1.1
|
||||
github.com/projectdiscovery/fastdialer v0.4.18
|
||||
github.com/projectdiscovery/hmap v0.0.97
|
||||
github.com/projectdiscovery/fastdialer v0.4.7
|
||||
github.com/projectdiscovery/hmap v0.0.93
|
||||
github.com/projectdiscovery/interactsh v1.2.4
|
||||
github.com/projectdiscovery/rawhttp v0.1.90
|
||||
github.com/projectdiscovery/retryabledns v1.0.110
|
||||
github.com/projectdiscovery/retryablehttp-go v1.0.133
|
||||
github.com/projectdiscovery/retryabledns v1.0.106
|
||||
github.com/projectdiscovery/retryablehttp-go v1.0.121
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/xid v1.6.0
|
||||
@@ -39,26 +37,24 @@ require (
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
github.com/weppos/publicsuffix-go v0.50.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/text v0.31.0
|
||||
golang.org/x/text v0.28.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
carvel.dev/ytt v0.52.0
|
||||
code.gitea.io/sdk/gitea v0.17.0
|
||||
code.gitea.io/sdk/gitea v0.21.0
|
||||
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/azure-sdk-for-go/sdk/storage/azblob v1.6.1
|
||||
github.com/DataDog/gostackparse v0.7.0
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057
|
||||
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697
|
||||
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883
|
||||
github.com/alexsnet/go-vnc v0.1.0
|
||||
github.com/alitto/pond v1.9.2
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/antchfx/xpath v1.3.5
|
||||
github.com/antchfx/xpath v1.3.4
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17
|
||||
@@ -75,7 +71,7 @@ require (
|
||||
github.com/getkin/kin-openapi v0.132.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-pg/pg/v10 v10.15.0
|
||||
github.com/go-pg/pg v8.0.7+incompatible
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -87,36 +83,34 @@ require (
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
|
||||
github.com/lib/pq v1.10.9
|
||||
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/mholt/archives v0.1.3
|
||||
github.com/microsoft/go-mssqldb v1.9.2
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/praetorian-inc/fingerprintx v1.1.15
|
||||
github.com/projectdiscovery/dsl v0.8.6
|
||||
github.com/projectdiscovery/dsl v0.5.1
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2
|
||||
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c
|
||||
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb
|
||||
github.com/projectdiscovery/goflags v0.1.74
|
||||
github.com/projectdiscovery/gologger v1.1.62
|
||||
github.com/projectdiscovery/gologger v1.1.54
|
||||
github.com/projectdiscovery/gostruct v0.0.2
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81
|
||||
github.com/projectdiscovery/httpx v1.7.2
|
||||
github.com/projectdiscovery/mapcidr v1.1.97
|
||||
github.com/projectdiscovery/gozero v0.1.0
|
||||
github.com/projectdiscovery/httpx v1.7.1-0.20250901093029-3909667cbaa6
|
||||
github.com/projectdiscovery/mapcidr v1.1.34
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
|
||||
github.com/projectdiscovery/networkpolicy v0.1.30
|
||||
github.com/projectdiscovery/networkpolicy v0.1.21
|
||||
github.com/projectdiscovery/ratelimit v0.0.82
|
||||
github.com/projectdiscovery/rdap v0.9.0
|
||||
github.com/projectdiscovery/sarif v0.0.1
|
||||
github.com/projectdiscovery/tlsx v1.2.2
|
||||
github.com/projectdiscovery/uncover v1.2.0
|
||||
github.com/projectdiscovery/useragent v0.0.104
|
||||
github.com/projectdiscovery/utils v0.7.3
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.57
|
||||
github.com/projectdiscovery/tlsx v1.2.1
|
||||
github.com/projectdiscovery/uncover v1.1.0
|
||||
github.com/projectdiscovery/useragent v0.0.101
|
||||
github.com/projectdiscovery/utils v0.4.24-0.20250823123502-bd7f2849ddb4
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.43
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/seh-msft/burpxml v1.0.1
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/stretchr/testify v1.11.0
|
||||
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
|
||||
github.com/testcontainers/testcontainers-go v0.38.0
|
||||
github.com/testcontainers/testcontainers-go/modules/mongodb v0.37.0
|
||||
@@ -124,7 +118,7 @@ require (
|
||||
github.com/zmap/zgrab2 v0.1.8
|
||||
gitlab.com/gitlab-org/api/client-go v0.130.1
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/term v0.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
moul.io/http2curl v1.0.0
|
||||
)
|
||||
@@ -134,25 +128,25 @@ require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
|
||||
github.com/42wim/httpsig v1.2.2 // 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/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // 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.10.3 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/akrylysov/pogreb v0.10.2 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0 // indirect
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
@@ -174,15 +168,12 @@ require (
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/brianvoe/gofakeit/v7 v7.2.1 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/caddyserver/certmagic v0.19.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/censys/censys-sdk-go v0.19.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
@@ -194,14 +185,14 @@ require (
|
||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect
|
||||
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // 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/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.5.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@@ -213,13 +204,12 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 // 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.26.0 // indirect
|
||||
github.com/gaissmai/bart v0.24.0 // indirect
|
||||
github.com/geoffgarside/ber v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
@@ -232,7 +222,6 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // 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
|
||||
@@ -248,16 +237,15 @@ require (
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // 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/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-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.7.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/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
@@ -267,10 +255,9 @@ require (
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
|
||||
github.com/kataras/jwt v0.1.10 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
@@ -278,7 +265,7 @@ require (
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
|
||||
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect
|
||||
github.com/mackerelio/go-osstat v0.2.4 // indirect
|
||||
@@ -290,7 +277,7 @@ require (
|
||||
github.com/mholt/acmez v1.2.0 // 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/minlz v1.0.0 // indirect
|
||||
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
@@ -307,25 +294,25 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // 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.2.8 // indirect
|
||||
github.com/opencontainers/runc v1.2.3 // 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.22 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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.1 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.2.12 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.1.33 // indirect
|
||||
github.com/projectdiscovery/freeport v0.0.7 // indirect
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
|
||||
github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect
|
||||
@@ -337,29 +324,22 @@ require (
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
github.com/tidwall/buntdb v1.3.1 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/grect v0.1.4 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/rtred v0.1.2 // indirect
|
||||
github.com/tidwall/tinyqueue v0.1.1 // indirect
|
||||
github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
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/ulikunitz/xz v0.5.12 // 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/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
|
||||
@@ -372,18 +352,18 @@ require (
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||
github.com/ysmood/got v0.40.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
github.com/zcalusic/sysinfo v1.0.2 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
)
|
||||
@@ -407,12 +387,12 @@ require (
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
goftp.io/server/v2 v2.0.1 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.38.0
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.35.0
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
@@ -426,3 +406,10 @@ require (
|
||||
|
||||
// https://go.dev/ref/mod#go-mod-file-retract
|
||||
retract v3.2.0 // retract due to broken js protocol issue
|
||||
|
||||
// Fix genproto version conflicts
|
||||
replace (
|
||||
google.golang.org/genproto => google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142
|
||||
google.golang.org/genproto/googleapis/api => google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142
|
||||
google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
id: fuzz-body
|
||||
|
||||
info:
|
||||
name: fuzzing error sqli payloads in http req body
|
||||
author: pdteam
|
||||
severity: info
|
||||
description: |
|
||||
This template attempts to find SQL injection vulnerabilities by fuzzing http body
|
||||
It automatically handles and parses json,xml,multipart form and x-www-form-urlencoded data
|
||||
and performs fuzzing on the value of every key
|
||||
|
||||
http:
|
||||
- pre-condition:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- method != "GET"
|
||||
- method != "HEAD"
|
||||
condition: and
|
||||
|
||||
payloads:
|
||||
injection:
|
||||
- "'"
|
||||
- "\""
|
||||
- ";"
|
||||
|
||||
fuzzing:
|
||||
- part: body
|
||||
type: postfix
|
||||
mode: single
|
||||
fuzz:
|
||||
- '{{injection}}'
|
||||
|
||||
stop-at-first-match: true
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "unrecognized token:"
|
||||
- "null"
|
||||
@@ -1,27 +0,0 @@
|
||||
id: signed-python-code-in-virtual-env
|
||||
|
||||
info:
|
||||
name: signed-python-code-in-virtual-env
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: code
|
||||
description: |
|
||||
signed python code in virtual environment
|
||||
|
||||
code:
|
||||
- engine:
|
||||
- sh
|
||||
- bash
|
||||
sandbox:
|
||||
working-dir: /tmp
|
||||
image: python:3.14
|
||||
source: |
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
print("hello from python virtual code")
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "hello from python virtual code"
|
||||
@@ -1,25 +0,0 @@
|
||||
id: signed-code-in-virtual-env
|
||||
|
||||
info:
|
||||
name: signed-code-in-virtual-env
|
||||
author: pdteam
|
||||
severity: info
|
||||
tags: code
|
||||
description: |
|
||||
signed code in virtual environment
|
||||
|
||||
code:
|
||||
- engine:
|
||||
- sh
|
||||
- bash
|
||||
sandbox:
|
||||
working-dir: /tmp
|
||||
image: ubuntu:latest
|
||||
source: |
|
||||
echo "hello from sh virtual code"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "hello from sh virtual code"
|
||||
# digest: 4a0a00473045022100a2a71c423b72e600ac2d78209482b48591924157d49e2dfb7767445b0073e92b022009a6365e84247e268c7ffcb2f9ed424529dada6f087d5f21400ab0b2a822ca56:4a3eb6b4988d95847d4203be25ed1d46
|
||||
@@ -1,28 +0,0 @@
|
||||
id: multi-ports
|
||||
|
||||
info:
|
||||
name: Multi Ports - Detection
|
||||
author: pdteam
|
||||
severity: info
|
||||
description: |
|
||||
Multi Ports template for testing
|
||||
metadata:
|
||||
max-request: 1
|
||||
tags: js,detect,multi-ports,enum,network
|
||||
|
||||
javascript:
|
||||
- pre-condition: |
|
||||
isPortOpen(Host,Port);
|
||||
code: |
|
||||
var m = require("nuclei/ssh");
|
||||
var c = m.SSHClient();
|
||||
var response = c.ConnectSSHInfoMode(Host, Port);
|
||||
Export(response);
|
||||
args:
|
||||
Host: "{{Host}}"
|
||||
Port: "2222,22" # Port 22 should match
|
||||
|
||||
extractors:
|
||||
- type: json
|
||||
json:
|
||||
- '.UserAuth'
|
||||
@@ -1,38 +0,0 @@
|
||||
id: vnc-password-test
|
||||
|
||||
info:
|
||||
name: VNC Password Authentication Test
|
||||
author: pdteam
|
||||
severity: high
|
||||
description: |
|
||||
Tests VNC authentication with correct and incorrect passwords.
|
||||
metadata:
|
||||
shodan-query: product:"vnc"
|
||||
tags: js,network,vnc,authentication
|
||||
|
||||
javascript:
|
||||
- pre-condition: |
|
||||
isPortOpen(Host,Port)
|
||||
|
||||
code: |
|
||||
let vnc = require('nuclei/vnc');
|
||||
let client = new vnc.VNCClient();
|
||||
client.Connect(Host, Port, Password);
|
||||
|
||||
args:
|
||||
Host: "{{Host}}"
|
||||
Port: "5900"
|
||||
Password: "{{passwords}}"
|
||||
payloads:
|
||||
passwords:
|
||||
- ""
|
||||
- root
|
||||
- password
|
||||
- admin
|
||||
- mysecret
|
||||
stop-at-first-match: true
|
||||
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- "success == true"
|
||||
@@ -104,7 +104,7 @@ func NewUploadWriter(ctx context.Context, logger *gologger.Logger, creds *pdcpau
|
||||
// SetScanID sets the scan id for the upload writer
|
||||
func (u *UploadWriter) SetScanID(id string) error {
|
||||
if !xidRegex.MatchString(id) {
|
||||
gologger.Warning().Msgf("invalid asset id provided (unknown xid format): %s", id)
|
||||
return fmt.Errorf("invalid scan id provided")
|
||||
}
|
||||
u.scanID = id
|
||||
return nil
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider/authx"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
@@ -69,7 +68,7 @@ func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret
|
||||
return func(d *authx.Dynamic) error {
|
||||
tmpls := opts.TemplateStore.LoadTemplates([]string{d.TemplatePath})
|
||||
if len(tmpls) == 0 {
|
||||
return fmt.Errorf("%w for path: %s", disk.ErrNoTemplatesFound, d.TemplatePath)
|
||||
return fmt.Errorf("no templates found for path: %s", d.TemplatePath)
|
||||
}
|
||||
if len(tmpls) > 1 {
|
||||
return fmt.Errorf("multiple templates found for path: %s", d.TemplatePath)
|
||||
|
||||
+12
-22
@@ -254,23 +254,8 @@ func New(options *types.Options) (*Runner, error) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create temporary directory")
|
||||
}
|
||||
runner.tmpDir = tmpDir
|
||||
|
||||
// Cleanup tmpDir only if initialization fails
|
||||
// On successful initialization, Close() method will handle cleanup
|
||||
cleanupOnError := true
|
||||
defer func() {
|
||||
if cleanupOnError && runner.tmpDir != "" {
|
||||
_ = os.RemoveAll(runner.tmpDir)
|
||||
}
|
||||
}()
|
||||
|
||||
// create the input provider and load the inputs
|
||||
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options, TempDir: runner.tmpDir})
|
||||
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create input provider")
|
||||
}
|
||||
@@ -399,10 +384,16 @@ func New(options *types.Options) (*Runner, error) {
|
||||
if options.RateLimit > 0 && options.RateLimitDuration == 0 {
|
||||
options.RateLimitDuration = time.Second
|
||||
}
|
||||
runner.rateLimiter = utils.GetRateLimiter(context.Background(), options.RateLimit, options.RateLimitDuration)
|
||||
if options.RateLimit == 0 && options.RateLimitDuration == 0 {
|
||||
runner.rateLimiter = ratelimit.NewUnlimited(context.Background())
|
||||
} else {
|
||||
runner.rateLimiter = ratelimit.New(context.Background(), uint(options.RateLimit), options.RateLimitDuration)
|
||||
}
|
||||
|
||||
if tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*"); err == nil {
|
||||
runner.tmpDir = tmpDir
|
||||
}
|
||||
|
||||
// Initialization successful, disable cleanup on error
|
||||
cleanupOnError = false
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
@@ -585,9 +576,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
}
|
||||
|
||||
if len(r.options.SecretsFile) > 0 && !r.options.Validate {
|
||||
// Clone options so GetAuthTmplStore can modify them without affecting the original
|
||||
authOptions := r.options.Copy()
|
||||
authTmplStore, err := GetAuthTmplStore(authOptions, r.catalog, executorOpts)
|
||||
authTmplStore, err := GetAuthTmplStore(r.options, r.catalog, executorOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load dynamic auth templates")
|
||||
}
|
||||
@@ -670,6 +659,7 @@ func (r *Runner) RunEnumeration() error {
|
||||
}
|
||||
store.Load()
|
||||
// TODO: remove below functions after v3 or update warning messages
|
||||
disk.PrintDeprecatedPathsMsgIfApplicable(r.options.Silent)
|
||||
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
|
||||
|
||||
// add the hosts from the metadata queries of loaded templates into input provider
|
||||
|
||||
@@ -44,13 +44,7 @@ func (r *Runner) listAvailableStoreTemplates(store *loader.Store) {
|
||||
config.DefaultConfig.TemplateVersion,
|
||||
config.DefaultConfig.TemplatesDirectory,
|
||||
)
|
||||
// order templates alphabetically by path
|
||||
templates := store.Templates()
|
||||
sort.Slice(templates, func(i, j int) bool {
|
||||
return templates[i].Path < templates[j].Path
|
||||
})
|
||||
|
||||
for _, tpl := range templates {
|
||||
for _, tpl := range store.Templates() {
|
||||
if hasExtraFlags(r.options) {
|
||||
if r.options.TemplateDisplay {
|
||||
colorize := !r.options.NoColor
|
||||
|
||||
+2
-18
@@ -3,12 +3,11 @@ package nuclei
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/goflags"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
|
||||
@@ -182,7 +181,7 @@ func WithGlobalRateLimitCtx(ctx context.Context, maxTokens int, duration time.Du
|
||||
return func(e *NucleiEngine) error {
|
||||
e.opts.RateLimit = maxTokens
|
||||
e.opts.RateLimitDuration = duration
|
||||
e.rateLimiter = utils.GetRateLimiter(ctx, e.opts.RateLimit, e.opts.RateLimitDuration)
|
||||
e.rateLimiter = ratelimit.New(ctx, uint(e.opts.RateLimit), e.opts.RateLimitDuration)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -560,18 +559,3 @@ func WithOptions(opts *pkgtypes.Options) NucleiSDKOptions {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTemporaryDirectory allows setting a parent directory for SDK-managed temporary files.
|
||||
// A temporary directory will be created inside the provided directory and cleaned up on engine close.
|
||||
// If not set, a temporary directory will be automatically created in the system temp location.
|
||||
// The parent directory is assumed to exist.
|
||||
func WithTemporaryDirectory(parentDir string) NucleiSDKOptions {
|
||||
return func(e *NucleiEngine) error {
|
||||
tmpDir, err := os.MkdirTemp(parentDir, "nuclei-tmp-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.tmpDir = tmpDir
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//go:build !race
|
||||
// +build !race
|
||||
|
||||
package nuclei_test
|
||||
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
@@ -53,7 +53,11 @@ func createEphemeralObjects(ctx context.Context, base *NucleiEngine, opts *types
|
||||
if opts.RateLimit > 0 && opts.RateLimitDuration == 0 {
|
||||
opts.RateLimitDuration = time.Second
|
||||
}
|
||||
u.executerOpts.RateLimiter = utils.GetRateLimiter(ctx, opts.RateLimit, opts.RateLimitDuration)
|
||||
if opts.RateLimit == 0 && opts.RateLimitDuration == 0 {
|
||||
u.executerOpts.RateLimiter = ratelimit.NewUnlimited(ctx)
|
||||
} else {
|
||||
u.executerOpts.RateLimiter = ratelimit.New(ctx, uint(opts.RateLimit), opts.RateLimitDuration)
|
||||
}
|
||||
u.engine = core.New(opts)
|
||||
u.engine.SetExecuterOptions(u.executerOpts)
|
||||
return u, nil
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
@@ -93,9 +92,6 @@ type NucleiEngine struct {
|
||||
|
||||
// Logger instance for the engine
|
||||
Logger *gologger.Logger
|
||||
|
||||
// Temporary directory for SDK-managed template files
|
||||
tmpDir string
|
||||
}
|
||||
|
||||
// LoadAllTemplates loads all nuclei template based on given options
|
||||
@@ -235,9 +231,6 @@ func (e *NucleiEngine) closeInternal() {
|
||||
if e.httpxClient != nil {
|
||||
_ = e.httpxClient.Close()
|
||||
}
|
||||
if e.tmpDir != "" {
|
||||
_ = os.RemoveAll(e.tmpDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Close all resources used by nuclei engine
|
||||
|
||||
+34
-23
@@ -38,6 +38,21 @@ import (
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
)
|
||||
|
||||
// enginePool provides shared parsed-cache across engines (opt-in)
|
||||
var (
|
||||
sharedParsedOnce sync.Once
|
||||
sharedParsed *templates.Cache
|
||||
)
|
||||
|
||||
func getSharedParser() *templates.Parser {
|
||||
// Initialize the shared parsed cache once
|
||||
sharedParsedOnce.Do(func() {
|
||||
sharedParsed = templates.NewCache()
|
||||
})
|
||||
// Return a fresh Parser each call that reuses only the shared parsed cache,
|
||||
return templates.NewParserWithParsedCache(sharedParsed)
|
||||
}
|
||||
|
||||
// applyRequiredDefaults to options
|
||||
func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
|
||||
mockoutput := testutils.NewMockOutputWriter(e.opts.OmitTemplate)
|
||||
@@ -124,7 +139,12 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if e.parser == nil {
|
||||
e.parser = templates.NewParser()
|
||||
//TODO: remove this feature flag after testing
|
||||
if os.Getenv("NUCLEI_USE_SHARED_COMPILED") == "1" {
|
||||
e.parser = templates.NewSharedParserWithCompiledCache()
|
||||
} else {
|
||||
e.parser = templates.NewParser()
|
||||
}
|
||||
}
|
||||
|
||||
if protocolstate.ShouldInit(e.opts.ExecutionId) {
|
||||
@@ -171,29 +191,20 @@ func (e *NucleiEngine) init(ctx context.Context) error {
|
||||
e.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
}
|
||||
|
||||
if e.tmpDir == "" {
|
||||
tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.tmpDir = tmpDir
|
||||
}
|
||||
|
||||
e.executerOpts = &protocols.ExecutorOptions{
|
||||
Output: e.customWriter,
|
||||
Options: e.opts,
|
||||
Progress: e.customProgress,
|
||||
Catalog: e.catalog,
|
||||
IssuesClient: e.rc,
|
||||
RateLimiter: e.rateLimiter,
|
||||
Interactsh: e.interactshClient,
|
||||
Colorizer: aurora.NewAurora(true),
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Browser: e.browserInstance,
|
||||
Parser: e.parser,
|
||||
InputHelper: input.NewHelper(),
|
||||
TemporaryDirectory: e.tmpDir,
|
||||
Logger: e.opts.Logger,
|
||||
Output: e.customWriter,
|
||||
Options: e.opts,
|
||||
Progress: e.customProgress,
|
||||
Catalog: e.catalog,
|
||||
IssuesClient: e.rc,
|
||||
RateLimiter: e.rateLimiter,
|
||||
Interactsh: e.interactshClient,
|
||||
Colorizer: aurora.NewAurora(true),
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Browser: e.browserInstance,
|
||||
Parser: e.parser,
|
||||
InputHelper: input.NewHelper(),
|
||||
Logger: e.opts.Logger,
|
||||
}
|
||||
if e.opts.ShouldUseHostError() && e.hostErrCache != nil {
|
||||
e.executerOpts.HostErrorsCache = e.hostErrCache
|
||||
|
||||
@@ -3,6 +3,7 @@ package nuclei_test
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -35,3 +36,28 @@ func TestContextCancelNucleiEngine(t *testing.T) {
|
||||
}
|
||||
defer ne.Close()
|
||||
}
|
||||
|
||||
func TestSharedParserOptIn(t *testing.T) {
|
||||
os.Setenv("NUCLEI_USE_SHARED_PARSER", "1")
|
||||
t.Cleanup(func() { os.Unsetenv("NUCLEI_USE_SHARED_PARSER") })
|
||||
|
||||
ne, err := nuclei.NewNucleiEngineCtx(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("engine error: %v", err)
|
||||
}
|
||||
p := ne.GetParser()
|
||||
if p == nil {
|
||||
t.Fatalf("expected templates.Parser")
|
||||
}
|
||||
ne2, err := nuclei.NewNucleiEngineCtx(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("engine2 error: %v", err)
|
||||
}
|
||||
p2 := ne2.GetParser()
|
||||
if p2 == nil {
|
||||
t.Fatalf("expected templates.Parser2")
|
||||
}
|
||||
if p.Cache() != p2.Cache() {
|
||||
t.Fatalf("expected shared parsed cache across engines when opt-in is set")
|
||||
}
|
||||
}
|
||||
@@ -59,11 +59,6 @@
|
||||
"title": "engine",
|
||||
"description": "Engine"
|
||||
},
|
||||
"sandbox": {
|
||||
"$ref": "#/$defs/code.Sandbox",
|
||||
"title": "sandbox",
|
||||
"description": "Sandbox"
|
||||
},
|
||||
"pre-condition": {
|
||||
"type": "string",
|
||||
"title": "pre-condition for the request",
|
||||
@@ -91,22 +86,6 @@
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"code.Sandbox": {
|
||||
"properties": {
|
||||
"working-dir": {
|
||||
"type": "string",
|
||||
"title": "working-dir",
|
||||
"description": "Working directory"
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"title": "image",
|
||||
"description": "Image"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"dns.DNSRequestTypeHolder": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -3,7 +3,7 @@ package authx
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
||||
@@ -30,8 +30,8 @@ type Dynamic struct {
|
||||
Input string `json:"input" yaml:"input"` // (optional) target for the dynamic secret
|
||||
Extracted map[string]interface{} `json:"-" yaml:"-"` // extracted values from the dynamic secret
|
||||
fetchCallback LazyFetchSecret `json:"-" yaml:"-"`
|
||||
fetched *atomic.Bool `json:"-" yaml:"-"` // atomic flag to check if the secret has been fetched
|
||||
fetching *atomic.Bool `json:"-" yaml:"-"` // atomic flag to prevent recursive fetch calls
|
||||
m *sync.Mutex `json:"-" yaml:"-"` // mutex for lazy fetch
|
||||
fetched bool `json:"-" yaml:"-"` // flag to check if the secret has been fetched
|
||||
error error `json:"-" yaml:"-"` // error if any
|
||||
}
|
||||
|
||||
@@ -70,8 +70,7 @@ func (d *Dynamic) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// Validate validates the dynamic secret
|
||||
func (d *Dynamic) Validate() error {
|
||||
d.fetched = &atomic.Bool{}
|
||||
d.fetching = &atomic.Bool{}
|
||||
d.m = &sync.Mutex{}
|
||||
if d.TemplatePath == "" {
|
||||
return errkit.New(" template-path is required for dynamic secret")
|
||||
}
|
||||
@@ -98,7 +97,9 @@ func (d *Dynamic) Validate() error {
|
||||
func (d *Dynamic) SetLazyFetchCallback(callback LazyFetchSecret) {
|
||||
d.fetchCallback = func(d *Dynamic) error {
|
||||
err := callback(d)
|
||||
d.fetched = true
|
||||
if err != nil {
|
||||
d.error = err
|
||||
return err
|
||||
}
|
||||
if len(d.Extracted) == 0 {
|
||||
@@ -183,15 +184,9 @@ func (d *Dynamic) applyValuesToSecret(secret *Secret) error {
|
||||
|
||||
// GetStrategy returns the auth strategies for the dynamic secret
|
||||
func (d *Dynamic) GetStrategies() []AuthStrategy {
|
||||
if d.fetched.Load() {
|
||||
if d.error != nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Try to fetch if not already fetched
|
||||
if !d.fetched {
|
||||
_ = d.Fetch(true)
|
||||
}
|
||||
|
||||
if d.error != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -208,23 +203,12 @@ func (d *Dynamic) GetStrategies() []AuthStrategy {
|
||||
// Fetch fetches the dynamic secret
|
||||
// if isFatal is true, it will stop the execution if the secret could not be fetched
|
||||
func (d *Dynamic) Fetch(isFatal bool) error {
|
||||
if d.fetched.Load() {
|
||||
return d.error
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.fetched {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to set fetching flag atomically
|
||||
if !d.fetching.CompareAndSwap(false, true) {
|
||||
// Already fetching, return current error
|
||||
return d.error
|
||||
}
|
||||
|
||||
// We're the only one fetching, call the callback
|
||||
d.error = d.fetchCallback(d)
|
||||
|
||||
// Mark as fetched and clear fetching flag
|
||||
d.fetched.Store(true)
|
||||
d.fetching.Store(false)
|
||||
|
||||
if d.error != nil && isFatal {
|
||||
gologger.Fatal().Msgf("Could not fetch dynamic secret: %s\n", d.error)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ type Secret struct {
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Domains []string `json:"domains" yaml:"domains"`
|
||||
DomainsRegex []string `json:"domains-regex" yaml:"domains-regex"`
|
||||
Headers []KV `json:"headers" yaml:"headers"` // Headers preserve exact casing (useful for case-sensitive APIs)
|
||||
Headers []KV `json:"headers" yaml:"headers"`
|
||||
Cookies []Cookie `json:"cookies" yaml:"cookies"`
|
||||
Params []KV `json:"params" yaml:"params"`
|
||||
Username string `json:"username" yaml:"username"` // can be either email or username
|
||||
@@ -148,7 +148,7 @@ func (s *Secret) Validate() error {
|
||||
}
|
||||
|
||||
type KV struct {
|
||||
Key string `json:"key" yaml:"key"` // Header key (preserves exact casing)
|
||||
Key string `json:"key" yaml:"key"`
|
||||
Value string `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
|
||||
@@ -21,19 +21,15 @@ func NewHeadersAuthStrategy(data *Secret) *HeadersAuthStrategy {
|
||||
}
|
||||
|
||||
// Apply applies the headers auth strategy to the request
|
||||
// NOTE: This preserves exact header casing (e.g., barAuthToken stays as barAuthToken)
|
||||
// This is useful for APIs that require case-sensitive header names
|
||||
func (s *HeadersAuthStrategy) Apply(req *http.Request) {
|
||||
for _, header := range s.Data.Headers {
|
||||
req.Header[header.Key] = []string{header.Value}
|
||||
req.Header.Set(header.Key, header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyOnRR applies the headers auth strategy to the retryable request
|
||||
// NOTE: This preserves exact header casing (e.g., barAuthToken stays as barAuthToken)
|
||||
// This is useful for APIs that require case-sensitive header names
|
||||
func (s *HeadersAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
|
||||
for _, header := range s.Data.Headers {
|
||||
req.Header[header.Key] = []string{header.Value}
|
||||
req.Header.Set(header.Key, header.Value)
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ info:
|
||||
# static secrets
|
||||
static:
|
||||
# for header based auth session
|
||||
# NOTE: Headers preserve exact casing (e.g., x-pdcp-key stays as x-pdcp-key)
|
||||
# This is useful for APIs that require case-sensitive header names
|
||||
- type: header
|
||||
domains:
|
||||
- api.projectdiscovery.io
|
||||
@@ -22,8 +20,6 @@ static:
|
||||
headers:
|
||||
- key: x-pdcp-key
|
||||
value: <api-key-here>
|
||||
- key: barAuthToken
|
||||
value: <auth-token-here>
|
||||
|
||||
# for query based auth session
|
||||
- type: Query
|
||||
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
CLIConfigFileName = "config.yaml"
|
||||
ReportingConfigFilename = "reporting-config.yaml"
|
||||
// Version is the current version of nuclei
|
||||
Version = `v3.6.0`
|
||||
Version = `v3.4.10`
|
||||
// Directory Names of custom templates
|
||||
CustomS3TemplatesDirName = "s3"
|
||||
CustomGitHubTemplatesDirName = "github"
|
||||
@@ -40,7 +40,6 @@ const (
|
||||
BinaryName = "nuclei"
|
||||
FallbackConfigFolderName = ".nuclei-config"
|
||||
NucleiConfigDirEnv = "NUCLEI_CONFIG_DIR"
|
||||
NucleiTemplatesDirEnv = "NUCLEI_TEMPLATES_DIR"
|
||||
)
|
||||
|
||||
// IsOutdatedVersion compares two versions and returns true
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -17,7 +18,7 @@ type IgnoreFile struct {
|
||||
func ReadIgnoreFile() IgnoreFile {
|
||||
file, err := os.Open(DefaultConfig.GetIgnoreFilePath())
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Could not read nuclei-ignore file: %s\n", err)
|
||||
gologger.Error().Msgf("Could not read nuclei-ignore file: %s\n%s\n", err, string(debug.Stack()))
|
||||
return IgnoreFile{}
|
||||
}
|
||||
defer func() {
|
||||
|
||||
@@ -350,7 +350,7 @@ func (c *Config) IsDebugArgEnabled(arg string) bool {
|
||||
|
||||
// parseDebugArgs from string
|
||||
func (c *Config) parseDebugArgs(data string) {
|
||||
// use space as separator instead of commas
|
||||
// use space as seperator instead of commas
|
||||
tmp := strings.Fields(data)
|
||||
for _, v := range tmp {
|
||||
key := v
|
||||
|
||||
@@ -12,10 +12,7 @@ import (
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
)
|
||||
|
||||
var (
|
||||
knownConfigFiles = []string{"cves.json", "contributors.json", "TEMPLATES-STATS.json"}
|
||||
knownMiscDirectories = []string{".git", ".github", "helpers"}
|
||||
)
|
||||
var knownConfigFiles = []string{"cves.json", "contributors.json", "TEMPLATES-STATS.json"}
|
||||
|
||||
// TemplateFormat
|
||||
type TemplateFormat uint8
|
||||
@@ -26,25 +23,6 @@ const (
|
||||
Unknown
|
||||
)
|
||||
|
||||
// GetKnownConfigFiles returns known config files.
|
||||
func GetKnownConfigFiles() []string {
|
||||
return knownConfigFiles
|
||||
}
|
||||
|
||||
// GetKnownMiscDirectories returns known misc directories with trailing slashes.
|
||||
//
|
||||
// The trailing slash ensures that directory matching is explicit and avoids
|
||||
// falsely match files with similar names (e.g. "helpers" matching
|
||||
// "some-helpers.yaml"), since [IsTemplate] checks against normalized full paths.
|
||||
func GetKnownMiscDirectories() []string {
|
||||
trailedSlashDirs := make([]string, 0, len(knownMiscDirectories))
|
||||
for _, dir := range knownMiscDirectories {
|
||||
trailedSlashDirs = append(trailedSlashDirs, dir+string(os.PathSeparator))
|
||||
}
|
||||
|
||||
return trailedSlashDirs
|
||||
}
|
||||
|
||||
// GetTemplateFormatFromExt returns template format
|
||||
func GetTemplateFormatFromExt(filePath string) TemplateFormat {
|
||||
fileExt := strings.ToLower(filepath.Ext(filePath))
|
||||
@@ -63,22 +41,13 @@ func GetSupportTemplateFileExtensions() []string {
|
||||
return []string{extensions.YAML, extensions.JSON}
|
||||
}
|
||||
|
||||
// IsTemplate returns true if the file is a template based on its path.
|
||||
// It used by goflags and other places to filter out non-template files.
|
||||
func IsTemplate(fpath string) bool {
|
||||
fpath = filepath.FromSlash(fpath)
|
||||
fname := filepath.Base(fpath)
|
||||
fext := strings.ToLower(filepath.Ext(fpath))
|
||||
|
||||
if stringsutil.ContainsAny(fname, GetKnownConfigFiles()...) {
|
||||
// IsTemplate is a callback function used by goflags to decide if given file should be read
|
||||
// if it is not a nuclei-template file only then file is read
|
||||
func IsTemplate(filename string) bool {
|
||||
if stringsutil.ContainsAny(filename, knownConfigFiles...) {
|
||||
return false
|
||||
}
|
||||
|
||||
if stringsutil.ContainsAny(fpath, GetKnownMiscDirectories()...) {
|
||||
return false
|
||||
}
|
||||
|
||||
return stringsutil.EqualFoldAny(fext, GetSupportTemplateFileExtensions()...)
|
||||
return stringsutil.EqualFoldAny(filepath.Ext(filename), GetSupportTemplateFileExtensions()...)
|
||||
}
|
||||
|
||||
type template struct {
|
||||
|
||||
@@ -38,7 +38,13 @@ func NewFSCatalog(fs fs.FS, directory string) *DiskCatalog {
|
||||
// It is used to read template and payload files based on catalog responses.
|
||||
func (d *DiskCatalog) OpenFile(filename string) (io.ReadCloser, error) {
|
||||
if d.templatesFS == nil {
|
||||
return os.Open(filename)
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
if file, errx := os.Open(BackwardsCompatiblePaths(d.templatesDirectory, filename)); errx == nil {
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
return d.templatesFS.Open(filename)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package disk
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNoTemplatesFound = errors.New("no templates found")
|
||||
)
|
||||
+88
-45
@@ -1,7 +1,6 @@
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -66,34 +65,41 @@ func (c *DiskCatalog) GetTemplatesPath(definitions []string) ([]string, map[stri
|
||||
// or folders provided as in.
|
||||
func (c *DiskCatalog) GetTemplatePath(target string) ([]string, error) {
|
||||
processed := make(map[string]struct{})
|
||||
// Template input includes a wildcard
|
||||
if strings.Contains(target, "*") {
|
||||
matches, findErr := c.findGlobPathMatches(target, processed)
|
||||
if findErr != nil {
|
||||
return nil, errors.Wrap(findErr, "could not find glob matches")
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return nil, errors.Errorf("no templates found for path")
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
// try to handle deprecated template paths
|
||||
absPath := target
|
||||
if c.templatesFS == nil {
|
||||
absPath = BackwardsCompatiblePaths(c.templatesDirectory, target)
|
||||
if absPath != target && strings.TrimPrefix(absPath, c.templatesDirectory+string(filepath.Separator)) != target {
|
||||
if config.DefaultConfig.LogAllEvents {
|
||||
config.DefaultConfig.Logger.Print().Msgf("[%v] requested Template path %s is deprecated, please update to %s\n", aurora.Yellow("WRN").String(), target, absPath)
|
||||
}
|
||||
deprecatedPathsCounter++
|
||||
}
|
||||
|
||||
var err error
|
||||
target, err = c.convertPathToAbsolute(target)
|
||||
absPath, err = c.convertPathToAbsolute(absPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not find template file")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(target, "*") {
|
||||
globMatches, err := c.findGlobPathMatches(target, processed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not globbing path")
|
||||
}
|
||||
|
||||
if len(globMatches) > 0 {
|
||||
return globMatches, nil
|
||||
} else {
|
||||
return globMatches, fmt.Errorf("%w in path %q", ErrNoTemplatesFound, target)
|
||||
}
|
||||
}
|
||||
|
||||
// `target` is either a file or a directory
|
||||
match, file, err := c.findFileMatches(target, processed)
|
||||
// Template input is either a file or a directory
|
||||
match, file, err := c.findFileMatches(absPath, processed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find file")
|
||||
}
|
||||
|
||||
if file {
|
||||
if match != "" {
|
||||
return []string{match}, nil
|
||||
@@ -103,15 +109,13 @@ func (c *DiskCatalog) GetTemplatePath(target string) ([]string, error) {
|
||||
|
||||
// Recursively walk down the Templates directory and run all
|
||||
// the template file checks
|
||||
matches, err := c.findDirectoryMatches(target, processed)
|
||||
matches, err := c.findDirectoryMatches(absPath, processed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find directory matches")
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("%w in path %q", ErrNoTemplatesFound, target)
|
||||
return nil, errors.Errorf("no templates found in path %s", absPath)
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
@@ -131,29 +135,72 @@ func (c *DiskCatalog) convertPathToAbsolute(t string) (string, error) {
|
||||
|
||||
// findGlobPathMatches returns the matched files from a glob path
|
||||
func (c *DiskCatalog) findGlobPathMatches(absPath string, processed map[string]struct{}) ([]string, error) {
|
||||
// to support globbing on old paths we use brute force to find matches with exit on first match
|
||||
// trim templateDir if any
|
||||
relPath := strings.TrimPrefix(absPath, c.templatesDirectory)
|
||||
// trim leading slash if any
|
||||
if c.templatesFS != nil {
|
||||
// fs.FS always uses forward slashes
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
} else {
|
||||
relPath = strings.TrimPrefix(relPath, string(os.PathSeparator))
|
||||
relPath = strings.TrimPrefix(relPath, string(os.PathSeparator))
|
||||
|
||||
OldPathsResolver := func(inputGlob string) []string {
|
||||
templateDir := c.templatesDirectory
|
||||
if c.templatesDirectory == "" {
|
||||
templateDir = "./"
|
||||
}
|
||||
|
||||
if c.templatesFS == nil {
|
||||
matches, _ := fs.Glob(os.DirFS(filepath.Join(templateDir, "http")), inputGlob)
|
||||
if len(matches) != 0 {
|
||||
return matches
|
||||
}
|
||||
|
||||
// condition to support network cve related globs
|
||||
matches, _ = fs.Glob(os.DirFS(filepath.Join(templateDir, "network")), inputGlob)
|
||||
return matches
|
||||
} else {
|
||||
sub, err := fs.Sub(c.templatesFS, filepath.Join(templateDir, "http"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
matches, _ := fs.Glob(sub, inputGlob)
|
||||
if len(matches) != 0 {
|
||||
return matches
|
||||
}
|
||||
|
||||
// condition to support network cve related globs
|
||||
sub, err = fs.Sub(c.templatesFS, filepath.Join(templateDir, "network"))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
matches, _ = fs.Glob(sub, inputGlob)
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var matched []string
|
||||
var matches []string
|
||||
|
||||
if c.templatesFS != nil {
|
||||
matches, err = fs.Glob(c.templatesFS, relPath)
|
||||
if c.templatesFS == nil {
|
||||
var err error
|
||||
matches, err = filepath.Glob(relPath)
|
||||
if len(matches) != 0 {
|
||||
matched = append(matched, matches...)
|
||||
} else {
|
||||
matched = append(matched, OldPathsResolver(relPath)...)
|
||||
}
|
||||
if err != nil && len(matched) == 0 {
|
||||
return nil, errors.Errorf("wildcard found, but unable to glob: %s\n", err)
|
||||
}
|
||||
} else {
|
||||
matches, err = filepath.Glob(absPath)
|
||||
var err error
|
||||
matches, err = fs.Glob(c.templatesFS, relPath)
|
||||
if len(matches) != 0 {
|
||||
matched = append(matched, matches...)
|
||||
} else {
|
||||
matched = append(matched, OldPathsResolver(relPath)...)
|
||||
}
|
||||
if err != nil && len(matched) == 0 {
|
||||
return nil, errors.Errorf("wildcard found, but unable to glob: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
if _, ok := processed[match]; !ok {
|
||||
@@ -161,7 +208,6 @@ func (c *DiskCatalog) findGlobPathMatches(absPath string, processed map[string]s
|
||||
results = append(results, match)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -211,7 +257,7 @@ func (c *DiskCatalog) findDirectoryMatches(absPath string, processed map[string]
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() && config.IsTemplate(path) {
|
||||
if !d.IsDir() && config.GetTemplateFormatFromExt(path) != config.Unknown {
|
||||
if _, ok := processed[path]; !ok {
|
||||
results = append(results, path)
|
||||
processed[path] = struct{}{}
|
||||
@@ -235,7 +281,7 @@ func (c *DiskCatalog) findDirectoryMatches(absPath string, processed map[string]
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() && config.IsTemplate(path) {
|
||||
if !d.IsDir() && config.GetTemplateFormatFromExt(path) != config.Unknown {
|
||||
if _, ok := processed[path]; !ok {
|
||||
results = append(results, path)
|
||||
processed[path] = struct{}{}
|
||||
@@ -248,11 +294,8 @@ func (c *DiskCatalog) findDirectoryMatches(absPath string, processed map[string]
|
||||
return results, err
|
||||
}
|
||||
|
||||
// PrintDeprecatedPathsMsgIfApplicable prints a warning message if any
|
||||
// deprecated paths are found. Unless mode is silent warning message is printed.
|
||||
//
|
||||
// Deprecated: No longer used since the official Nuclei Templates repository
|
||||
// have restructured this a long time ago.
|
||||
// PrintDeprecatedPathsMsgIfApplicable prints a warning message if any deprecated paths are found
|
||||
// Unless mode is silent warning message is printed
|
||||
func PrintDeprecatedPathsMsgIfApplicable(isSilent bool) {
|
||||
if !updateutils.IsOutdated("v9.4.3", config.DefaultConfig.TemplateVersion) {
|
||||
return
|
||||
|
||||
@@ -72,9 +72,6 @@ func (c *DiskCatalog) tryResolve(fullPath string) (string, error) {
|
||||
|
||||
// BackwardsCompatiblePaths returns new paths for all old/legacy template paths
|
||||
// Note: this is a temporary function and will be removed in the future release
|
||||
//
|
||||
// Deprecated: No longer used since the official Nuclei Templates repository
|
||||
// have restructured this a long time ago.
|
||||
func BackwardsCompatiblePaths(templateDir string, oldPath string) string {
|
||||
// TODO: remove this function in the future release
|
||||
// 1. all http related paths are now moved at path /http
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
)
|
||||
|
||||
// Filter represents filtering criteria for template metadata.
|
||||
//
|
||||
// Inclusion fields (e.g., Authors, Tags, IDs, Severities, ProtocolTypes) use
|
||||
// AND logic across different filter types and OR logic within each type.
|
||||
// Exclusion fields (e.g., ExcludeTags, ExcludeIDs, ExcludeSeverities,
|
||||
// ExcludeProtocolTypes) take precedence over inclusion fields. Additionally,
|
||||
// IncludeTemplates and IncludeTags can force inclusion of templates even if
|
||||
// they match exclusion criteria.
|
||||
type Filter struct {
|
||||
// Authors to include.
|
||||
Authors []string
|
||||
|
||||
// Tags to include.
|
||||
Tags []string
|
||||
|
||||
// ExcludeTags to exclude (takes precedence over Tags).
|
||||
ExcludeTags []string
|
||||
|
||||
// IncludeTags to force include even if excluded.
|
||||
IncludeTags []string
|
||||
|
||||
// IDs to include (supports wildcards, OR logic).
|
||||
IDs []string
|
||||
|
||||
// ExcludeIDs to exclude (supports wildcards).
|
||||
ExcludeIDs []string
|
||||
|
||||
// IncludeTemplates paths to force include even if excluded.
|
||||
IncludeTemplates []string
|
||||
|
||||
// ExcludeTemplates paths to exclude.
|
||||
ExcludeTemplates []string
|
||||
|
||||
// Severities to include.
|
||||
Severities []severity.Severity
|
||||
|
||||
// ExcludeSeverities to exclude.
|
||||
ExcludeSeverities []severity.Severity
|
||||
|
||||
// ProtocolTypes to include.
|
||||
ProtocolTypes []types.ProtocolType
|
||||
|
||||
// ExcludeProtocolTypes to exclude.
|
||||
ExcludeProtocolTypes []types.ProtocolType
|
||||
}
|
||||
|
||||
// Matches checks if metadata matches the filter criteria.
|
||||
func (f *Filter) Matches(m *Metadata) bool {
|
||||
if f.isForcedInclude(m) {
|
||||
return true
|
||||
}
|
||||
|
||||
if f.isExcluded(m) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !f.matchesIncludes(m) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isForcedInclude checks if template is forced to be included.
|
||||
func (f *Filter) isForcedInclude(m *Metadata) bool {
|
||||
if len(f.IncludeTemplates) > 0 {
|
||||
for _, includePath := range f.IncludeTemplates {
|
||||
if matchesPath(m.FilePath, includePath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.IncludeTags) > 0 {
|
||||
if slices.ContainsFunc(f.IncludeTags, m.HasTag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isExcluded checks if template should be excluded.
|
||||
func (f *Filter) isExcluded(m *Metadata) bool {
|
||||
if len(f.ExcludeTemplates) > 0 {
|
||||
for _, excludePath := range f.ExcludeTemplates {
|
||||
if matchesPath(m.FilePath, excludePath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ExcludeTags) > 0 {
|
||||
if slices.ContainsFunc(f.ExcludeTags, m.HasTag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ExcludeIDs) > 0 {
|
||||
for _, excludeID := range f.ExcludeIDs {
|
||||
if matchesID(m.ID, excludeID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ExcludeSeverities) > 0 {
|
||||
if slices.ContainsFunc(f.ExcludeSeverities, m.MatchesSeverity) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ExcludeProtocolTypes) > 0 {
|
||||
if slices.ContainsFunc(f.ExcludeProtocolTypes, m.MatchesProtocol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesIncludes checks if metadata matches include filters.
|
||||
//
|
||||
// Returns true if no include filters are specified, or if all specified filter
|
||||
// types match.
|
||||
func (f *Filter) matchesIncludes(m *Metadata) bool {
|
||||
if len(f.Authors) > 0 {
|
||||
if !slices.ContainsFunc(f.Authors, m.HasAuthor) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Tags) > 0 {
|
||||
if !slices.ContainsFunc(f.Tags, m.HasTag) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.IDs) > 0 {
|
||||
matched := false
|
||||
for _, id := range f.IDs {
|
||||
if matchesID(m.ID, id) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Severities) > 0 {
|
||||
if !slices.ContainsFunc(f.Severities, m.MatchesSeverity) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.ProtocolTypes) > 0 {
|
||||
if !slices.ContainsFunc(f.ProtocolTypes, m.MatchesProtocol) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// matchesID checks if template ID matches pattern (supports wildcards).
|
||||
func matchesID(templateID, pattern string) bool {
|
||||
// Convert to lowercase for case-insensitive matching
|
||||
templateID = strings.ToLower(templateID)
|
||||
pattern = strings.ToLower(pattern)
|
||||
|
||||
if templateID == pattern {
|
||||
return true
|
||||
}
|
||||
|
||||
matched, _ := filepath.Match(pattern, templateID)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// matchesPath checks if template path matches pattern.
|
||||
func matchesPath(templatePath, pattern string) bool {
|
||||
templatePath = filepath.Clean(templatePath)
|
||||
pattern = filepath.Clean(pattern)
|
||||
|
||||
if templatePath == pattern {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(templatePath, pattern+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
|
||||
matched, _ := filepath.Match(pattern, templatePath)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// FilterFunc is a function that filters metadata.
|
||||
type FilterFunc func(*Metadata) bool
|
||||
|
||||
// UnmarshalFilter creates a Filter from nuclei options.
|
||||
func UnmarshalFilter(
|
||||
authors, tags, excludeTags, includeTags []string,
|
||||
ids, excludeIDs []string,
|
||||
includeTemplates, excludeTemplates []string,
|
||||
severities, excludeSeverities []string,
|
||||
protocolTypes, excludeProtocolTypes []string,
|
||||
) (*Filter, error) {
|
||||
filter := &Filter{
|
||||
Authors: authors,
|
||||
Tags: tags,
|
||||
ExcludeTags: excludeTags,
|
||||
IncludeTags: includeTags,
|
||||
IDs: ids,
|
||||
ExcludeIDs: excludeIDs,
|
||||
IncludeTemplates: includeTemplates,
|
||||
ExcludeTemplates: excludeTemplates,
|
||||
}
|
||||
|
||||
for _, sev := range severities {
|
||||
holder := &severity.Holder{}
|
||||
if err := holder.UnmarshalYAML(func(v interface{}) error {
|
||||
*v.(*string) = sev
|
||||
return nil
|
||||
}); err == nil {
|
||||
filter.Severities = append(filter.Severities, holder.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sev := range excludeSeverities {
|
||||
holder := &severity.Holder{}
|
||||
if err := holder.UnmarshalYAML(func(v interface{}) error {
|
||||
*v.(*string) = sev
|
||||
return nil
|
||||
}); err == nil {
|
||||
filter.ExcludeSeverities = append(filter.ExcludeSeverities, holder.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pt := range protocolTypes {
|
||||
holder := &types.TypeHolder{}
|
||||
if err := holder.UnmarshalYAML(func(v interface{}) error {
|
||||
*v.(*string) = pt
|
||||
return nil
|
||||
}); err == nil && holder.ProtocolType != types.InvalidProtocol {
|
||||
filter.ProtocolTypes = append(filter.ProtocolTypes, holder.ProtocolType)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pt := range excludeProtocolTypes {
|
||||
holder := &types.TypeHolder{}
|
||||
if err := holder.UnmarshalYAML(func(v interface{}) error {
|
||||
*v.(*string) = pt
|
||||
return nil
|
||||
}); err == nil && holder.ProtocolType != types.InvalidProtocol {
|
||||
filter.ExcludeProtocolTypes = append(filter.ExcludeProtocolTypes, holder.ProtocolType)
|
||||
}
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// UnmarshalFilterFunc creates a FilterFunc from filter criteria.
|
||||
func UnmarshalFilterFunc(filter *Filter) FilterFunc {
|
||||
if filter == nil {
|
||||
return func(*Metadata) bool { return true }
|
||||
}
|
||||
|
||||
return filter.Matches
|
||||
}
|
||||
|
||||
// IsEmpty returns true if filter has no criteria set.
|
||||
func (f *Filter) IsEmpty() bool {
|
||||
return len(f.Authors) == 0 &&
|
||||
len(f.Tags) == 0 &&
|
||||
len(f.ExcludeTags) == 0 &&
|
||||
len(f.IncludeTags) == 0 &&
|
||||
len(f.IDs) == 0 &&
|
||||
len(f.ExcludeIDs) == 0 &&
|
||||
len(f.IncludeTemplates) == 0 &&
|
||||
len(f.ExcludeTemplates) == 0 &&
|
||||
len(f.Severities) == 0 &&
|
||||
len(f.ExcludeSeverities) == 0 &&
|
||||
len(f.ProtocolTypes) == 0 &&
|
||||
len(f.ExcludeProtocolTypes) == 0
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the filter.
|
||||
func (f *Filter) String() string {
|
||||
var parts []string
|
||||
|
||||
if len(f.Authors) > 0 {
|
||||
parts = append(parts, "authors="+strings.Join(f.Authors, ","))
|
||||
}
|
||||
|
||||
if len(f.Tags) > 0 {
|
||||
parts = append(parts, "tags="+strings.Join(f.Tags, ","))
|
||||
}
|
||||
|
||||
if len(f.ExcludeTags) > 0 {
|
||||
parts = append(parts, "exclude-tags="+strings.Join(f.ExcludeTags, ","))
|
||||
}
|
||||
|
||||
if len(f.IDs) > 0 {
|
||||
parts = append(parts, "ids="+strings.Join(f.IDs, ","))
|
||||
}
|
||||
|
||||
if len(f.Severities) > 0 {
|
||||
sevs := make([]string, len(f.Severities))
|
||||
for i, s := range f.Severities {
|
||||
sevs[i] = s.String()
|
||||
}
|
||||
|
||||
parts = append(parts, "severities="+strings.Join(sevs, ","))
|
||||
}
|
||||
|
||||
if len(f.ProtocolTypes) > 0 {
|
||||
pts := make([]string, len(f.ProtocolTypes))
|
||||
for i, p := range f.ProtocolTypes {
|
||||
pts[i] = p.String()
|
||||
}
|
||||
|
||||
parts = append(parts, "types="+strings.Join(pts, ","))
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "filter=<nil>"
|
||||
}
|
||||
|
||||
return "filter(" + strings.Join(parts, ", ") + ")"
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFilterMatches(t *testing.T) {
|
||||
metadata := &Metadata{
|
||||
ID: "test-template-1",
|
||||
FilePath: "/templates/cves/2021/CVE-2021-1234.yaml",
|
||||
Name: "Test CVE Template",
|
||||
Authors: []string{"pdteam", "geeknik"},
|
||||
Tags: []string{"cve", "rce", "apache"},
|
||||
Severity: "critical",
|
||||
ProtocolType: "http",
|
||||
}
|
||||
|
||||
t.Run("Empty filter matches all", func(t *testing.T) {
|
||||
filter := &Filter{}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
require.True(t, filter.IsEmpty())
|
||||
})
|
||||
|
||||
t.Run("Author filter - match", func(t *testing.T) {
|
||||
filter := &Filter{Authors: []string{"pdteam"}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Author filter - no match", func(t *testing.T) {
|
||||
filter := &Filter{Authors: []string{"unknown"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Multiple authors - OR logic", func(t *testing.T) {
|
||||
filter := &Filter{Authors: []string{"unknown", "geeknik"}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Tag filter - match", func(t *testing.T) {
|
||||
filter := &Filter{Tags: []string{"cve"}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Tag filter - no match", func(t *testing.T) {
|
||||
filter := &Filter{Tags: []string{"xss"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude tags - match", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeTags: []string{"rce"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Include tags overrides exclude", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
ExcludeTags: []string{"rce"},
|
||||
IncludeTags: []string{"cve"},
|
||||
}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("ID filter - exact match", func(t *testing.T) {
|
||||
filter := &Filter{IDs: []string{"test-template-1"}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("ID filter - wildcard match", func(t *testing.T) {
|
||||
filter := &Filter{IDs: []string{"test-*"}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("ID filter - no match", func(t *testing.T) {
|
||||
filter := &Filter{IDs: []string{"other-*"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude ID - exact match", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeIDs: []string{"test-template-1"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude ID - wildcard match", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeIDs: []string{"test-*"}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Severity filter - match", func(t *testing.T) {
|
||||
filter := &Filter{Severities: []severity.Severity{severity.Critical}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Severity filter - no match", func(t *testing.T) {
|
||||
filter := &Filter{Severities: []severity.Severity{severity.High, severity.Medium}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude severity - match", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeSeverities: []severity.Severity{severity.Critical}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Protocol type filter - match", func(t *testing.T) {
|
||||
filter := &Filter{ProtocolTypes: []types.ProtocolType{types.HTTPProtocol}}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Protocol type filter - no match", func(t *testing.T) {
|
||||
filter := &Filter{ProtocolTypes: []types.ProtocolType{types.DNSProtocol}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude protocol type - match", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeProtocolTypes: []types.ProtocolType{types.HTTPProtocol}}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Include templates - path match", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
ExcludeTags: []string{"cve"},
|
||||
IncludeTemplates: []string{"/templates/cves/"},
|
||||
}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Exclude templates - path match", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
ExcludeTemplates: []string{"/templates/cves/"},
|
||||
}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Complex filter - all match", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
Authors: []string{"pdteam"},
|
||||
Tags: []string{"cve"},
|
||||
Severities: []severity.Severity{severity.Critical},
|
||||
ProtocolTypes: []types.ProtocolType{types.HTTPProtocol},
|
||||
}
|
||||
require.True(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Complex filter - AND logic across types", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
Authors: []string{"pdteam"}, // matches
|
||||
Tags: []string{"xss"}, // doesn't match
|
||||
Severities: []severity.Severity{severity.Critical}, // matches
|
||||
}
|
||||
// With AND logic across filter types, doesn't match because tags don't match
|
||||
// even though author and severity match
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
|
||||
t.Run("Complex filter - no match at all", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
Authors: []string{"unknown"}, // doesn't match
|
||||
Tags: []string{"xss"}, // doesn't match
|
||||
Severities: []severity.Severity{severity.Low}, // doesn't match
|
||||
}
|
||||
require.False(t, filter.Matches(metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatchesPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
pattern string
|
||||
expected bool
|
||||
}{
|
||||
{"exact match", "/templates/cves/2021/test.yaml", "/templates/cves/2021/test.yaml", true},
|
||||
{"directory prefix", "/templates/cves/2021/test.yaml", "/templates/cves", true},
|
||||
{"directory with slash", "/templates/cves/2021/test.yaml", "/templates/cves/", true},
|
||||
{"no match", "/templates/cves/2021/test.yaml", "/templates/exploits", false},
|
||||
{"wildcard match", "/templates/cves/2021/test.yaml", "/templates/*/2021/*.yaml", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := matchesPath(tt.path, tt.pattern)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
pattern string
|
||||
expected bool
|
||||
}{
|
||||
{"exact match", "CVE-2021-1234", "CVE-2021-1234", true},
|
||||
{"wildcard prefix", "CVE-2021-1234", "CVE-*", true},
|
||||
{"wildcard suffix", "CVE-2021-1234", "*-1234", true},
|
||||
{"wildcard middle", "CVE-2021-1234", "CVE-*-1234", true},
|
||||
{"no match", "CVE-2021-1234", "CVE-2022-*", false},
|
||||
{"partial no match", "CVE-2021-1234", "CVE-2021-12", false},
|
||||
{"case insensitive exact", "cve-2021-1234", "CVE-2021-1234", true},
|
||||
{"case insensitive wildcard", "CVE-2021-1234", "cve-*", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := matchesID(tt.id, tt.pattern)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalFilter(t *testing.T) {
|
||||
filter, err := UnmarshalFilter(
|
||||
[]string{"author1", "author2"},
|
||||
[]string{"tag1", "tag2"},
|
||||
[]string{"exclude-tag"},
|
||||
[]string{"include-tag"},
|
||||
[]string{"id1", "id2*"},
|
||||
[]string{"exclude-id*"},
|
||||
[]string{"/include/path"},
|
||||
[]string{"/exclude/path"},
|
||||
[]string{"critical", "high"},
|
||||
[]string{"info"},
|
||||
[]string{"http", "dns"},
|
||||
[]string{"file"},
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, filter)
|
||||
|
||||
require.Equal(t, []string{"author1", "author2"}, filter.Authors)
|
||||
require.Equal(t, []string{"tag1", "tag2"}, filter.Tags)
|
||||
require.Equal(t, []string{"exclude-tag"}, filter.ExcludeTags)
|
||||
require.Equal(t, []string{"include-tag"}, filter.IncludeTags)
|
||||
require.Equal(t, []string{"id1", "id2*"}, filter.IDs)
|
||||
require.Equal(t, []string{"exclude-id*"}, filter.ExcludeIDs)
|
||||
require.Equal(t, []string{"/include/path"}, filter.IncludeTemplates)
|
||||
require.Equal(t, []string{"/exclude/path"}, filter.ExcludeTemplates)
|
||||
|
||||
require.Len(t, filter.Severities, 2)
|
||||
require.Contains(t, filter.Severities, severity.Critical)
|
||||
require.Contains(t, filter.Severities, severity.High)
|
||||
|
||||
require.Len(t, filter.ExcludeSeverities, 1)
|
||||
require.Contains(t, filter.ExcludeSeverities, severity.Info)
|
||||
|
||||
require.Len(t, filter.ProtocolTypes, 2)
|
||||
require.Contains(t, filter.ProtocolTypes, types.HTTPProtocol)
|
||||
require.Contains(t, filter.ProtocolTypes, types.DNSProtocol)
|
||||
|
||||
require.Len(t, filter.ExcludeProtocolTypes, 1)
|
||||
require.Contains(t, filter.ExcludeProtocolTypes, types.FileProtocol)
|
||||
}
|
||||
|
||||
func TestIndexFilter(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
idx, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create test templates and metadata
|
||||
templates := []struct {
|
||||
id string
|
||||
path string
|
||||
authors []string
|
||||
tags []string
|
||||
severity string
|
||||
protocol string
|
||||
}{
|
||||
{"cve-2021-1", "/templates/cves/CVE-2021-1.yaml", []string{"pdteam"}, []string{"cve", "rce"}, "critical", "http"},
|
||||
{"cve-2021-2", "/templates/cves/CVE-2021-2.yaml", []string{"pdteam"}, []string{"cve", "xss"}, "high", "http"},
|
||||
{"exploit-1", "/templates/exploits/exploit-1.yaml", []string{"geeknik"}, []string{"exploit"}, "medium", "dns"},
|
||||
{"info-1", "/templates/info/info-1.yaml", []string{"author1"}, []string{"info"}, "info", "http"},
|
||||
}
|
||||
|
||||
for _, tmpl := range templates {
|
||||
tmpFile := filepath.Join(tmpDir, filepath.Base(tmpl.path))
|
||||
err := os.WriteFile(tmpFile, []byte("id: "+tmpl.id), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: tmpl.id,
|
||||
FilePath: tmpFile,
|
||||
Authors: tmpl.authors,
|
||||
Tags: tmpl.tags,
|
||||
Severity: tmpl.severity,
|
||||
ProtocolType: tmpl.protocol,
|
||||
}
|
||||
idx.Set(tmpl.path, metadata)
|
||||
}
|
||||
|
||||
t.Run("No filter returns all", func(t *testing.T) {
|
||||
results := idx.Filter(nil)
|
||||
require.Len(t, results, 4)
|
||||
})
|
||||
|
||||
t.Run("Filter by author", func(t *testing.T) {
|
||||
filter := &Filter{Authors: []string{"pdteam"}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 2)
|
||||
})
|
||||
|
||||
t.Run("Filter by tag", func(t *testing.T) {
|
||||
filter := &Filter{Tags: []string{"cve"}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 2)
|
||||
})
|
||||
|
||||
t.Run("Filter by severity", func(t *testing.T) {
|
||||
filter := &Filter{Severities: []severity.Severity{severity.Critical}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 1)
|
||||
})
|
||||
|
||||
t.Run("Filter by protocol type", func(t *testing.T) {
|
||||
filter := &Filter{ProtocolTypes: []types.ProtocolType{types.HTTPProtocol}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 3)
|
||||
})
|
||||
|
||||
t.Run("Exclude by severity", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeSeverities: []severity.Severity{severity.Info}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 3)
|
||||
})
|
||||
|
||||
t.Run("Exclude by tag", func(t *testing.T) {
|
||||
filter := &Filter{ExcludeTags: []string{"info"}}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 3)
|
||||
})
|
||||
|
||||
t.Run("Complex filter", func(t *testing.T) {
|
||||
filter := &Filter{
|
||||
Tags: []string{"cve"},
|
||||
Severities: []severity.Severity{severity.Critical, severity.High},
|
||||
ExcludeSeverities: []severity.Severity{severity.Info},
|
||||
}
|
||||
results := idx.Filter(filter)
|
||||
require.Len(t, results, 2)
|
||||
})
|
||||
|
||||
t.Run("Count with filter", func(t *testing.T) {
|
||||
filter := &Filter{Tags: []string{"cve"}}
|
||||
count := idx.Count(filter)
|
||||
require.Equal(t, 2, count)
|
||||
})
|
||||
|
||||
t.Run("Count without filter", func(t *testing.T) {
|
||||
count := idx.Count(nil)
|
||||
require.Equal(t, 4, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIndexFilterFunc(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
idx, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add test metadata
|
||||
for i := 0; i < 5; i++ {
|
||||
metadata := &Metadata{
|
||||
ID: "test-" + string(rune('a'+i)),
|
||||
FilePath: "/tmp/test.yaml",
|
||||
Severity: "high",
|
||||
}
|
||||
if i%2 == 0 {
|
||||
metadata.Tags = []string{"even"}
|
||||
} else {
|
||||
metadata.Tags = []string{"odd"}
|
||||
}
|
||||
idx.Set("/tmp/test-"+string(rune('a'+i))+".yaml", metadata)
|
||||
}
|
||||
|
||||
t.Run("Custom filter function", func(t *testing.T) {
|
||||
results := idx.FilterFunc(func(m *Metadata) bool {
|
||||
return m.HasTag("even")
|
||||
})
|
||||
require.Len(t, results, 3) // 0, 2, 4
|
||||
})
|
||||
|
||||
t.Run("Nil filter function returns all", func(t *testing.T) {
|
||||
results := idx.FilterFunc(nil)
|
||||
require.Len(t, results, 5)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterString(t *testing.T) {
|
||||
filter := &Filter{
|
||||
Authors: []string{"author1", "author2"},
|
||||
Tags: []string{"tag1"},
|
||||
Severities: []severity.Severity{severity.Critical, severity.High},
|
||||
ProtocolTypes: []types.ProtocolType{types.HTTPProtocol},
|
||||
}
|
||||
|
||||
str := filter.String()
|
||||
require.Contains(t, str, "authors=")
|
||||
require.Contains(t, str, "tags=")
|
||||
require.Contains(t, str, "severities=")
|
||||
require.Contains(t, str, "types=")
|
||||
|
||||
emptyFilter := &Filter{}
|
||||
require.Equal(t, "filter=<nil>", emptyFilter.String())
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/maypok86/otter/v2"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
folderutil "github.com/projectdiscovery/utils/folder"
|
||||
)
|
||||
|
||||
const (
|
||||
// IndexFileName is the name of the persistent cache file.
|
||||
IndexFileName = "index.gob"
|
||||
|
||||
// IndexVersion is the schema version for cache invalidation on breaking
|
||||
// changes.
|
||||
IndexVersion = 1
|
||||
|
||||
// DefaultMaxSize is the default maximum number of templates to cache.
|
||||
DefaultMaxSize = 50000
|
||||
|
||||
// DefaultMaxWeight is the default maximum weight of the cache.
|
||||
DefaultMaxWeight = DefaultMaxSize * 800 // ~40MB assuming ~800B/entry
|
||||
)
|
||||
|
||||
// Index represents a cache for template metadata.
|
||||
type Index struct {
|
||||
cache *otter.Cache[string, *Metadata]
|
||||
cacheFile string
|
||||
mu sync.RWMutex
|
||||
version int
|
||||
}
|
||||
|
||||
// cacheSnapshot represents the serialized cache structure.
|
||||
type cacheSnapshot struct {
|
||||
Version int `gob:"version"`
|
||||
Data map[string]*Metadata `gob:"data"`
|
||||
}
|
||||
|
||||
// NewIndex creates a new template metadata cache with the given options.
|
||||
func NewIndex(cacheDir string) (*Index, error) {
|
||||
if cacheDir == "" {
|
||||
cacheDir = folderutil.AppCacheDirOrDefault(".nuclei-cache", config.BinaryName)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheFile := filepath.Join(cacheDir, IndexFileName)
|
||||
|
||||
// NOTE(dwisiswant0): Build cache with adaptive sizing based on memory cost.
|
||||
opts := &otter.Options[string, *Metadata]{
|
||||
MaximumWeight: uint64(DefaultMaxWeight),
|
||||
Weigher: func(key string, value *Metadata) uint32 {
|
||||
if value == nil {
|
||||
return uint32(len(key))
|
||||
}
|
||||
|
||||
weight := len(key)
|
||||
weight += len(value.ID)
|
||||
weight += len(value.FilePath)
|
||||
weight += 24 // ModTime is time.Time (24B)
|
||||
weight += len(value.Name)
|
||||
weight += len(value.Severity)
|
||||
weight += len(value.ProtocolType)
|
||||
weight += len(value.TemplateVerifier)
|
||||
|
||||
for _, author := range value.Authors {
|
||||
weight += len(author)
|
||||
}
|
||||
for _, tag := range value.Tags {
|
||||
weight += len(tag)
|
||||
}
|
||||
|
||||
return uint32(weight)
|
||||
},
|
||||
}
|
||||
|
||||
cache, err := otter.New(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Index{
|
||||
cache: cache,
|
||||
cacheFile: cacheFile,
|
||||
version: IndexVersion,
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewDefaultIndex creates a index with default settings in the default cache
|
||||
// directory.
|
||||
func NewDefaultIndex() (*Index, error) {
|
||||
return NewIndex("")
|
||||
}
|
||||
|
||||
// Get retrieves metadata for a template path, validating freshness via mtime.
|
||||
func (i *Index) Get(path string) (*Metadata, bool) {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
metadata, found := i.cache.GetIfPresent(path)
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !metadata.IsValid() {
|
||||
go i.Delete(path)
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return metadata, true
|
||||
}
|
||||
|
||||
// Set stores metadata for a template path.
|
||||
//
|
||||
// The caller is responsible for ensuring the metadata is valid and contains
|
||||
// the correct checksum before calling this method.
|
||||
// Use [SetFromTemplate] for automatic extraction and checksum computation.
|
||||
//
|
||||
// Returns the metadata and whether it was successfully cached (false if evicted).
|
||||
func (i *Index) Set(path string, metadata *Metadata) (*Metadata, bool) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
return i.cache.Set(path, metadata)
|
||||
}
|
||||
|
||||
// SetFromTemplate extracts metadata from a parsed template and stores it.
|
||||
//
|
||||
// Returns the metadata and whether it was successfully cached. The metadata is
|
||||
// always returned (even on checksum failure) for immediate filtering use.
|
||||
// Returns false if the metadata was not cached (e.g., set, evicted).
|
||||
func (i *Index) SetFromTemplate(path string, tpl *templates.Template) (*Metadata, bool) {
|
||||
metadata := NewMetadataFromTemplate(path, tpl)
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return metadata, false
|
||||
}
|
||||
metadata.ModTime = info.ModTime()
|
||||
|
||||
if i.cache == nil {
|
||||
return metadata, false
|
||||
}
|
||||
|
||||
return i.Set(path, metadata)
|
||||
}
|
||||
|
||||
// Has checks if metadata exists for a path without validation.
|
||||
func (i *Index) Has(path string) bool {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
_, found := i.cache.GetIfPresent(path)
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// Delete removes metadata for a path.
|
||||
func (i *Index) Delete(path string) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
i.cache.Invalidate(path)
|
||||
}
|
||||
|
||||
// Size returns the number of cached entries.
|
||||
func (i *Index) Size() int {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
return i.cache.EstimatedSize()
|
||||
}
|
||||
|
||||
// Clear removes all cached entries.
|
||||
func (i *Index) Clear() {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
i.cache.InvalidateAll()
|
||||
}
|
||||
|
||||
// Save persists the cache to disk using gob encoding.
|
||||
func (i *Index) Save() error {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
snapshot := &cacheSnapshot{
|
||||
Version: i.version,
|
||||
Data: make(map[string]*Metadata),
|
||||
}
|
||||
|
||||
maps.Insert(snapshot.Data, i.cache.All())
|
||||
|
||||
// NOTE(dwisiswant0): write to temp for atomic op.
|
||||
tmpFile := i.cacheFile + ".tmp"
|
||||
file, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoder := gob.NewEncoder(file)
|
||||
if err := encoder.Encode(snapshot); err != nil {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(tmpFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.Close(); err != nil {
|
||||
_ = os.Remove(tmpFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpFile, i.cacheFile); err != nil {
|
||||
_ = os.Remove(tmpFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load loads the cache from disk using gob decoding.
|
||||
func (i *Index) Load() error {
|
||||
file, err := os.Open(i.cacheFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var snapshot cacheSnapshot
|
||||
|
||||
decoder := gob.NewDecoder(file)
|
||||
if err := decoder.Decode(&snapshot); err != nil {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(i.cacheFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if snapshot.Version != i.version {
|
||||
_ = file.Close()
|
||||
_ = os.Remove(i.cacheFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
for key, value := range snapshot.Data {
|
||||
i.cache.Set(key, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter returns all template paths that match the given filter criteria.
|
||||
func (i *Index) Filter(filter *Filter) []string {
|
||||
if filter == nil || filter.IsEmpty() {
|
||||
return i.All()
|
||||
}
|
||||
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
var matched []string
|
||||
for path, metadata := range i.cache.All() {
|
||||
if filter.Matches(metadata) {
|
||||
matched = append(matched, path)
|
||||
}
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// FilterFunc returns all template paths that match the given filter function.
|
||||
func (i *Index) FilterFunc(fn FilterFunc) []string {
|
||||
if fn == nil {
|
||||
return i.All()
|
||||
}
|
||||
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
var matched []string
|
||||
for path, metadata := range i.cache.All() {
|
||||
if fn(metadata) {
|
||||
matched = append(matched, path)
|
||||
}
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// All returns all template paths in the index.
|
||||
func (i *Index) All() []string {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
paths := make([]string, 0, i.cache.EstimatedSize())
|
||||
for path := range i.cache.All() {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// GetAll returns all metadata entries in the index.
|
||||
func (i *Index) GetAll() map[string]*Metadata {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
result := maps.Collect(i.cache.All())
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Count returns the number of templates matching the filter.
|
||||
func (i *Index) Count(filter *Filter) int {
|
||||
if filter == nil || filter.IsEmpty() {
|
||||
return i.Size()
|
||||
}
|
||||
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
count := 0
|
||||
for _, metadata := range i.cache.All() {
|
||||
if filter.Matches(metadata) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
@@ -1,737 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/code"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewIndex(t *testing.T) {
|
||||
t.Run("with custom directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err, "Failed to create cache with custom directory")
|
||||
require.NotNil(t, cache, "Cache should not be nil")
|
||||
require.Equal(t, filepath.Join(tmpDir, IndexFileName), cache.cacheFile)
|
||||
require.Equal(t, IndexVersion, cache.version)
|
||||
})
|
||||
|
||||
t.Run("with default directory", func(t *testing.T) {
|
||||
cache, err := NewDefaultIndex()
|
||||
require.NoError(t, err, "Failed to create cache with default directory")
|
||||
require.NotNil(t, cache, "Cache should not be nil")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheBasicOperations(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "concurrent-test",
|
||||
FilePath: "/tmp/concurrent.yaml",
|
||||
}
|
||||
|
||||
t.Run("Set and Has", func(t *testing.T) {
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
require.Equal(t, 1, cache.Size(), "Cache size should be 1 after Set")
|
||||
require.True(t, cache.Has(metadata.FilePath), "Cache should contain the path after Set")
|
||||
require.False(t, cache.Has("/nonexistent"), "Cache should not contain nonexistent path")
|
||||
})
|
||||
|
||||
t.Run("Get with validation", func(t *testing.T) {
|
||||
// Get should fail validation for nonexistent file
|
||||
retrieved, found := cache.Get(metadata.FilePath)
|
||||
require.False(t, found, "Get should fail validation for nonexistent file")
|
||||
require.Nil(t, retrieved, "Retrieved metadata should be nil for invalid entry")
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
require.True(t, cache.Has(metadata.FilePath), "Cache should contain path before Delete")
|
||||
|
||||
cache.Delete(metadata.FilePath)
|
||||
require.False(t, cache.Has(metadata.FilePath), "Cache should not contain path after Delete")
|
||||
})
|
||||
|
||||
t.Run("Clear", func(t *testing.T) {
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
cache.Set("/tmp/test2.yaml", &Metadata{ID: "test2", FilePath: "/tmp/test2.yaml"})
|
||||
require.True(t, cache.Size() > 0, "Cache should have entries before Clear")
|
||||
|
||||
cache.Clear()
|
||||
require.Equal(t, 0, cache.Size(), "Cache should be empty after Clear")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCachePersistence(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
metadata1 := &Metadata{
|
||||
ID: "persist-test-1",
|
||||
FilePath: "/tmp/persist1.yaml",
|
||||
Name: "Persistence Test 1",
|
||||
Authors: []string{"tester"},
|
||||
Tags: []string{"test"},
|
||||
Severity: "medium",
|
||||
ProtocolType: "dns",
|
||||
}
|
||||
|
||||
metadata2 := &Metadata{
|
||||
ID: "persist-test-2",
|
||||
FilePath: "/tmp/persist2.yaml",
|
||||
Name: "Persistence Test 2",
|
||||
Authors: []string{"tester2"},
|
||||
Tags: []string{"cve"},
|
||||
Severity: "critical",
|
||||
ProtocolType: "http",
|
||||
}
|
||||
|
||||
t.Run("Save and Load", func(t *testing.T) {
|
||||
// Create cache and add entries
|
||||
cache1, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache1.Set(metadata1.FilePath, metadata1)
|
||||
cache1.Set(metadata2.FilePath, metadata2)
|
||||
require.Equal(t, 2, cache1.Size())
|
||||
|
||||
// Save to disk
|
||||
err = cache1.Save()
|
||||
require.NoError(t, err, "Failed to save cache")
|
||||
|
||||
// Verify cache file exists
|
||||
cacheFile := filepath.Join(tmpDir, IndexFileName)
|
||||
stat, err := os.Stat(cacheFile)
|
||||
require.NoError(t, err, "Cache file should exist")
|
||||
require.Greater(t, stat.Size(), int64(0), "Cache file should not be empty")
|
||||
|
||||
// Create new cache and load
|
||||
cache2, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, cache2.Size(), "New cache should be empty before Load")
|
||||
|
||||
err = cache2.Load()
|
||||
require.NoError(t, err, "Failed to load cache")
|
||||
|
||||
// Verify data was loaded
|
||||
require.Equal(t, 2, cache2.Size(), "Loaded cache should have 2 entries")
|
||||
require.True(t, cache2.Has(metadata1.FilePath), "Loaded cache should contain first entry")
|
||||
require.True(t, cache2.Has(metadata2.FilePath), "Loaded cache should contain second entry")
|
||||
})
|
||||
|
||||
t.Run("Load non-existent cache", func(t *testing.T) {
|
||||
emptyDir := t.TempDir()
|
||||
cache, err := NewIndex(emptyDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Loading non-existent cache should not error
|
||||
err = cache.Load()
|
||||
require.NoError(t, err, "Loading non-existent cache should not error")
|
||||
require.Equal(t, 0, cache.Size(), "Cache should be empty after loading non-existent file")
|
||||
})
|
||||
|
||||
t.Run("Atomic save", func(t *testing.T) {
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache.Set(metadata1.FilePath, metadata1)
|
||||
err = cache.Save()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify no .tmp file left behind
|
||||
tmpFile := filepath.Join(tmpDir, IndexFileName+".tmp")
|
||||
_, err = os.Stat(tmpFile)
|
||||
require.True(t, os.IsNotExist(err), "Temporary file should not exist after save")
|
||||
|
||||
// Verify actual cache file exists
|
||||
cacheFile := filepath.Join(tmpDir, IndexFileName)
|
||||
_, err = os.Stat(cacheFile)
|
||||
require.NoError(t, err, "Cache file should exist")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIndexVersionMismatch(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create cache with current version
|
||||
cache1, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "version-test",
|
||||
FilePath: "/tmp/version.yaml",
|
||||
}
|
||||
cache1.Set(metadata.FilePath, metadata)
|
||||
|
||||
// Save with current version
|
||||
err = cache1.Save()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Manually modify version and save again
|
||||
cache1.version = 999
|
||||
err = cache1.Save()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to load with different version
|
||||
cache2, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load should succeed but cache should be empty (version mismatch)
|
||||
err = cache2.Load()
|
||||
require.NoError(t, err, "Load should not error on version mismatch")
|
||||
require.Equal(t, 0, cache2.Size(), "Cache should be empty after version mismatch")
|
||||
}
|
||||
|
||||
func TestCacheCorruptedFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cacheFile := filepath.Join(tmpDir, IndexFileName)
|
||||
|
||||
// Create corrupted cache file
|
||||
err := os.WriteFile(cacheFile, []byte("corrupted data that is not valid gob"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to load corrupted cache
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cache.Load()
|
||||
require.NoError(t, err, "Load should not error on corrupted cache")
|
||||
require.Equal(t, 0, cache.Size(), "Cache should be empty after loading corrupted file")
|
||||
|
||||
// Corrupted file should be removed
|
||||
_, err = os.Stat(cacheFile)
|
||||
require.True(t, os.IsNotExist(err), "Corrupted cache file should be removed")
|
||||
}
|
||||
|
||||
func TestMetadataValidation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.yaml")
|
||||
|
||||
t.Run("Valid metadata", func(t *testing.T) {
|
||||
// Create a test file
|
||||
err := os.WriteFile(tmpFile, []byte("id: test\ninfo:\n name: Test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := os.Stat(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create metadata with correct checksum
|
||||
metadata := &Metadata{
|
||||
ID: "test",
|
||||
FilePath: tmpFile,
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
|
||||
// Should be valid
|
||||
require.True(t, metadata.IsValid(), "Metadata should be valid for unchanged file")
|
||||
})
|
||||
|
||||
t.Run("Invalid metadata after file modification", func(t *testing.T) {
|
||||
// Create the test file first to ensure it exists in this subtest
|
||||
err := os.WriteFile(tmpFile, []byte("id: test\ninfo:\n name: Test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set file ModTime to past to ensure modification is detectable
|
||||
oldTime := time.Now().Add(-2 * time.Second)
|
||||
err = os.Chtimes(tmpFile, oldTime, oldTime)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := os.Stat(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "test",
|
||||
FilePath: tmpFile,
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
|
||||
// Modify file
|
||||
err = os.WriteFile(tmpFile, []byte("id: test\ninfo:\n name: Modified"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should now be invalid
|
||||
require.False(t, metadata.IsValid(), "Metadata should be invalid after file modification")
|
||||
})
|
||||
|
||||
t.Run("Invalid metadata for deleted file", func(t *testing.T) {
|
||||
// Create the test file first to ensure it exists in this subtest
|
||||
err := os.WriteFile(tmpFile, []byte("id: test\ninfo:\n name: Test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := os.Stat(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "test",
|
||||
FilePath: tmpFile,
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
|
||||
// Delete file
|
||||
err = os.Remove(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be invalid
|
||||
require.False(t, metadata.IsValid(), "Metadata should be invalid for deleted file")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetFromTemplate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "extract.yaml")
|
||||
|
||||
// Create a test file
|
||||
err := os.WriteFile(tmpFile, []byte("id: extract-test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Basic metadata extraction", func(t *testing.T) {
|
||||
template := &templates.Template{
|
||||
ID: "extract-test",
|
||||
Info: model.Info{
|
||||
Name: "Extract Test Template",
|
||||
Authors: stringslice.StringSlice{Value: "author1,author2"},
|
||||
Tags: stringslice.StringSlice{Value: "tag1,tag2"},
|
||||
Description: "Test description",
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.High,
|
||||
},
|
||||
},
|
||||
SelfContained: true,
|
||||
Verified: true,
|
||||
TemplateVerifier: "test-verifier",
|
||||
}
|
||||
|
||||
metadata, ok := cache.SetFromTemplate(tmpFile, template)
|
||||
require.True(t, ok, "Failed to set metadata from template")
|
||||
require.NotNil(t, metadata, "Metadata should not be nil")
|
||||
|
||||
// Verify core fields
|
||||
require.Equal(t, "extract-test", metadata.ID)
|
||||
require.Equal(t, tmpFile, metadata.FilePath)
|
||||
|
||||
// Verify Info fields
|
||||
require.Equal(t, "Extract Test Template", metadata.Name)
|
||||
require.Equal(t, []string{"author1,author2"}, metadata.Authors)
|
||||
require.Equal(t, []string{"tag1,tag2"}, metadata.Tags)
|
||||
require.Equal(t, "high", metadata.Severity)
|
||||
|
||||
// Verify flags
|
||||
require.True(t, metadata.Verified)
|
||||
require.Equal(t, "test-verifier", metadata.TemplateVerifier)
|
||||
})
|
||||
|
||||
t.Run("HTTP protocol detection", func(t *testing.T) {
|
||||
// Create a separate test file for this test
|
||||
httpFile := filepath.Join(tmpDir, "http-test.yaml")
|
||||
err := os.WriteFile(httpFile, []byte("id: http-test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &templates.Template{
|
||||
ID: "http-test",
|
||||
Info: model.Info{
|
||||
Name: "HTTP Test",
|
||||
Authors: stringslice.StringSlice{Value: "tester"},
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Medium,
|
||||
},
|
||||
},
|
||||
RequestsHTTP: []*http.Request{{Method: http.HTTPMethodTypeHolder{MethodType: http.HTTPGet}}},
|
||||
}
|
||||
|
||||
metadata, ok := cache.SetFromTemplate(httpFile, template)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, metadata)
|
||||
require.Equal(t, "http", metadata.ProtocolType)
|
||||
})
|
||||
|
||||
t.Run("Extract with missing file", func(t *testing.T) {
|
||||
template := &templates.Template{
|
||||
ID: "missing-test",
|
||||
Info: model.Info{
|
||||
Name: "Missing File Test",
|
||||
Authors: stringslice.StringSlice{Value: "tester"},
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Low,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
metadata, ok := cache.SetFromTemplate("/nonexistent/file.yaml", template)
|
||||
require.False(t, ok, "Should return false for nonexistent file")
|
||||
require.NotNil(t, metadata, "Metadata should still be returned")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetadataMatchingHelpers(t *testing.T) {
|
||||
metadata := &Metadata{
|
||||
Tags: []string{"cve", "rce", "apache"},
|
||||
Authors: []string{"pdteam", "geeknik"},
|
||||
Severity: "critical",
|
||||
ProtocolType: "http",
|
||||
}
|
||||
|
||||
t.Run("HasTag", func(t *testing.T) {
|
||||
require.True(t, metadata.HasTag("cve"))
|
||||
require.True(t, metadata.HasTag("rce"))
|
||||
require.True(t, metadata.HasTag("apache"))
|
||||
require.False(t, metadata.HasTag("xxe"))
|
||||
require.False(t, metadata.HasTag(""))
|
||||
})
|
||||
|
||||
t.Run("HasAuthor", func(t *testing.T) {
|
||||
require.True(t, metadata.HasAuthor("pdteam"))
|
||||
require.True(t, metadata.HasAuthor("geeknik"))
|
||||
require.False(t, metadata.HasAuthor("unknown"))
|
||||
require.False(t, metadata.HasAuthor(""))
|
||||
})
|
||||
|
||||
t.Run("MatchesSeverity", func(t *testing.T) {
|
||||
require.True(t, metadata.MatchesSeverity(severity.Critical))
|
||||
require.False(t, metadata.MatchesSeverity(severity.High))
|
||||
require.False(t, metadata.MatchesSeverity(severity.Medium))
|
||||
require.False(t, metadata.MatchesSeverity(severity.Low))
|
||||
require.False(t, metadata.MatchesSeverity(severity.Info))
|
||||
})
|
||||
|
||||
t.Run("MatchesProtocol", func(t *testing.T) {
|
||||
require.True(t, metadata.MatchesProtocol(types.HTTPProtocol))
|
||||
require.False(t, metadata.MatchesProtocol(types.DNSProtocol))
|
||||
require.False(t, metadata.MatchesProtocol(types.FileProtocol))
|
||||
require.False(t, metadata.MatchesProtocol(types.NetworkProtocol))
|
||||
})
|
||||
|
||||
t.Run("Empty metadata", func(t *testing.T) {
|
||||
emptyMetadata := &Metadata{}
|
||||
require.False(t, emptyMetadata.HasTag("any"))
|
||||
require.False(t, emptyMetadata.HasAuthor("any"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheConcurrency(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test concurrent writes
|
||||
t.Run("Concurrent Set", func(t *testing.T) {
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(id int) {
|
||||
metadata := &Metadata{
|
||||
ID: string(rune('a' + id)),
|
||||
FilePath: filepath.Join("/tmp", string(rune('a'+id))+".yaml"),
|
||||
}
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
require.Equal(t, 10, cache.Size(), "All concurrent writes should succeed")
|
||||
})
|
||||
|
||||
// Test concurrent reads
|
||||
t.Run("Concurrent Has", func(t *testing.T) {
|
||||
metadata := &Metadata{
|
||||
ID: "concurrent-test",
|
||||
FilePath: "/tmp/concurrent.yaml",
|
||||
}
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 20; i++ {
|
||||
go func() {
|
||||
_ = cache.Has(metadata.FilePath)
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 20; i++ {
|
||||
<-done
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheSize(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, cache.Size(), "New cache should have size 0")
|
||||
|
||||
// Add entries
|
||||
for i := 0; i < 5; i++ {
|
||||
metadata := &Metadata{
|
||||
ID: string(rune('a' + i)),
|
||||
FilePath: filepath.Join("/tmp", string(rune('a'+i))+".yaml"),
|
||||
}
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
}
|
||||
|
||||
require.Equal(t, 5, cache.Size(), "Cache should have size 5 after adding 5 entries")
|
||||
|
||||
// Delete entries
|
||||
cache.Delete(filepath.Join("/tmp", "a.yaml"))
|
||||
cache.Delete(filepath.Join("/tmp", "b.yaml"))
|
||||
|
||||
require.Equal(t, 3, cache.Size(), "Cache should have size 3 after deleting 2 entries")
|
||||
|
||||
// Clear cache
|
||||
cache.Clear()
|
||||
require.Equal(t, 0, cache.Size(), "Cache should have size 0 after Clear")
|
||||
}
|
||||
|
||||
func TestCacheGetWithValidFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a real file for testing validation
|
||||
tmpFile := filepath.Join(tmpDir, "test.yaml")
|
||||
err = os.WriteFile(tmpFile, []byte("id: test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := os.Stat(tmpFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "test",
|
||||
FilePath: tmpFile,
|
||||
ModTime: info.ModTime(),
|
||||
Name: "Test Template",
|
||||
}
|
||||
|
||||
// Set and get should work with valid file
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
retrieved, found := cache.Get(metadata.FilePath)
|
||||
require.True(t, found, "Should find entry with valid file")
|
||||
require.NotNil(t, retrieved, "Retrieved metadata should not be nil")
|
||||
require.Equal(t, metadata.ID, retrieved.ID)
|
||||
}
|
||||
|
||||
func TestCacheSaveErrorHandling(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata := &Metadata{
|
||||
ID: "test",
|
||||
FilePath: filepath.Join("/tmp", "test.yaml"),
|
||||
}
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
|
||||
// Create a directory where the temp file would be created to force an error
|
||||
// The Save method creates a file at cacheFile + ".tmp"
|
||||
conflictPath := filepath.Join(tmpDir, IndexFileName+".tmp")
|
||||
err = os.Mkdir(conflictPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cache.Save()
|
||||
require.Error(t, err, "Save should fail when temp file cannot be created")
|
||||
}
|
||||
|
||||
func TestNewCacheWithInvalidDirectory(t *testing.T) {
|
||||
// Try to create cache in a file path (should fail)
|
||||
tmpFile := filepath.Join(t.TempDir(), "file.txt")
|
||||
err := os.WriteFile(tmpFile, []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cache, err := NewIndex(tmpFile)
|
||||
require.Error(t, err, "NewCache should fail when path is a file")
|
||||
require.Nil(t, cache, "Cache should be nil on error")
|
||||
}
|
||||
|
||||
func TestCacheLoadCorruptedRemoval(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cacheFile := filepath.Join(tmpDir, IndexFileName)
|
||||
|
||||
// Create corrupted cache file with invalid gob data
|
||||
err := os.WriteFile(cacheFile, []byte("this is not valid gob encoding at all!"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify file exists before Load
|
||||
_, err = os.Stat(cacheFile)
|
||||
require.NoError(t, err, "Corrupted file should exist")
|
||||
|
||||
// Load should not error but should remove corrupted file
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cache.Load()
|
||||
require.NoError(t, err, "Load should not return error for corrupted file")
|
||||
|
||||
// Verify corrupted file was removed
|
||||
_, err = os.Stat(cacheFile)
|
||||
require.True(t, os.IsNotExist(err), "Corrupted file should be removed")
|
||||
require.Equal(t, 0, cache.Size(), "Cache should be empty after loading corrupted file")
|
||||
}
|
||||
|
||||
func TestMetadataExtractionWithNilClassification(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.yaml")
|
||||
err := os.WriteFile(tmpFile, []byte("id: test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &templates.Template{
|
||||
ID: "nil-classification",
|
||||
Info: model.Info{
|
||||
Name: "Template without classification",
|
||||
Authors: stringslice.StringSlice{Value: "tester"},
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Medium,
|
||||
},
|
||||
Classification: nil, // Explicitly nil
|
||||
},
|
||||
}
|
||||
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata, ok := cache.SetFromTemplate(tmpFile, template)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, metadata)
|
||||
}
|
||||
|
||||
func TestCachePersistenceWithLargeDataset(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add 100 entries to test bulk operations
|
||||
for i := 0; i < 100; i++ {
|
||||
metadata := &Metadata{
|
||||
ID: fmt.Sprintf("template-%d", i),
|
||||
FilePath: filepath.Join("/tmp", fmt.Sprintf("template-%d.yaml", i)),
|
||||
Name: fmt.Sprintf("Template %d", i),
|
||||
Authors: []string{fmt.Sprintf("author%d", i)},
|
||||
Tags: []string{"tag1", "tag2", "tag3"},
|
||||
Severity: "high",
|
||||
}
|
||||
cache.Set(metadata.FilePath, metadata)
|
||||
}
|
||||
|
||||
require.Equal(t, 100, cache.Size(), "Cache should contain 100 entries")
|
||||
|
||||
// Save to disk
|
||||
err = cache.Save()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load into new cache
|
||||
cache2, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
err = cache2.Load()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 100, cache2.Size(), "Loaded cache should contain 100 entries")
|
||||
|
||||
// Verify a sample entry
|
||||
found := cache2.Has(filepath.Join("/tmp", "template-50.yaml"))
|
||||
require.True(t, found, "Should find sample entry")
|
||||
}
|
||||
|
||||
func TestMetadataHelperMethods(t *testing.T) {
|
||||
metadata := &Metadata{
|
||||
ID: "helper-test",
|
||||
Tags: []string{},
|
||||
Authors: []string{},
|
||||
Severity: "",
|
||||
ProtocolType: "",
|
||||
}
|
||||
|
||||
t.Run("Empty tags", func(t *testing.T) {
|
||||
require.False(t, metadata.HasTag("anytag"))
|
||||
})
|
||||
|
||||
t.Run("Empty authors", func(t *testing.T) {
|
||||
require.False(t, metadata.HasAuthor("anyauthor"))
|
||||
})
|
||||
|
||||
t.Run("Empty severity", func(t *testing.T) {
|
||||
require.False(t, metadata.MatchesSeverity(severity.Critical))
|
||||
})
|
||||
|
||||
t.Run("Empty protocol", func(t *testing.T) {
|
||||
require.False(t, metadata.MatchesProtocol(types.HTTPProtocol))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleProtocolsDetection(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "multi.yaml")
|
||||
err := os.WriteFile(tmpFile, []byte("id: multi"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Template with multiple protocol types
|
||||
template := &templates.Template{
|
||||
ID: "multi-protocol",
|
||||
Info: model.Info{
|
||||
Name: "Multi Protocol Template",
|
||||
Authors: stringslice.StringSlice{Value: "tester"},
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.High,
|
||||
},
|
||||
},
|
||||
RequestsHTTP: []*http.Request{{Method: http.HTTPMethodTypeHolder{MethodType: http.HTTPGet}}},
|
||||
RequestsHeadless: []*headless.Request{{}},
|
||||
RequestsCode: []*code.Request{{}},
|
||||
}
|
||||
|
||||
cache, err := NewIndex(tmpDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata, ok := cache.SetFromTemplate(tmpFile, template)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, metadata)
|
||||
require.Equal(t, "http", metadata.ProtocolType, "Primary protocol should be http")
|
||||
}
|
||||
|
||||
func TestNewMetadataFromTemplate(t *testing.T) {
|
||||
tmpl := &templates.Template{
|
||||
ID: "test-template",
|
||||
Info: model.Info{
|
||||
Name: "Test Template",
|
||||
Authors: stringslice.StringSlice{Value: []string{"author"}},
|
||||
Tags: stringslice.StringSlice{Value: []string{"tag"}},
|
||||
SeverityHolder: severity.Holder{
|
||||
Severity: severity.Low,
|
||||
},
|
||||
},
|
||||
Verified: true,
|
||||
TemplateVerifier: "verifier",
|
||||
}
|
||||
|
||||
path := "/tmp/test.yaml"
|
||||
metadata := NewMetadataFromTemplate(path, tmpl)
|
||||
|
||||
require.Equal(t, tmpl.ID, metadata.ID)
|
||||
require.Equal(t, path, metadata.FilePath)
|
||||
require.Equal(t, tmpl.Info.Name, metadata.Name)
|
||||
require.Equal(t, tmpl.Info.Authors.ToSlice(), metadata.Authors)
|
||||
require.Equal(t, tmpl.Info.Tags.ToSlice(), metadata.Tags)
|
||||
require.Equal(t, tmpl.Info.SeverityHolder.Severity.String(), metadata.Severity)
|
||||
require.Equal(t, tmpl.Type().String(), metadata.ProtocolType)
|
||||
require.Equal(t, tmpl.Verified, metadata.Verified)
|
||||
require.Equal(t, tmpl.TemplateVerifier, metadata.TemplateVerifier)
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
)
|
||||
|
||||
// Metadata contains lightweight metadata extracted from a template.
|
||||
type Metadata struct {
|
||||
// ID is the unique identifier of the template.
|
||||
ID string `gob:"id"`
|
||||
|
||||
// FilePath is the path to the template file.
|
||||
FilePath string `gob:"file_path"`
|
||||
|
||||
// ModTime is the modification time of the template file.
|
||||
ModTime time.Time `gob:"mod_time"`
|
||||
|
||||
// Name is the name of the template.
|
||||
Name string `gob:"name"`
|
||||
|
||||
// Authors are the authors of the template.
|
||||
Authors []string `gob:"authors"`
|
||||
|
||||
// Tags are the tags associated with the template.
|
||||
Tags []string `gob:"tags"`
|
||||
|
||||
// Severity is the severity level of the template.
|
||||
Severity string `gob:"severity"`
|
||||
|
||||
// ProtocolType is the primary protocol type of the template.
|
||||
ProtocolType string `gob:"protocol_type"`
|
||||
|
||||
// Verified indicates whether the template is verified.
|
||||
Verified bool `gob:"verified"`
|
||||
|
||||
// TemplateVerifier is the verifier used for the template.
|
||||
TemplateVerifier string `gob:"verifier,omitempty"`
|
||||
|
||||
// NOTE(dwisiswant0): Consider adding more fields here in the future to
|
||||
// enhance filtering caps w/o loading full templates, such as:
|
||||
// `has_{code,headless,file}` to indicate presence of protocol-based
|
||||
// requests, and/or classification fields (CVE, CWE, CVSS, EPSS), if needed.
|
||||
//
|
||||
// For maintainers: when adding new fields, don't forget to update the
|
||||
// Weigher logic in [NewIndex] to account for the new fields in cache weight
|
||||
// calculation, because it affects cache eviction behavior. Also, consider
|
||||
// the impact on existing cached data and whether a [IndexVersion] bump is
|
||||
// needed.
|
||||
}
|
||||
|
||||
// NewMetadataFromTemplate creates a new metadata object from a template.
|
||||
func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {
|
||||
return &Metadata{
|
||||
ID: tpl.ID,
|
||||
FilePath: path,
|
||||
|
||||
Name: tpl.Info.Name,
|
||||
Authors: tpl.Info.Authors.ToSlice(),
|
||||
Tags: tpl.Info.Tags.ToSlice(),
|
||||
Severity: tpl.Info.SeverityHolder.Severity.String(),
|
||||
|
||||
ProtocolType: tpl.Type().String(),
|
||||
|
||||
Verified: tpl.Verified,
|
||||
TemplateVerifier: tpl.TemplateVerifier,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid checks if the cached metadata is still valid by comparing the file
|
||||
// modification time.
|
||||
func (m *Metadata) IsValid() bool {
|
||||
info, err := os.Stat(m.FilePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.ModTime.Equal(info.ModTime())
|
||||
}
|
||||
|
||||
// MatchesSeverity checks if the metadata matches the given severity.
|
||||
func (m *Metadata) MatchesSeverity(sev severity.Severity) bool {
|
||||
return m.Severity == sev.String()
|
||||
}
|
||||
|
||||
// MatchesProtocol checks if the metadata matches the given protocol type.
|
||||
func (m *Metadata) MatchesProtocol(protocolType types.ProtocolType) bool {
|
||||
return m.ProtocolType == protocolType.String()
|
||||
}
|
||||
|
||||
// HasTag checks if the metadata contains the given tag.
|
||||
func (m *Metadata) HasTag(tag string) bool {
|
||||
return slices.Contains(m.Tags, tag)
|
||||
}
|
||||
|
||||
// HasAuthor checks if the metadata contains the given author.
|
||||
func (m *Metadata) HasAuthor(author string) bool {
|
||||
return slices.Contains(m.Authors, author)
|
||||
}
|
||||
+43
-242
@@ -7,14 +7,13 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/index"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader/filter"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/keys"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
@@ -26,7 +25,6 @@ import (
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
mapsutil "github.com/projectdiscovery/utils/maps"
|
||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
syncutil "github.com/projectdiscovery/utils/sync"
|
||||
@@ -77,6 +75,7 @@ type Config struct {
|
||||
type Store struct {
|
||||
id string // id of the store (optional)
|
||||
tagFilter *templates.TagFilter
|
||||
pathFilter *filter.PathFilter
|
||||
config *Config
|
||||
finalTemplates []string
|
||||
finalWorkflows []string
|
||||
@@ -88,19 +87,6 @@ type Store struct {
|
||||
|
||||
logger *gologger.Logger
|
||||
|
||||
// parserCacheOnce is used to cache the parser cache result
|
||||
parserCacheOnce func() *templates.Cache
|
||||
|
||||
// metadataIndex is the template metadata cache
|
||||
metadataIndex *index.Index
|
||||
|
||||
// indexFilter is the cached filter for metadata matching
|
||||
indexFilter *index.Filter
|
||||
|
||||
// saveTemplatesIndexOnce is used to ensure we only save the metadata index
|
||||
// once
|
||||
saveMetadataIndexOnce func()
|
||||
|
||||
// NotFoundCallback is called for each not found template
|
||||
// This overrides error handling for not found templates
|
||||
NotFoundCallback func(template string) bool
|
||||
@@ -138,10 +124,17 @@ func NewConfig(options *types.Options, catalog catalog.Catalog, executerOpts *pr
|
||||
|
||||
// New creates a new template store based on provided configuration
|
||||
func New(cfg *Config) (*Store, error) {
|
||||
// tagFilter only for IncludeConditions (advanced filtering).
|
||||
// All other filtering (tags, authors, severities, IDs, protocols, paths) is
|
||||
// handled by [index.Filter].
|
||||
tagFilter, err := templates.NewTagFilter(&templates.TagFilterConfig{
|
||||
Tags: cfg.Tags,
|
||||
ExcludeTags: cfg.ExcludeTags,
|
||||
Authors: cfg.Authors,
|
||||
Severities: cfg.Severities,
|
||||
ExcludeSeverities: cfg.ExcludeSeverities,
|
||||
IncludeTags: cfg.IncludeTags,
|
||||
IncludeIds: cfg.IncludeIds,
|
||||
ExcludeIds: cfg.ExcludeIds,
|
||||
Protocols: cfg.Protocols,
|
||||
ExcludeProtocols: cfg.ExcludeProtocols,
|
||||
IncludeConditions: cfg.IncludeConditions,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -149,41 +142,18 @@ func New(cfg *Config) (*Store, error) {
|
||||
}
|
||||
|
||||
store := &Store{
|
||||
id: cfg.StoreId,
|
||||
config: cfg,
|
||||
tagFilter: tagFilter,
|
||||
id: cfg.StoreId,
|
||||
config: cfg,
|
||||
tagFilter: tagFilter,
|
||||
pathFilter: filter.NewPathFilter(&filter.PathFilterConfig{
|
||||
IncludedTemplates: cfg.IncludeTemplates,
|
||||
ExcludedTemplates: cfg.ExcludeTemplates,
|
||||
}, cfg.Catalog),
|
||||
finalTemplates: cfg.Templates,
|
||||
finalWorkflows: cfg.Workflows,
|
||||
logger: cfg.Logger,
|
||||
}
|
||||
|
||||
store.parserCacheOnce = sync.OnceValue(func() *templates.Cache {
|
||||
if cfg.ExecutorOptions == nil || cfg.ExecutorOptions.Parser == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if parser, ok := cfg.ExecutorOptions.Parser.(*templates.Parser); ok {
|
||||
return parser.Cache()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Initialize metadata index and filter (load from disk & cache for reuse)
|
||||
store.metadataIndex = store.loadTemplatesIndex()
|
||||
store.indexFilter = store.buildIndexFilter()
|
||||
store.saveMetadataIndexOnce = sync.OnceFunc(func() {
|
||||
if store.metadataIndex == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.metadataIndex.Save(); err != nil {
|
||||
store.logger.Warning().Msgf("Could not save metadata cache: %v", err)
|
||||
} else {
|
||||
store.logger.Verbose().Msgf("Saved %d templates to metadata cache", store.metadataIndex.Size())
|
||||
}
|
||||
})
|
||||
|
||||
// Do a check to see if we have URLs in templates flag, if so
|
||||
// we need to processs them separately and remove them from the initial list
|
||||
var templatesFinal []string
|
||||
@@ -315,102 +285,17 @@ func init() {
|
||||
templateIDPathMap = make(map[string]string)
|
||||
}
|
||||
|
||||
// buildIndexFilter creates an [index.Filter] from the store configuration.
|
||||
// This filter handles all basic filtering (paths, tags, authors, severities,
|
||||
// IDs, protocols). Advanced IncludeConditions filtering is handled separately
|
||||
// by tagFilter.
|
||||
func (store *Store) buildIndexFilter() *index.Filter {
|
||||
includeTemplates, _ := store.config.Catalog.GetTemplatesPath(store.config.IncludeTemplates)
|
||||
excludeTemplates, _ := store.config.Catalog.GetTemplatesPath(store.config.ExcludeTemplates)
|
||||
|
||||
return &index.Filter{
|
||||
Authors: store.config.Authors,
|
||||
Tags: store.config.Tags,
|
||||
ExcludeTags: store.config.ExcludeTags,
|
||||
IncludeTags: store.config.IncludeTags,
|
||||
IDs: store.config.IncludeIds,
|
||||
ExcludeIDs: store.config.ExcludeIds,
|
||||
IncludeTemplates: includeTemplates,
|
||||
ExcludeTemplates: excludeTemplates,
|
||||
Severities: []severity.Severity(store.config.Severities),
|
||||
ExcludeSeverities: []severity.Severity(store.config.ExcludeSeverities),
|
||||
ProtocolTypes: []templateTypes.ProtocolType(store.config.Protocols),
|
||||
ExcludeProtocolTypes: []templateTypes.ProtocolType(store.config.ExcludeProtocols),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) loadTemplatesIndex() *index.Index {
|
||||
var metadataIdx *index.Index
|
||||
|
||||
idx, err := index.NewDefaultIndex()
|
||||
if err != nil {
|
||||
store.logger.Warning().Msgf("Could not create metadata cache: %v", err)
|
||||
} else {
|
||||
metadataIdx = idx
|
||||
if err := metadataIdx.Load(); err != nil {
|
||||
store.logger.Warning().Msgf("Could not load metadata cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return metadataIdx
|
||||
}
|
||||
|
||||
// LoadTemplatesOnlyMetadata loads only the metadata of the templates
|
||||
func (store *Store) LoadTemplatesOnlyMetadata() error {
|
||||
defer store.saveMetadataIndexOnce()
|
||||
|
||||
templatePaths, errs := store.config.Catalog.GetTemplatesPath(store.finalTemplates)
|
||||
store.logErroredTemplates(errs)
|
||||
|
||||
indexFilter := store.indexFilter
|
||||
filteredTemplatePaths := store.pathFilter.Match(templatePaths)
|
||||
|
||||
validPaths := make(map[string]struct{})
|
||||
|
||||
for _, templatePath := range templatePaths {
|
||||
if store.metadataIndex != nil {
|
||||
if metadata, found := store.metadataIndex.Get(templatePath); found {
|
||||
if !indexFilter.Matches(metadata) {
|
||||
continue
|
||||
}
|
||||
|
||||
if store.tagFilter != nil {
|
||||
loaded, err := store.config.ExecutorOptions.Parser.LoadTemplate(templatePath, store.tagFilter, nil, store.config.Catalog)
|
||||
if !loaded {
|
||||
if err != nil && strings.Contains(err.Error(), templates.ErrExcluded.Error()) {
|
||||
stats.Increment(templates.TemplatesExcludedStats)
|
||||
if config.DefaultConfig.LogAllEvents {
|
||||
store.logger.Print().Msgf("[%v] %v\n", aurora.Yellow("WRN").String(), err.Error())
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
validPaths[templatePath] = struct{}{}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for templatePath := range filteredTemplatePaths {
|
||||
loaded, err := store.config.ExecutorOptions.Parser.LoadTemplate(templatePath, store.tagFilter, nil, store.config.Catalog)
|
||||
if loaded {
|
||||
templatesCache := store.parserCacheOnce()
|
||||
if templatesCache != nil {
|
||||
if template, _, _ := templatesCache.Has(templatePath); template != nil {
|
||||
var metadata *index.Metadata
|
||||
if store.metadataIndex != nil {
|
||||
metadata, _ = store.metadataIndex.SetFromTemplate(templatePath, template)
|
||||
} else {
|
||||
metadata = index.NewMetadataFromTemplate(templatePath, template)
|
||||
}
|
||||
|
||||
if !indexFilter.Matches(metadata) {
|
||||
continue
|
||||
}
|
||||
|
||||
validPaths[templatePath] = struct{}{}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if loaded || store.pathFilter.MatchIncluded(templatePath) {
|
||||
validPaths[templatePath] = struct{}{}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -424,13 +309,11 @@ func (store *Store) LoadTemplatesOnlyMetadata() error {
|
||||
store.logger.Warning().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
templatesCache := store.parserCacheOnce()
|
||||
if templatesCache == nil {
|
||||
parserItem, ok := store.config.ExecutorOptions.Parser.(*templates.Parser)
|
||||
if !ok {
|
||||
return errors.New("invalid parser")
|
||||
}
|
||||
|
||||
loadedTemplateIDs := mapsutil.NewSyncLockMap[string, struct{}]()
|
||||
templatesCache := parserItem.Cache()
|
||||
|
||||
for templatePath := range validPaths {
|
||||
template, _, _ := templatesCache.Has(templatePath)
|
||||
@@ -456,12 +339,6 @@ func (store *Store) LoadTemplatesOnlyMetadata() error {
|
||||
}
|
||||
|
||||
if template != nil {
|
||||
if loadedTemplateIDs.Has(template.ID) {
|
||||
store.logger.Debug().Msgf("Skipping duplicate template ID '%s' from path '%s'", template.ID, templatePath)
|
||||
continue
|
||||
}
|
||||
|
||||
_ = loadedTemplateIDs.Set(template.ID, struct{}{})
|
||||
template.Path = templatePath
|
||||
store.templates = append(store.templates, template)
|
||||
}
|
||||
@@ -474,24 +351,15 @@ func (store *Store) LoadTemplatesOnlyMetadata() error {
|
||||
func (store *Store) ValidateTemplates() error {
|
||||
templatePaths, errs := store.config.Catalog.GetTemplatesPath(store.finalTemplates)
|
||||
store.logErroredTemplates(errs)
|
||||
|
||||
workflowPaths, errs := store.config.Catalog.GetTemplatesPath(store.finalWorkflows)
|
||||
store.logErroredTemplates(errs)
|
||||
|
||||
templatePathsMap := make(map[string]struct{}, len(templatePaths))
|
||||
for _, path := range templatePaths {
|
||||
templatePathsMap[path] = struct{}{}
|
||||
}
|
||||
filteredTemplatePaths := store.pathFilter.Match(templatePaths)
|
||||
filteredWorkflowPaths := store.pathFilter.Match(workflowPaths)
|
||||
|
||||
workflowPathsMap := make(map[string]struct{}, len(workflowPaths))
|
||||
for _, path := range workflowPaths {
|
||||
workflowPathsMap[path] = struct{}{}
|
||||
}
|
||||
|
||||
if store.areTemplatesValid(templatePathsMap) && store.areWorkflowsValid(workflowPathsMap) {
|
||||
if store.areTemplatesValid(filteredTemplatePaths) && store.areWorkflowsValid(filteredWorkflowPaths) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("errors occurred during template validation")
|
||||
}
|
||||
|
||||
@@ -509,7 +377,6 @@ func (store *Store) areTemplatesValid(filteredTemplatePaths map[string]struct{})
|
||||
|
||||
func (store *Store) areWorkflowOrTemplatesValid(filteredTemplatePaths map[string]struct{}, isWorkflow bool, load func(templatePath string, tagFilter *templates.TagFilter) (bool, error)) bool {
|
||||
areTemplatesValid := true
|
||||
parsedCache := store.parserCacheOnce()
|
||||
|
||||
for templatePath := range filteredTemplatePaths {
|
||||
if _, err := load(templatePath, store.tagFilter); err != nil {
|
||||
@@ -519,26 +386,13 @@ func (store *Store) areWorkflowOrTemplatesValid(filteredTemplatePaths map[string
|
||||
}
|
||||
}
|
||||
|
||||
var template *templates.Template
|
||||
var err error
|
||||
|
||||
if parsedCache != nil {
|
||||
if cachedTemplate, _, cacheErr := parsedCache.Has(templatePath); cacheErr == nil && cachedTemplate != nil {
|
||||
template = cachedTemplate
|
||||
template, err := templates.Parse(templatePath, store.preprocessor, store.config.ExecutorOptions)
|
||||
if err != nil {
|
||||
if isParsingError(store, "Error occurred parsing template %s: %s\n", templatePath, err) {
|
||||
areTemplatesValid = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if template == nil {
|
||||
template, err = templates.Parse(templatePath, store.preprocessor, store.config.ExecutorOptions)
|
||||
if err != nil {
|
||||
if isParsingError(store, "Error occurred parsing template %s: %s\n", templatePath, err) {
|
||||
areTemplatesValid = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if template == nil {
|
||||
} else if template == nil {
|
||||
// NOTE(dwisiswant0): possibly global matchers template.
|
||||
// This could definitely be handled better, for example by returning an
|
||||
// `ErrGlobalMatchersTemplate` during `templates.Parse` and checking it
|
||||
@@ -610,9 +464,10 @@ func (store *Store) LoadTemplates(templatesList []string) []*templates.Template
|
||||
func (store *Store) LoadWorkflows(workflowsList []string) []*templates.Template {
|
||||
includedWorkflows, errs := store.config.Catalog.GetTemplatesPath(workflowsList)
|
||||
store.logErroredTemplates(errs)
|
||||
workflowPathMap := store.pathFilter.Match(includedWorkflows)
|
||||
|
||||
loadedWorkflows := make([]*templates.Template, 0, len(includedWorkflows))
|
||||
for _, workflowPath := range includedWorkflows {
|
||||
loadedWorkflows := make([]*templates.Template, 0, len(workflowPathMap))
|
||||
for workflowPath := range workflowPathMap {
|
||||
loaded, err := store.config.ExecutorOptions.Parser.LoadWorkflow(workflowPath, store.config.Catalog)
|
||||
if err != nil {
|
||||
store.logger.Warning().Msgf("Could not load workflow %s: %s\n", workflowPath, err)
|
||||
@@ -632,24 +487,13 @@ func (store *Store) LoadWorkflows(workflowsList []string) []*templates.Template
|
||||
// LoadTemplatesWithTags takes a list of templates and extra tags
|
||||
// returning templates that match.
|
||||
func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templates.Template {
|
||||
defer store.saveMetadataIndexOnce()
|
||||
|
||||
indexFilter := store.indexFilter
|
||||
|
||||
includedTemplates, errs := store.config.Catalog.GetTemplatesPath(templatesList)
|
||||
store.logErroredTemplates(errs)
|
||||
templatePathMap := store.pathFilter.Match(includedTemplates)
|
||||
|
||||
loadedTemplates := sliceutil.NewSyncSlice[*templates.Template]()
|
||||
loadedTemplateIDs := mapsutil.NewSyncLockMap[string, struct{}]()
|
||||
|
||||
loadTemplate := func(tmpl *templates.Template) {
|
||||
if loadedTemplateIDs.Has(tmpl.ID) {
|
||||
store.logger.Debug().Msgf("Skipping duplicate template ID '%s' from path '%s'", tmpl.ID, tmpl.Path)
|
||||
return
|
||||
}
|
||||
|
||||
_ = loadedTemplateIDs.Set(tmpl.ID, struct{}{})
|
||||
|
||||
loadedTemplates.Append(tmpl)
|
||||
// increment signed/unsigned counters
|
||||
if tmpl.Verified {
|
||||
@@ -663,11 +507,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
|
||||
}
|
||||
}
|
||||
|
||||
concurrency := store.config.ExecutorOptions.Options.TemplateLoadingConcurrency
|
||||
if concurrency <= 0 {
|
||||
concurrency = types.DefaultTemplateLoadingConcurrency
|
||||
}
|
||||
wgLoadTemplates, errWg := syncutil.New(syncutil.WithSize(concurrency))
|
||||
wgLoadTemplates, errWg := syncutil.New(syncutil.WithSize(50))
|
||||
if errWg != nil {
|
||||
panic("could not create wait group")
|
||||
}
|
||||
@@ -681,46 +521,14 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
|
||||
panic("dialers with executionId " + store.config.ExecutorOptions.Options.ExecutionId + " not found")
|
||||
}
|
||||
|
||||
for _, templatePath := range includedTemplates {
|
||||
for templatePath := range templatePathMap {
|
||||
wgLoadTemplates.Add()
|
||||
go func(templatePath string) {
|
||||
defer wgLoadTemplates.Done()
|
||||
|
||||
var (
|
||||
metadata *index.Metadata
|
||||
metadataCached bool
|
||||
)
|
||||
|
||||
if store.metadataIndex != nil {
|
||||
if cachedMetadata, found := store.metadataIndex.Get(templatePath); found {
|
||||
metadata = cachedMetadata
|
||||
if !indexFilter.Matches(metadata) {
|
||||
return
|
||||
}
|
||||
// NOTE(dwisiswant0): else, tagFilter probably exists (for
|
||||
// IncludeConditions), which still need to check via
|
||||
// LoadTemplate.
|
||||
|
||||
metadataCached = true
|
||||
}
|
||||
}
|
||||
|
||||
loaded, err := store.config.ExecutorOptions.Parser.LoadTemplate(templatePath, store.tagFilter, tags, store.config.Catalog)
|
||||
if loaded {
|
||||
if loaded || store.pathFilter.MatchIncluded(templatePath) {
|
||||
parsed, err := templates.Parse(templatePath, store.preprocessor, store.config.ExecutorOptions)
|
||||
|
||||
if parsed != nil && !metadataCached {
|
||||
if store.metadataIndex != nil {
|
||||
metadata, _ = store.metadataIndex.SetFromTemplate(templatePath, parsed)
|
||||
} else {
|
||||
metadata = index.NewMetadataFromTemplate(templatePath, parsed)
|
||||
}
|
||||
|
||||
if metadata != nil && !indexFilter.Matches(metadata) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// exclude templates not compatible with offline matching from total runtime warning stats
|
||||
if !errors.Is(err, templates.ErrIncompatibleWithOfflineMatching) {
|
||||
@@ -755,14 +563,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) []*templ
|
||||
// check if the template is a DAST template
|
||||
// also allow global matchers template to be loaded
|
||||
if parsed.IsFuzzing() || parsed.Options.GlobalMatchers != nil && parsed.Options.GlobalMatchers.HasMatchers() {
|
||||
if len(parsed.RequestsHeadless) > 0 && !store.config.ExecutorOptions.Options.Headless {
|
||||
stats.Increment(templates.ExcludedHeadlessTmplStats)
|
||||
if config.DefaultConfig.LogAllEvents {
|
||||
store.logger.Print().Msgf("[%v] Headless flag is required for headless template '%s'.\n", aurora.Yellow("WRN").String(), templatePath)
|
||||
}
|
||||
} else {
|
||||
loadTemplate(parsed)
|
||||
}
|
||||
loadTemplate(parsed)
|
||||
}
|
||||
} else if len(parsed.RequestsHeadless) > 0 && !store.config.ExecutorOptions.Options.Headless {
|
||||
// donot include headless template in final list if headless flag is not set
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
package loader_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
|
||||
)
|
||||
|
||||
func BenchmarkStoreValidateTemplates(b *testing.B) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Logger = &gologger.Logger{}
|
||||
testutils.Init(options)
|
||||
|
||||
catalog := disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
|
||||
workflowLoader, err := workflow.NewLoader(executerOpts)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create workflow loader: %s", err)
|
||||
}
|
||||
executerOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
loaderCfg := loader.NewConfig(options, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.ValidateTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoadTemplates(b *testing.B) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Logger = &gologger.Logger{}
|
||||
options.ExecutionId = "bench-load-templates"
|
||||
testutils.Init(options)
|
||||
|
||||
catalog := disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
|
||||
workflowLoader, err := workflow.NewLoader(executerOpts)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create workflow loader: %s", err)
|
||||
}
|
||||
executerOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
b.Run("NoFilter", func(b *testing.B) {
|
||||
loaderCfg := loader.NewConfig(options, catalog, executerOpts)
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterBySeverityCritical", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Severities = severity.Severities{severity.Critical}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterBySeverityHighCritical", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Severities = severity.Severities{severity.High, severity.Critical}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterByAuthor", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Authors = []string{"pdteam"}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterByTags", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Tags = []string{"cve", "rce"}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterByProtocol", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Protocols = templateTypes.ProtocolTypes{templateTypes.HTTPProtocol}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ComplexFilter", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Severities = severity.Severities{severity.High, severity.Critical}
|
||||
opts.Authors = []string{"pdteam"}
|
||||
opts.Tags = []string{"cve"}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplates([]string{config.DefaultConfig.TemplatesDirectory})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkLoadTemplatesOnlyMetadata(b *testing.B) {
|
||||
options := testutils.DefaultOptions.Copy()
|
||||
options.Logger = &gologger.Logger{}
|
||||
options.ExecutionId = "bench-metadata"
|
||||
testutils.Init(options)
|
||||
|
||||
catalog := disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
|
||||
executerOpts := testutils.NewMockExecuterOptions(options, nil)
|
||||
executerOpts.Parser = templates.NewParser()
|
||||
|
||||
workflowLoader, err := workflow.NewLoader(executerOpts)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create workflow loader: %s", err)
|
||||
}
|
||||
executerOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
b.Run("WithoutFilter", func(b *testing.B) {
|
||||
loaderCfg := loader.NewConfig(options, catalog, executerOpts)
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
// Pre-warm the cache
|
||||
_ = store.LoadTemplatesOnlyMetadata()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplatesOnlyMetadata()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("WithSeverityFilter", func(b *testing.B) {
|
||||
opts := options.Copy()
|
||||
opts.Severities = severity.Severities{severity.Critical}
|
||||
loaderCfg := loader.NewConfig(opts, catalog, executerOpts)
|
||||
|
||||
store, err := loader.New(loaderCfg)
|
||||
if err != nil {
|
||||
b.Fatalf("could not create store: %s", err)
|
||||
}
|
||||
|
||||
// Pre-warm the cache
|
||||
_ = store.LoadTemplatesOnlyMetadata()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
_ = store.LoadTemplatesOnlyMetadata()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func getRemoteContent(URL string, remoteTemplateDomainList []string, contentType
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
if response.StatusCode < 200 || response.StatusCode > 299 {
|
||||
return RemoteContent{Error: fmt.Errorf("get \"%s\": unexpected status %d", URL, response.StatusCode)}
|
||||
return RemoteContent{Error: fmt.Errorf("get \"%s\": unexpect status %d", URL, response.StatusCode)}
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(response.Body)
|
||||
|
||||
@@ -109,6 +109,7 @@ func (e *Engine) executeTemplateSpray(ctx context.Context, templatesList []*temp
|
||||
defer wp.Wait()
|
||||
|
||||
for _, template := range templatesList {
|
||||
template := template
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
+56
-73
@@ -48,15 +48,8 @@ func (e *Engine) executeAllSelfContained(ctx context.Context, alltemplates []*te
|
||||
|
||||
// executeTemplateWithTargets executes a given template on x targets (with a internal targetpool(i.e concurrency))
|
||||
func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templates.Template, target provider.InputProvider, results *atomic.Bool) {
|
||||
if e.workPool == nil {
|
||||
e.workPool = e.GetWorkPool()
|
||||
}
|
||||
// Bounded worker pool using input concurrency
|
||||
pool := e.workPool.InputPool(template.Type())
|
||||
workerCount := 1
|
||||
if pool != nil && pool.Size > 0 {
|
||||
workerCount = pool.Size
|
||||
}
|
||||
// this is target pool i.e max target to execute
|
||||
wg := e.workPool.InputPool(template.Type())
|
||||
|
||||
var (
|
||||
index uint32
|
||||
@@ -85,41 +78,6 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
currentInfo.Unlock()
|
||||
}
|
||||
|
||||
// task represents a single target execution unit
|
||||
type task struct {
|
||||
index uint32
|
||||
skip bool
|
||||
value *contextargs.MetaInput
|
||||
}
|
||||
|
||||
tasks := make(chan task)
|
||||
var workersWg sync.WaitGroup
|
||||
workersWg.Add(workerCount)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
go func() {
|
||||
defer workersWg.Done()
|
||||
for t := range tasks {
|
||||
func() {
|
||||
defer cleanupInFlight(t.index)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
if t.skip {
|
||||
return
|
||||
}
|
||||
|
||||
match, err := e.executeTemplateOnInput(ctx, template, t.value)
|
||||
if err != nil {
|
||||
e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), t.value.Input, err)
|
||||
}
|
||||
results.CompareAndSwap(false, match)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
target.Iterate(func(scannedValue *contextargs.MetaInput) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -170,13 +128,43 @@ func (e *Engine) executeTemplateWithTargets(ctx context.Context, template *templ
|
||||
return true
|
||||
}
|
||||
|
||||
tasks <- task{index: index, skip: skip, value: scannedValue}
|
||||
wg.Add()
|
||||
go func(index uint32, skip bool, value *contextargs.MetaInput) {
|
||||
defer wg.Done()
|
||||
defer cleanupInFlight(index)
|
||||
if skip {
|
||||
return
|
||||
}
|
||||
|
||||
var match bool
|
||||
var err error
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
ctxArgs.MetaInput = value
|
||||
ctx := scan.NewScanContext(ctx, ctxArgs)
|
||||
switch template.Type() {
|
||||
case types.WorkflowProtocol:
|
||||
match = e.executeWorkflow(ctx, template.CompiledWorkflow)
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
if results, err := template.Executer.ExecuteWithResults(ctx); err == nil {
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
}
|
||||
}
|
||||
match = true
|
||||
} else {
|
||||
match, err = template.Executer.Execute(ctx)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), value.Input, err)
|
||||
}
|
||||
results.CompareAndSwap(false, match)
|
||||
}(index, skip, scannedValue)
|
||||
index++
|
||||
return true
|
||||
})
|
||||
|
||||
close(tasks)
|
||||
workersWg.Wait()
|
||||
wg.Wait()
|
||||
|
||||
// on completion marks the template as completed
|
||||
currentInfo.Lock()
|
||||
@@ -214,7 +202,26 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
go func(template *templates.Template, value *contextargs.MetaInput, wg *syncutil.AdaptiveWaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
match, err := e.executeTemplateOnInput(ctx, template, value)
|
||||
var match bool
|
||||
var err error
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
ctxArgs.MetaInput = value
|
||||
ctx := scan.NewScanContext(ctx, ctxArgs)
|
||||
switch template.Type() {
|
||||
case types.WorkflowProtocol:
|
||||
match = e.executeWorkflow(ctx, template.CompiledWorkflow)
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
if results, err := template.Executer.ExecuteWithResults(ctx); err == nil {
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
}
|
||||
}
|
||||
match = true
|
||||
} else {
|
||||
match, err = template.Executer.Execute(ctx)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
e.options.Logger.Warning().Msgf("[%s] Could not execute step on %s: %s\n", e.executerOpts.Colorizer.BrightBlue(template.ID), value.Input, err)
|
||||
}
|
||||
@@ -222,27 +229,3 @@ func (e *Engine) executeTemplatesOnTarget(ctx context.Context, alltemplates []*t
|
||||
}(tpl, target, sg)
|
||||
}
|
||||
}
|
||||
|
||||
// executeTemplateOnInput performs template execution for a single input and returns match status and error
|
||||
func (e *Engine) executeTemplateOnInput(ctx context.Context, template *templates.Template, value *contextargs.MetaInput) (bool, error) {
|
||||
ctxArgs := contextargs.New(ctx)
|
||||
ctxArgs.MetaInput = value
|
||||
scanCtx := scan.NewScanContext(ctx, ctxArgs)
|
||||
|
||||
switch template.Type() {
|
||||
case types.WorkflowProtocol:
|
||||
return e.executeWorkflow(scanCtx, template.CompiledWorkflow), nil
|
||||
default:
|
||||
if e.Callback != nil {
|
||||
results, err := template.Executer.ExecuteWithResults(scanCtx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, result := range results {
|
||||
e.Callback(result)
|
||||
}
|
||||
return len(results) > 0, nil
|
||||
}
|
||||
return template.Executer.Execute(scanCtx)
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
inputtypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
|
||||
tmpltypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
)
|
||||
|
||||
// fakeExecuter is a simple stub for protocols.Executer used to test executeTemplateOnInput
|
||||
type fakeExecuter struct {
|
||||
withResults bool
|
||||
}
|
||||
|
||||
func (f *fakeExecuter) Compile() error { return nil }
|
||||
func (f *fakeExecuter) Requests() int { return 1 }
|
||||
func (f *fakeExecuter) Execute(ctx *scan.ScanContext) (bool, error) { return !f.withResults, nil }
|
||||
func (f *fakeExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
if !f.withResults {
|
||||
return nil, nil
|
||||
}
|
||||
return []*output.ResultEvent{{Host: "h"}}, nil
|
||||
}
|
||||
|
||||
// newTestEngine creates a minimal Engine for tests
|
||||
func newTestEngine() *Engine {
|
||||
return New(&types.Options{})
|
||||
}
|
||||
|
||||
func Test_executeTemplateOnInput_CallbackPath(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
called := 0
|
||||
e.Callback = func(*output.ResultEvent) { called++ }
|
||||
|
||||
tpl := &templates.Template{}
|
||||
tpl.Executer = &fakeExecuter{withResults: true}
|
||||
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "x"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("expected match true")
|
||||
}
|
||||
if called == 0 {
|
||||
t.Fatalf("expected callback to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_executeTemplateOnInput_ExecutePath(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
tpl := &templates.Template{}
|
||||
tpl.Executer = &fakeExecuter{withResults: false}
|
||||
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "x"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("expected match true from Execute path")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeExecuterErr struct{}
|
||||
|
||||
func (f *fakeExecuterErr) Compile() error { return nil }
|
||||
func (f *fakeExecuterErr) Requests() int { return 1 }
|
||||
func (f *fakeExecuterErr) Execute(ctx *scan.ScanContext) (bool, error) { return false, nil }
|
||||
func (f *fakeExecuterErr) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, fmt.Errorf("boom")
|
||||
}
|
||||
|
||||
func Test_executeTemplateOnInput_CallbackErrorPropagates(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
e.Callback = func(*output.ResultEvent) {}
|
||||
tpl := &templates.Template{}
|
||||
tpl.Executer = &fakeExecuterErr{}
|
||||
|
||||
ok, err := e.executeTemplateOnInput(context.Background(), tpl, &contextargs.MetaInput{Input: "x"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error to propagate")
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("expected match to be false on error")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTargetProvider struct {
|
||||
values []*contextargs.MetaInput
|
||||
}
|
||||
|
||||
func (f *fakeTargetProvider) Count() int64 { return int64(len(f.values)) }
|
||||
func (f *fakeTargetProvider) Iterate(cb func(value *contextargs.MetaInput) bool) {
|
||||
for _, v := range f.values {
|
||||
if !cb(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
func (f *fakeTargetProvider) Set(string, string) {}
|
||||
func (f *fakeTargetProvider) SetWithProbe(string, string, inputtypes.InputLivenessProbe) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeTargetProvider) SetWithExclusions(string, string) error { return nil }
|
||||
func (f *fakeTargetProvider) InputType() string { return "test" }
|
||||
func (f *fakeTargetProvider) Close() {}
|
||||
|
||||
type slowExecuter struct{}
|
||||
|
||||
func (s *slowExecuter) Compile() error { return nil }
|
||||
func (s *slowExecuter) Requests() int { return 1 }
|
||||
func (s *slowExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
select {
|
||||
case <-ctx.Context().Done():
|
||||
return false, ctx.Context().Err()
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
func (s *slowExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func Test_executeTemplateWithTargets_RespectsCancellation(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
e.SetExecuterOptions(&protocols.ExecutorOptions{Logger: e.Logger, ResumeCfg: types.NewResumeCfg(), ProtocolType: tmpltypes.HTTPProtocol})
|
||||
|
||||
tpl := &templates.Template{}
|
||||
tpl.Executer = &slowExecuter{}
|
||||
|
||||
targets := &fakeTargetProvider{values: []*contextargs.MetaInput{{Input: "a"}, {Input: "b"}, {Input: "c"}}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
var matched atomic.Bool
|
||||
e.executeTemplateWithTargets(ctx, tpl, targets, &matched)
|
||||
}
|
||||
@@ -34,14 +34,17 @@ func (e *Engine) executeWorkflow(ctx *scan.ScanContext, w *workflows.Workflow) b
|
||||
swg, _ := syncutil.New(syncutil.WithSize(templateThreads))
|
||||
|
||||
for _, template := range w.Workflows {
|
||||
newCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
|
||||
if err := e.runWorkflowStep(template, newCtx, results, swg, w); err != nil {
|
||||
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
|
||||
}
|
||||
swg.Add()
|
||||
|
||||
func(template *workflows.WorkflowTemplate) {
|
||||
defer swg.Done()
|
||||
|
||||
if err := e.runWorkflowStep(template, ctx, results, swg, w); err != nil {
|
||||
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
|
||||
}
|
||||
}(template)
|
||||
}
|
||||
|
||||
swg.Wait()
|
||||
|
||||
return results.Load()
|
||||
}
|
||||
|
||||
|
||||
+6
-33
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
folderutil "github.com/projectdiscovery/utils/folder"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -47,45 +46,19 @@ func (customTemplate *customTemplateGitHubRepo) Update(ctx context.Context) {
|
||||
downloadPath := config.DefaultConfig.CustomGitHubTemplatesDirectory
|
||||
clonePath := customTemplate.getLocalRepoClonePath(downloadPath)
|
||||
|
||||
// If folder does not exist then clone/download the repo
|
||||
// If folder does not exits then clone/download the repo
|
||||
if !fileutil.FolderExists(clonePath) {
|
||||
customTemplate.Download(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to pull changes and handle the result
|
||||
customTemplate.handlePullChanges(clonePath)
|
||||
}
|
||||
|
||||
// handlePullChanges attempts to pull changes and logs the appropriate message
|
||||
func (customTemplate *customTemplateGitHubRepo) handlePullChanges(clonePath string) {
|
||||
err := customTemplate.pullChanges(clonePath, customTemplate.githubToken)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
customTemplate.logPullSuccess()
|
||||
case errors.Is(err, git.NoErrAlreadyUpToDate):
|
||||
customTemplate.logAlreadyUpToDate(err)
|
||||
default:
|
||||
customTemplate.logPullError(err)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("%s", err)
|
||||
} else {
|
||||
gologger.Info().Msgf("Repo %s/%s successfully pulled the changes.\n", customTemplate.owner, customTemplate.reponame)
|
||||
}
|
||||
}
|
||||
|
||||
// logPullSuccess logs a success message when changes are pulled
|
||||
func (customTemplate *customTemplateGitHubRepo) logPullSuccess() {
|
||||
gologger.Info().Msgf("Repo %s/%s successfully pulled the changes.\n", customTemplate.owner, customTemplate.reponame)
|
||||
}
|
||||
|
||||
// logAlreadyUpToDate logs an info message when repo is already up to date
|
||||
func (customTemplate *customTemplateGitHubRepo) logAlreadyUpToDate(err error) {
|
||||
gologger.Info().Msgf("%s", err)
|
||||
}
|
||||
|
||||
// logPullError logs an error message when pull fails
|
||||
func (customTemplate *customTemplateGitHubRepo) logPullError(err error) {
|
||||
gologger.Error().Msgf("%s", err)
|
||||
}
|
||||
|
||||
// NewGitHubProviders returns new instance of GitHub providers for downloading custom templates
|
||||
func NewGitHubProviders(options *types.Options) ([]*customTemplateGitHubRepo, error) {
|
||||
providers := []*customTemplateGitHubRepo{}
|
||||
@@ -214,7 +187,7 @@ func (ctr *customTemplateGitHubRepo) pullChanges(repoPath, githubToken string) e
|
||||
|
||||
err = w.Pull(pullOpts)
|
||||
if err != nil {
|
||||
return errkit.Wrapf(err, "%s/%s", ctr.owner, ctr.reponame)
|
||||
return errors.Errorf("%s/%s: %s", ctr.owner, ctr.reponame, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -61,7 +61,7 @@ func checkTimingDependency(
|
||||
|
||||
var requestsSent []requestsSentMetadata
|
||||
for requestsLeft > 0 {
|
||||
isCorrelationPossible, delayReceived, err := sendRequestAndTestConfidence(regression, highSleepTimeSeconds, requestSender, baselineDelay)
|
||||
isCorrelationPossible, delayRecieved, err := sendRequestAndTestConfidence(regression, highSleepTimeSeconds, requestSender, baselineDelay)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
@@ -69,29 +69,29 @@ func checkTimingDependency(
|
||||
return false, "", nil
|
||||
}
|
||||
// Check the delay is greater than baseline by seconds requested
|
||||
if delayReceived < baselineDelay+float64(highSleepTimeSeconds)*0.8 {
|
||||
if delayRecieved < baselineDelay+float64(highSleepTimeSeconds)*0.8 {
|
||||
return false, "", nil
|
||||
}
|
||||
requestsSent = append(requestsSent, requestsSentMetadata{
|
||||
delay: highSleepTimeSeconds,
|
||||
delayReceived: delayReceived,
|
||||
delayReceived: delayRecieved,
|
||||
})
|
||||
|
||||
isCorrelationPossibleSecond, delayReceivedSecond, err := sendRequestAndTestConfidence(regression, int(DefaultLowSleepTimeSeconds), requestSender, baselineDelay)
|
||||
isCorrelationPossibleSecond, delayRecievedSecond, err := sendRequestAndTestConfidence(regression, int(DefaultLowSleepTimeSeconds), requestSender, baselineDelay)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if !isCorrelationPossibleSecond {
|
||||
return false, "", nil
|
||||
}
|
||||
if delayReceivedSecond < baselineDelay+float64(DefaultLowSleepTimeSeconds)*0.8 {
|
||||
if delayRecievedSecond < baselineDelay+float64(DefaultLowSleepTimeSeconds)*0.8 {
|
||||
return false, "", nil
|
||||
}
|
||||
requestsLeft = requestsLeft - 2
|
||||
|
||||
requestsSent = append(requestsSent, requestsSentMetadata{
|
||||
delay: int(DefaultLowSleepTimeSeconds),
|
||||
delayReceived: delayReceivedSecond,
|
||||
delayReceived: delayRecievedSecond,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// and not arrays
|
||||
//
|
||||
// TODO: Support arrays + other JSON oddities by
|
||||
// adding more attributes to the map[string]interface{}
|
||||
// adding more attirbutes to the map[string]interface{}
|
||||
type JSON struct{}
|
||||
|
||||
var (
|
||||
|
||||
@@ -27,29 +27,7 @@ var (
|
||||
|
||||
// NewMultiPartForm returns a new MultiPartForm encoder
|
||||
func NewMultiPartForm() *MultiPartForm {
|
||||
return &MultiPartForm{
|
||||
filesMetadata: make(map[string]FileMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
// SetFileMetadata sets the file metadata for a given field name
|
||||
func (m *MultiPartForm) SetFileMetadata(fieldName string, metadata FileMetadata) {
|
||||
if m.filesMetadata == nil {
|
||||
m.filesMetadata = make(map[string]FileMetadata)
|
||||
}
|
||||
|
||||
m.filesMetadata[fieldName] = metadata
|
||||
}
|
||||
|
||||
// GetFileMetadata gets the file metadata for a given field name
|
||||
func (m *MultiPartForm) GetFileMetadata(fieldName string) (FileMetadata, bool) {
|
||||
if m.filesMetadata == nil {
|
||||
return FileMetadata{}, false
|
||||
}
|
||||
|
||||
metadata, exists := m.filesMetadata[fieldName]
|
||||
|
||||
return metadata, exists
|
||||
return &MultiPartForm{}
|
||||
}
|
||||
|
||||
// IsType returns true if the data is MultiPartForm encoded
|
||||
@@ -147,24 +125,16 @@ func (m *MultiPartForm) ParseBoundary(contentType string) error {
|
||||
if m.boundary == "" {
|
||||
return fmt.Errorf("no boundary found in the content type")
|
||||
}
|
||||
|
||||
// NOTE(dwisiswant0): boundary cannot exceed 70 characters according to
|
||||
// RFC-2046.
|
||||
if len(m.boundary) > 70 {
|
||||
return fmt.Errorf("boundary exceeds maximum length of 70 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode decodes the data from MultiPartForm format
|
||||
func (m *MultiPartForm) Decode(data string) (KV, error) {
|
||||
if m.boundary == "" {
|
||||
return KV{}, fmt.Errorf("boundary not set, call ParseBoundary first")
|
||||
}
|
||||
|
||||
// Create a buffer from the string data
|
||||
b := bytes.NewBufferString(data)
|
||||
// The boundary parameter should be extracted from the Content-Type header of the HTTP request
|
||||
// which is not available in this context, so this is a placeholder for demonstration.
|
||||
// You will need to pass the actual boundary value to this function.
|
||||
r := multipart.NewReader(b, m.boundary)
|
||||
|
||||
form, err := r.ReadForm(32 << 20) // 32MB is the max memory used to parse the form
|
||||
@@ -183,44 +153,30 @@ func (m *MultiPartForm) Decode(data string) (KV, error) {
|
||||
result.Set(key, values[0])
|
||||
}
|
||||
}
|
||||
|
||||
if m.filesMetadata == nil {
|
||||
m.filesMetadata = make(map[string]FileMetadata)
|
||||
}
|
||||
|
||||
m.filesMetadata = make(map[string]FileMetadata)
|
||||
for key, files := range form.File {
|
||||
fileContents := []interface{}{}
|
||||
var fileMetadataList []FileMetadata
|
||||
|
||||
for _, fileHeader := range files {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return KV{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
if _, err := buffer.ReadFrom(file); err != nil {
|
||||
_ = file.Close()
|
||||
|
||||
return KV{}, err
|
||||
}
|
||||
_ = file.Close()
|
||||
|
||||
fileContents = append(fileContents, buffer.String())
|
||||
|
||||
fileMetadataList = append(fileMetadataList, FileMetadata{
|
||||
m.filesMetadata[key] = FileMetadata{
|
||||
ContentType: fileHeader.Header.Get("Content-Type"),
|
||||
Filename: fileHeader.Filename,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result.Set(key, fileContents)
|
||||
|
||||
// NOTE(dwisiswant0): store the first file's metadata instead of the
|
||||
// last one
|
||||
if len(fileMetadataList) > 0 {
|
||||
m.filesMetadata[key] = fileMetadataList[0]
|
||||
}
|
||||
}
|
||||
return KVOrderedMap(&result), nil
|
||||
}
|
||||
|
||||
@@ -240,131 +240,3 @@ Python
|
||||
assert.Contains(t, documentsArray, "fake_pdf_content_1")
|
||||
assert.Contains(t, documentsArray, "fake_pdf_content_2")
|
||||
}
|
||||
|
||||
func TestMultiPartForm_SetGetFileMetadata(t *testing.T) {
|
||||
form := NewMultiPartForm()
|
||||
metadata := FileMetadata{
|
||||
ContentType: "image/jpeg",
|
||||
Filename: "test.jpg",
|
||||
}
|
||||
form.SetFileMetadata("avatar", metadata)
|
||||
|
||||
// Test GetFileMetadata for existing field
|
||||
retrievedMetadata, exists := form.GetFileMetadata("avatar")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, metadata.ContentType, retrievedMetadata.ContentType)
|
||||
assert.Equal(t, metadata.Filename, retrievedMetadata.Filename)
|
||||
|
||||
// Test GetFileMetadata for non-existing field
|
||||
_, exists = form.GetFileMetadata("nonexistent")
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestMultiPartForm_FilesMetadataInitialization(t *testing.T) {
|
||||
form := NewMultiPartForm()
|
||||
assert.NotNil(t, form.filesMetadata)
|
||||
|
||||
metadata := FileMetadata{
|
||||
ContentType: "text/plain",
|
||||
Filename: "test.txt",
|
||||
}
|
||||
form.SetFileMetadata("file", metadata)
|
||||
|
||||
retrievedMetadata, exists := form.GetFileMetadata("file")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, metadata, retrievedMetadata)
|
||||
}
|
||||
|
||||
func TestMultiPartForm_BoundaryValidation(t *testing.T) {
|
||||
form := NewMultiPartForm()
|
||||
|
||||
// Test valid boundary
|
||||
err := form.ParseBoundary("multipart/form-data; boundary=testboundary")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "testboundary", form.boundary)
|
||||
|
||||
// Test missing boundary
|
||||
err = form.ParseBoundary("multipart/form-data")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no boundary found")
|
||||
|
||||
// Test boundary too long (over 70 characters)
|
||||
longBoundary := "multipart/form-data; boundary=" + string(make([]byte, 71))
|
||||
for i := range longBoundary[len("multipart/form-data; boundary="):] {
|
||||
longBoundary = longBoundary[:len("multipart/form-data; boundary=")+i] + "a" + longBoundary[len("multipart/form-data; boundary=")+i+1:]
|
||||
}
|
||||
|
||||
err = form.ParseBoundary(longBoundary)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "boundary exceeds maximum length")
|
||||
}
|
||||
|
||||
func TestMultiPartForm_DecodeRequiresBoundary(t *testing.T) {
|
||||
form := NewMultiPartForm()
|
||||
|
||||
// Decode should fail if boundary is not set
|
||||
_, err := form.Decode("some data")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "boundary not set")
|
||||
}
|
||||
|
||||
func TestMultiPartForm_MultipleFilesMetadata(t *testing.T) {
|
||||
form := NewMultiPartForm()
|
||||
form.boundary = "----WebKitFormBoundaryMultiFileTest"
|
||||
|
||||
// Test with multiple files having the same field name
|
||||
multipartData := `------WebKitFormBoundaryMultiFileTest
|
||||
Content-Disposition: form-data; name="documents"; filename="file1.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
content1
|
||||
------WebKitFormBoundaryMultiFileTest
|
||||
Content-Disposition: form-data; name="documents"; filename="file2.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
content2
|
||||
------WebKitFormBoundaryMultiFileTest--
|
||||
`
|
||||
|
||||
decoded, err := form.Decode(multipartData)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify files are decoded correctly
|
||||
documents := decoded.Get("documents")
|
||||
require.NotNil(t, documents)
|
||||
documentsArray, ok := documents.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, documentsArray, 2)
|
||||
assert.Contains(t, documentsArray, "content1")
|
||||
assert.Contains(t, documentsArray, "content2")
|
||||
|
||||
// Verify metadata for the field exists (should be from the first file)
|
||||
metadata, exists := form.GetFileMetadata("documents")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "text/plain", metadata.ContentType)
|
||||
assert.Equal(t, "file1.txt", metadata.Filename) // Should be from first file, not last
|
||||
}
|
||||
|
||||
func TestMultiPartForm_SetFileMetadataWithNilMap(t *testing.T) {
|
||||
form := &MultiPartForm{}
|
||||
|
||||
// SetFileMetadata should handle nil filesMetadata
|
||||
metadata := FileMetadata{
|
||||
ContentType: "application/pdf",
|
||||
Filename: "document.pdf",
|
||||
}
|
||||
form.SetFileMetadata("doc", metadata)
|
||||
|
||||
// Should be able to retrieve the metadata
|
||||
retrievedMetadata, exists := form.GetFileMetadata("doc")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, metadata, retrievedMetadata)
|
||||
}
|
||||
|
||||
func TestMultiPartForm_GetFileMetadataWithNilMap(t *testing.T) {
|
||||
form := &MultiPartForm{}
|
||||
|
||||
// GetFileMetadata should handle nil filesMetadata gracefully
|
||||
_, exists := form.GetFileMetadata("anything")
|
||||
assert.False(t, exists)
|
||||
}
|
||||
@@ -21,14 +21,14 @@ import (
|
||||
// for parameters that are less likely to give results for a rule.
|
||||
type Tracker struct {
|
||||
frequencies gcache.Cache
|
||||
paramOccurrenceThreshold int
|
||||
paramOccurenceThreshold int
|
||||
|
||||
isDebug bool
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultMaxTrackCount = 10000
|
||||
DefaultParamOccurrenceThreshold = 10
|
||||
DefaultParamOccurenceThreshold = 10
|
||||
)
|
||||
|
||||
type cacheItem struct {
|
||||
@@ -38,7 +38,7 @@ type cacheItem struct {
|
||||
|
||||
// New creates a new frequency tracker with a given maximum
|
||||
// number of params to track in LRU fashion with a max error threshold
|
||||
func New(maxTrackCount, paramOccurrenceThreshold int) *Tracker {
|
||||
func New(maxTrackCount, paramOccurenceThreshold int) *Tracker {
|
||||
gc := gcache.New(maxTrackCount).ARC().Build()
|
||||
|
||||
var isDebug bool
|
||||
@@ -48,7 +48,7 @@ func New(maxTrackCount, paramOccurrenceThreshold int) *Tracker {
|
||||
return &Tracker{
|
||||
isDebug: isDebug,
|
||||
frequencies: gc,
|
||||
paramOccurrenceThreshold: paramOccurrenceThreshold,
|
||||
paramOccurenceThreshold: paramOccurenceThreshold,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ func (t *Tracker) Close() {
|
||||
t.frequencies.Purge()
|
||||
}
|
||||
|
||||
// MarkParameter marks a parameter as frequently occurring once.
|
||||
// MarkParameter marks a parameter as frequently occuring once.
|
||||
//
|
||||
// The logic requires a parameter to be marked as frequently occurring
|
||||
// multiple times before it's considered as frequently occurring.
|
||||
// The logic requires a parameter to be marked as frequently occuring
|
||||
// multiple times before it's considered as frequently occuring.
|
||||
func (t *Tracker) MarkParameter(parameter, target, template string) {
|
||||
normalizedTarget := normalizeTarget(target)
|
||||
key := getFrequencyKey(parameter, normalizedTarget, template)
|
||||
@@ -81,7 +81,7 @@ func (t *Tracker) MarkParameter(parameter, target, template string) {
|
||||
_ = t.frequencies.Set(key, existingCacheItemValue)
|
||||
}
|
||||
|
||||
// IsParameterFrequent checks if a parameter is frequently occurring
|
||||
// IsParameterFrequent checks if a parameter is frequently occuring
|
||||
// in the input with no much results.
|
||||
func (t *Tracker) IsParameterFrequent(parameter, target, template string) bool {
|
||||
normalizedTarget := normalizeTarget(target)
|
||||
@@ -97,7 +97,7 @@ func (t *Tracker) IsParameterFrequent(parameter, target, template string) bool {
|
||||
}
|
||||
existingCacheItemValue := existingCacheItem.(*cacheItem)
|
||||
|
||||
if existingCacheItemValue.errors.Load() >= int32(t.paramOccurrenceThreshold) {
|
||||
if existingCacheItemValue.errors.Load() >= int32(t.paramOccurenceThreshold) {
|
||||
existingCacheItemValue.Do(func() {
|
||||
gologger.Verbose().Msgf("[%s] Skipped %s from parameter for %s as found uninteresting %d times", template, parameter, target, existingCacheItemValue.errors.Load())
|
||||
})
|
||||
@@ -106,7 +106,7 @@ func (t *Tracker) IsParameterFrequent(parameter, target, template string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarkParameter unmarks a parameter as frequently occurring. This carries
|
||||
// UnmarkParameter unmarks a parameter as frequently occuring. This carries
|
||||
// more weight and resets the frequency counter for the parameter causing
|
||||
// it to be checked again. This is done when results are found.
|
||||
func (t *Tracker) UnmarkParameter(parameter, target, template string) {
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ func (rule *Rule) executePartComponentOnKV(input *ExecuteRuleInput, payload Valu
|
||||
return qerr
|
||||
}
|
||||
|
||||
// after building change back to original value to avoid repeating it in further requests
|
||||
// after building change back to original value to avoid repeating it in furthur requests
|
||||
if origKey != "" {
|
||||
err = ruleComponent.SetValue(origKey, types.ToString(origValue)) // change back to previous value for temp
|
||||
if err != nil {
|
||||
|
||||
@@ -43,6 +43,7 @@ func (j *BurpFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallba
|
||||
|
||||
// Print the parsed data for verification
|
||||
for _, item := range items.Items {
|
||||
item := item
|
||||
binx, err := base64.StdEncoding.DecodeString(item.Request.Raw)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not decode base64")
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
fileutil "github.com/projectdiscovery/utils/file"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -29,12 +28,6 @@ type InputFormatOptions struct {
|
||||
// RequiredOnly only uses required fields when generating requests
|
||||
// instead of all fields
|
||||
RequiredOnly bool
|
||||
// VarsTextTemplating uses Variables and inject it into the input
|
||||
// this is used for text templating of variables based on carvel ytt
|
||||
// Only available for Yaml formats
|
||||
VarsTextTemplating bool
|
||||
// VarsFilePaths is the path to the file containing variables
|
||||
VarsFilePaths []string
|
||||
}
|
||||
|
||||
// Format is an interface implemented by all input formats
|
||||
@@ -48,16 +41,6 @@ type Format interface {
|
||||
SetOptions(options InputFormatOptions)
|
||||
}
|
||||
|
||||
// SpecDownloader is an interface for downloading API specifications from URLs
|
||||
type SpecDownloader interface {
|
||||
// Download downloads the spec from the given URL and saves it to tmpDir
|
||||
// Returns the path to the downloaded file
|
||||
// httpClient is a retryablehttp.Client instance (can be nil for fallback)
|
||||
Download(url, tmpDir string, httpClient *retryablehttp.Client) (string, error)
|
||||
// SupportedExtensions returns the list of supported file extensions
|
||||
SupportedExtensions() []string
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultVarDumpFileName = "required_openapi_params.yaml"
|
||||
ErrNoVarsDumpFile = errors.New("no required params file found")
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
// OpenAPIDownloader implements the SpecDownloader interface for OpenAPI 3.0 specs
|
||||
type OpenAPIDownloader struct{}
|
||||
|
||||
// NewDownloader creates a new OpenAPI downloader
|
||||
func NewDownloader() formats.SpecDownloader {
|
||||
return &OpenAPIDownloader{}
|
||||
}
|
||||
|
||||
// This function downloads an OpenAPI 3.0 spec from the given URL and saves it to tmpDir
|
||||
func (d *OpenAPIDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {
|
||||
// Validate URL format, OpenAPI 3.0 specs are typically JSON
|
||||
if !strings.HasSuffix(urlStr, ".json") {
|
||||
return "", fmt.Errorf("URL does not appear to be an OpenAPI JSON spec")
|
||||
}
|
||||
|
||||
const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// Use provided httpClient or create a fallback
|
||||
var client *http.Client
|
||||
if httpClient != nil {
|
||||
client = httpClient.HTTPClient
|
||||
} else {
|
||||
// Fallback to simple client if no httpClient provided
|
||||
client = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
|
||||
resp, err := client.Get(urlStr)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to download OpenAPI spec")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d when downloading OpenAPI spec", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to read response body")
|
||||
}
|
||||
|
||||
// Validate it's a valid JSON and has OpenAPI structure
|
||||
var spec map[string]interface{}
|
||||
if err := json.Unmarshal(bodyBytes, &spec); err != nil {
|
||||
return "", fmt.Errorf("downloaded content is not valid JSON: %w", err)
|
||||
}
|
||||
|
||||
// Check if it's an OpenAPI 3.0 spec
|
||||
if openapi, exists := spec["openapi"]; exists {
|
||||
if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") {
|
||||
// Valid OpenAPI 3.0 spec
|
||||
} else {
|
||||
return "", fmt.Errorf("not a valid OpenAPI 3.0 spec (found version: %v)", openapi)
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("not an OpenAPI spec (missing 'openapi' field)")
|
||||
}
|
||||
|
||||
// Extract host from URL for server configuration
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse URL")
|
||||
}
|
||||
host := parsedURL.Host
|
||||
scheme := parsedURL.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Add servers section if missing or empty
|
||||
servers, exists := spec["servers"]
|
||||
if !exists || servers == nil {
|
||||
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
|
||||
} else if serverList, ok := servers.([]interface{}); ok && len(serverList) == 0 {
|
||||
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
|
||||
}
|
||||
|
||||
// Marshal back to JSON
|
||||
modifiedJSON, err := json.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal modified spec")
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
openapiDir := filepath.Join(tmpDir, "openapi")
|
||||
if err := os.MkdirAll(openapiDir, 0755); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create openapi directory")
|
||||
}
|
||||
|
||||
// Generate filename
|
||||
filename := fmt.Sprintf("openapi-spec-%d.json", time.Now().Unix())
|
||||
filePath := filepath.Join(openapiDir, filename)
|
||||
|
||||
// Write file
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if _, writeErr := file.Write(modifiedJSON); writeErr != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", errors.Wrap(writeErr, "failed to write OpenAPI spec to file")
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// SupportedExtensions returns the list of supported file extensions for OpenAPI
|
||||
func (d *OpenAPIDownloader) SupportedExtensions() []string {
|
||||
return []string{".json"}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOpenAPIDownloader_SupportedExtensions(t *testing.T) {
|
||||
downloader := &OpenAPIDownloader{}
|
||||
extensions := downloader.SupportedExtensions()
|
||||
|
||||
expected := []string{".json"}
|
||||
if len(extensions) != len(expected) {
|
||||
t.Errorf("Expected %d extensions, got %d", len(expected), len(extensions))
|
||||
}
|
||||
|
||||
for i, ext := range extensions {
|
||||
if ext != expected[i] {
|
||||
t.Errorf("Expected extension %s, got %s", expected[i], ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_Success(t *testing.T) {
|
||||
// Create a mock OpenAPI spec
|
||||
mockSpec := map[string]interface{}{
|
||||
"openapi": "3.0.0",
|
||||
"info": map[string]interface{}{
|
||||
"title": "Test API",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"paths": map[string]interface{}{
|
||||
"/test": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"summary": "Test endpoint",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mockSpec); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test download
|
||||
downloader := &OpenAPIDownloader{}
|
||||
filePath, err := downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Download failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if !fileExists(filePath) {
|
||||
t.Errorf("Downloaded file does not exist: %s", filePath)
|
||||
}
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read downloaded file: %v", err)
|
||||
}
|
||||
|
||||
var downloadedSpec map[string]interface{}
|
||||
if err := json.Unmarshal(content, &downloadedSpec); err != nil {
|
||||
t.Fatalf("Failed to parse downloaded JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify servers field was added
|
||||
servers, exists := downloadedSpec["servers"]
|
||||
if !exists {
|
||||
t.Error("Servers field was not added to the spec")
|
||||
}
|
||||
|
||||
if serversList, ok := servers.([]interface{}); ok {
|
||||
if len(serversList) == 0 {
|
||||
t.Error("Servers list is empty")
|
||||
}
|
||||
} else {
|
||||
t.Error("Servers field is not a list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_NonJSONURL(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
_, err = downloader.Download("http://example.com/spec.yaml", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-JSON URL, but got none")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "URL does not appear to be an OpenAPI JSON spec") {
|
||||
t.Errorf("Unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_HTTPError(t *testing.T) {
|
||||
// Create mock server that returns 404
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for HTTP 404, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_InvalidJSON(t *testing.T) {
|
||||
// Create mock server that returns invalid JSON
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := w.Write([]byte("invalid json")); err != nil {
|
||||
http.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_Timeout(t *testing.T) {
|
||||
// Create mock server with delay
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(35 * time.Second) // Longer than 30 second timeout
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"test": "data"}); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAPIDownloader_Download_WithExistingServers(t *testing.T) {
|
||||
// Create a mock OpenAPI spec with existing servers
|
||||
mockSpec := map[string]interface{}{
|
||||
"openapi": "3.0.0",
|
||||
"info": map[string]interface{}{
|
||||
"title": "Test API",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"servers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"url": "https://existing-server.com",
|
||||
},
|
||||
},
|
||||
"paths": map[string]interface{}{},
|
||||
}
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mockSpec); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "openapi_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &OpenAPIDownloader{}
|
||||
filePath, err := downloader.Download(server.URL+"/openapi.json", tmpDir, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Download failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify existing servers are preserved
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read downloaded file: %v", err)
|
||||
}
|
||||
|
||||
var downloadedSpec map[string]interface{}
|
||||
if err := json.Unmarshal(content, &downloadedSpec); err != nil {
|
||||
t.Fatalf("Failed to parse downloaded JSON: %v", err)
|
||||
}
|
||||
|
||||
servers, exists := downloadedSpec["servers"]
|
||||
if !exists {
|
||||
t.Error("Servers field was removed from the spec")
|
||||
}
|
||||
|
||||
if serversList, ok := servers.([]interface{}); ok {
|
||||
if len(serversList) != 1 {
|
||||
t.Errorf("Expected 1 server, got %d", len(serversList))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if file exists
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// SwaggerDownloader implements the SpecDownloader interface for Swagger 2.0 specs
|
||||
type SwaggerDownloader struct{}
|
||||
|
||||
// NewDownloader creates a new Swagger downloader
|
||||
func NewDownloader() formats.SpecDownloader {
|
||||
return &SwaggerDownloader{}
|
||||
}
|
||||
|
||||
// This function downloads a Swagger 2.0 spec from the given URL and saves it to tmpDir
|
||||
func (d *SwaggerDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {
|
||||
// Swagger can be JSON or YAML
|
||||
supportedExts := d.SupportedExtensions()
|
||||
isSupported := false
|
||||
for _, ext := range supportedExts {
|
||||
if strings.HasSuffix(urlStr, ext) {
|
||||
isSupported = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSupported {
|
||||
return "", fmt.Errorf("URL does not appear to be a Swagger spec (supported: %v)", supportedExts)
|
||||
}
|
||||
|
||||
const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
// Use provided httpClient or create a fallback
|
||||
var client *http.Client
|
||||
if httpClient != nil {
|
||||
client = httpClient.HTTPClient
|
||||
} else {
|
||||
// Fallback to simple client if no httpClient provided
|
||||
client = &http.Client{Timeout: 30 * time.Second}
|
||||
}
|
||||
|
||||
resp, err := client.Get(urlStr)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to download Swagger spec")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d when downloading Swagger spec", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to read response body")
|
||||
}
|
||||
|
||||
// Determine format and parse
|
||||
var spec map[string]interface{}
|
||||
var isYAML bool
|
||||
|
||||
// Try JSON first
|
||||
if err := json.Unmarshal(bodyBytes, &spec); err != nil {
|
||||
// Then try YAML
|
||||
if err := yaml.Unmarshal(bodyBytes, &spec); err != nil {
|
||||
return "", fmt.Errorf("downloaded content is neither valid JSON nor YAML: %w", err)
|
||||
}
|
||||
isYAML = true
|
||||
}
|
||||
|
||||
// Validate it's a Swagger 2.0 spec
|
||||
if swagger, exists := spec["swagger"]; exists {
|
||||
if swaggerStr, ok := swagger.(string); ok && strings.HasPrefix(swaggerStr, "2.") {
|
||||
// Valid Swagger 2.0 spec
|
||||
} else {
|
||||
return "", fmt.Errorf("not a valid Swagger 2.0 spec (found version: %v)", swagger)
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("not a Swagger spec (missing 'swagger' field)")
|
||||
}
|
||||
|
||||
// Extract host from URL for host configuration
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse URL")
|
||||
}
|
||||
|
||||
host := parsedURL.Host
|
||||
scheme := parsedURL.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Add host if missing
|
||||
if _, exists := spec["host"]; !exists {
|
||||
spec["host"] = host
|
||||
}
|
||||
|
||||
// Add schemes if missing
|
||||
if _, exists := spec["schemes"]; !exists {
|
||||
spec["schemes"] = []string{scheme}
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
swaggerDir := filepath.Join(tmpDir, "swagger")
|
||||
if err := os.MkdirAll(swaggerDir, 0755); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create swagger directory")
|
||||
}
|
||||
|
||||
// Generate filename and content based on original format
|
||||
var filename string
|
||||
var content []byte
|
||||
|
||||
if isYAML {
|
||||
filename = fmt.Sprintf("swagger-spec-%d.yaml", time.Now().Unix())
|
||||
content, err = yaml.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal modified YAML spec")
|
||||
}
|
||||
} else {
|
||||
filename = fmt.Sprintf("swagger-spec-%d.json", time.Now().Unix())
|
||||
content, err = json.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal modified JSON spec")
|
||||
}
|
||||
}
|
||||
|
||||
filePath := filepath.Join(swaggerDir, filename)
|
||||
|
||||
// Write file
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create file")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if _, writeErr := file.Write(content); writeErr != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", errors.Wrap(writeErr, "failed to write file")
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
// SupportedExtensions returns the list of supported file extensions for Swagger
|
||||
func (d *SwaggerDownloader) SupportedExtensions() []string {
|
||||
return []string{".json", ".yaml", ".yml"}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package swagger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestSwaggerDownloader_SupportedExtensions(t *testing.T) {
|
||||
downloader := &SwaggerDownloader{}
|
||||
extensions := downloader.SupportedExtensions()
|
||||
|
||||
expected := []string{".json", ".yaml", ".yml"}
|
||||
if len(extensions) != len(expected) {
|
||||
t.Errorf("Expected %d extensions, got %d", len(expected), len(extensions))
|
||||
}
|
||||
|
||||
for i, ext := range extensions {
|
||||
if ext != expected[i] {
|
||||
t.Errorf("Expected extension %s, got %s", expected[i], ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_JSON_Success(t *testing.T) {
|
||||
// Create a mock Swagger spec (JSON)
|
||||
mockSpec := map[string]interface{}{
|
||||
"swagger": "2.0",
|
||||
"info": map[string]interface{}{
|
||||
"title": "Test API",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"paths": map[string]interface{}{
|
||||
"/test": map[string]interface{}{
|
||||
"get": map[string]interface{}{
|
||||
"summary": "Test endpoint",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mockSpec); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test download
|
||||
downloader := &SwaggerDownloader{}
|
||||
filePath, err := downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Download failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if !fileExists(filePath) {
|
||||
t.Errorf("Downloaded file does not exist: %s", filePath)
|
||||
}
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read downloaded file: %v", err)
|
||||
}
|
||||
|
||||
var downloadedSpec map[string]interface{}
|
||||
if err := json.Unmarshal(content, &downloadedSpec); err != nil {
|
||||
t.Fatalf("Failed to parse downloaded JSON: %v", err)
|
||||
}
|
||||
|
||||
// Verify host field was added
|
||||
_, exists := downloadedSpec["host"]
|
||||
if !exists {
|
||||
t.Error("Host field was not added to the spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_YAML_Success(t *testing.T) {
|
||||
// Create a mock Swagger spec (YAML)
|
||||
mockSpecYAML := `
|
||||
swagger: "2.0"
|
||||
info:
|
||||
title: "Test API"
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/test:
|
||||
get:
|
||||
summary: "Test endpoint"
|
||||
`
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml")
|
||||
if _, err := w.Write([]byte(mockSpecYAML)); err != nil {
|
||||
http.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
|
||||
defer server.Close()
|
||||
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test download
|
||||
downloader := &SwaggerDownloader{}
|
||||
filePath, err := downloader.Download(server.URL+"/swagger.yaml", tmpDir, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Download failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if !fileExists(filePath) {
|
||||
t.Errorf("Downloaded file does not exist: %s", filePath)
|
||||
}
|
||||
|
||||
// Verify file content
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read downloaded file: %v", err)
|
||||
}
|
||||
|
||||
var downloadedSpec map[string]interface{}
|
||||
if err := yaml.Unmarshal(content, &downloadedSpec); err != nil {
|
||||
t.Fatalf("Failed to parse downloaded YAML: %v", err)
|
||||
}
|
||||
|
||||
// Verify host field was added
|
||||
_, exists := downloadedSpec["host"]
|
||||
if !exists {
|
||||
t.Error("Host field was not added to the spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_UnsupportedExtension(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download("http://example.com/spec.xml", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for unsupported extension, but got none")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "URL does not appear to be a Swagger spec") {
|
||||
t.Errorf("Unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_HTTPError(t *testing.T) {
|
||||
// Create mock server that returns 404
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for HTTP 404, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_InvalidJSON(t *testing.T) {
|
||||
// Create mock server that returns invalid JSON
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := w.Write([]byte("invalid json")); err != nil {
|
||||
http.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_InvalidYAML(t *testing.T) {
|
||||
// Create mock server that returns invalid YAML
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/yaml")
|
||||
if _, err := w.Write([]byte("invalid: yaml: content: [")); err != nil {
|
||||
http.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/swagger.yaml", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid YAML, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_Timeout(t *testing.T) {
|
||||
// Create mock server with delay
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(35 * time.Second) // Longer than 30 second timeout
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{"test": "data"}); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
_, err = downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error, but got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwaggerDownloader_Download_WithExistingHost(t *testing.T) {
|
||||
// Create a mock Swagger spec with existing host
|
||||
mockSpec := map[string]interface{}{
|
||||
"swagger": "2.0",
|
||||
"info": map[string]interface{}{
|
||||
"title": "Test API",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
"host": "existing-host.com",
|
||||
"paths": map[string]interface{}{},
|
||||
}
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(mockSpec); err != nil {
|
||||
http.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "swagger_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to remove temp dir: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
downloader := &SwaggerDownloader{}
|
||||
filePath, err := downloader.Download(server.URL+"/swagger.json", tmpDir, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Download failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify existing host is preserved
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read downloaded file: %v", err)
|
||||
}
|
||||
|
||||
var downloadedSpec map[string]interface{}
|
||||
if err := json.Unmarshal(content, &downloadedSpec); err != nil {
|
||||
t.Fatalf("Failed to parse downloaded JSON: %v", err)
|
||||
}
|
||||
|
||||
host, exists := downloadedSpec["host"]
|
||||
if !exists {
|
||||
t.Error("Host field was removed from the spec")
|
||||
}
|
||||
|
||||
if hostStr, ok := host.(string); !ok || hostStr != "existing-host.com" {
|
||||
t.Errorf("Expected host 'existing-host.com', got '%v'", host)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if file exists
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
+6
-6
@@ -131,7 +131,7 @@ paths:
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully created user
|
||||
description: Sucessfully created user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -170,7 +170,7 @@ paths:
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully logged in user
|
||||
description: Sucessfully logged in user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -262,7 +262,7 @@ paths:
|
||||
example: 'name1'
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully deleted user
|
||||
description: Sucessfully deleted user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -331,7 +331,7 @@ paths:
|
||||
required: true
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully updated user email
|
||||
description: Sucessfully updated user email
|
||||
content: {}
|
||||
'400':
|
||||
description: Invalid request
|
||||
@@ -389,7 +389,7 @@ paths:
|
||||
required: true
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully updated users password
|
||||
description: Sucessfully updated users password
|
||||
content: {}
|
||||
'400':
|
||||
description: Invalid request
|
||||
@@ -475,7 +475,7 @@ paths:
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully added a book
|
||||
description: Sucessfully added a book
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:json", "json")
|
||||
|
||||
#@ def get_value(key, default=""):
|
||||
#@ if hasattr(data.values, key):
|
||||
#@ return str(getattr(data.values, key))
|
||||
#@ else:
|
||||
#@ return default
|
||||
#@ end
|
||||
#@ end
|
||||
|
||||
timestamp: 2024-02-20T19:24:13+05:32
|
||||
url: https://ginandjuice.shop/users/3
|
||||
request:
|
||||
#@yaml/text-templated-strings
|
||||
raw: |+
|
||||
POST /users/3 HTTP/1.1
|
||||
Host: ginandjuice.shop
|
||||
Authorization: Bearer (@= get_value("token", "3x4mpl3t0k3n") @)
|
||||
Accept-Encoding: gzip
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Connection: close
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
|
||||
|
||||
foo=(@= json.encode(data.values.foo) @)&bar=(@= get_value("bar") @)&debug=(@= get_value("debug", "false") @)
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
list: pkg/input/formats/testdata/ytt/ginandjuice.ytt.yaml
|
||||
input-mode: yaml
|
||||
templates:
|
||||
- integration_tests/fuzz/fuzz-body.yaml
|
||||
var:
|
||||
- debug=true
|
||||
- bar=bar
|
||||
vars-text-templating: true
|
||||
var-file-paths:
|
||||
- pkg/input/formats/testdata/ytt/ytt-vars.yaml
|
||||
dast: true
|
||||
@@ -1,3 +0,0 @@
|
||||
token: foobar
|
||||
foo:
|
||||
bar: baz
|
||||
@@ -1,8 +1,8 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/projectdiscovery/gologger"
|
||||
@@ -46,41 +46,23 @@ func (j *YamlMultiDocFormat) SetOptions(options formats.InputFormatOptions) {
|
||||
// Parse parses the input and calls the provided callback
|
||||
// function for each RawRequest it discovers.
|
||||
func (j *YamlMultiDocFormat) Parse(input io.Reader, resultsCb formats.ParseReqRespCallback, filePath string) error {
|
||||
finalInput := input
|
||||
|
||||
// Apply text templating if enabled
|
||||
if j.opts.VarsTextTemplating {
|
||||
data, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not read input")
|
||||
}
|
||||
tpl := []string{string(data)}
|
||||
dvs := mapToKeyValueSlice(j.opts.Variables)
|
||||
finalData, err := ytt(tpl, dvs, j.opts.VarsFilePaths)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not apply ytt templating")
|
||||
}
|
||||
finalInput = bytes.NewReader(finalData)
|
||||
}
|
||||
|
||||
decoder := YamlUtil.NewDecoder(finalInput)
|
||||
decoder := YamlUtil.NewDecoder(input)
|
||||
for {
|
||||
var request proxifyRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return errors.Wrap(err, "could not decode yaml file")
|
||||
err := decoder.Decode(&request)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
raw := request.Request.Raw
|
||||
if raw == "" {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not decode json file")
|
||||
}
|
||||
if strings.TrimSpace(request.Request.Raw) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rawRequest, err := types.ParseRawRequestWithURL(raw, request.URL)
|
||||
rawRequest, err := types.ParseRawRequestWithURL(request.Request.Raw, request.URL)
|
||||
if err != nil {
|
||||
gologger.Warning().Msgf("multidoc-yaml: Could not parse raw request %s: %s", request.URL, err)
|
||||
gologger.Warning().Msgf("multidoc-yaml: Could not parse raw request %s: %s\n", request.URL, err)
|
||||
continue
|
||||
}
|
||||
resultsCb(rawRequest)
|
||||
|
||||
@@ -2,10 +2,8 @@ package yaml
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -35,48 +33,3 @@ func TestYamlFormatterParse(t *testing.T) {
|
||||
require.Len(t, urls, len(expectedUrls), "invalid number of urls")
|
||||
require.ElementsMatch(t, urls, expectedUrls, "invalid urls")
|
||||
}
|
||||
|
||||
func TestYamlFormatterParseWithVariables(t *testing.T) {
|
||||
format := New()
|
||||
proxifyYttFile := "../testdata/ytt/ginandjuice.ytt.yaml"
|
||||
|
||||
expectedUrls := []string{
|
||||
"https://ginandjuice.shop/users/3",
|
||||
}
|
||||
|
||||
format.SetOptions(formats.InputFormatOptions{
|
||||
VarsTextTemplating: true,
|
||||
Variables: map[string]interface{}{
|
||||
"foo": "catalog",
|
||||
"bar": "product",
|
||||
},
|
||||
})
|
||||
file, err := os.Open(proxifyYttFile)
|
||||
require.Nilf(t, err, "error opening proxify ytt input file: %v", err)
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
var urls []string
|
||||
err = format.Parse(file, func(request *types.RequestResponse) bool {
|
||||
urls = append(urls, request.URL.String())
|
||||
expectedRaw := `POST /users/3 HTTP/1.1
|
||||
Host: ginandjuice.shop
|
||||
Authorization: Bearer 3x4mpl3t0k3n
|
||||
Accept-Encoding: gzip
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Connection: close
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
|
||||
|
||||
foo="catalog"&bar=product&debug=false`
|
||||
normalised := strings.ReplaceAll(request.Request.Raw, "\r\n", "\n")
|
||||
require.Equal(t, expectedRaw, strings.TrimSuffix(normalised, "\n"), "request raw does not match expected value")
|
||||
|
||||
return false
|
||||
}, proxifyYttFile)
|
||||
|
||||
require.Nilf(t, err, "error parsing yaml file: %v", err)
|
||||
require.Len(t, urls, len(expectedUrls), "invalid number of urls")
|
||||
require.ElementsMatch(t, urls, expectedUrls, "invalid urls")
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
yttcmd "carvel.dev/ytt/pkg/cmd/template"
|
||||
yttui "carvel.dev/ytt/pkg/cmd/ui"
|
||||
yttfiles "carvel.dev/ytt/pkg/files"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func ytt(tpl, dvs []string, varFiles []string) ([]byte, error) {
|
||||
// create and invoke ytt "template" command
|
||||
templatingOptions := yttcmd.NewOptions()
|
||||
|
||||
input, err := templatesAsInput(tpl...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(varFiles) > 0 {
|
||||
// Load vaarFiles into the templating options.
|
||||
templatingOptions.DataValuesFlags.FromFiles = varFiles
|
||||
}
|
||||
|
||||
// equivalent to `--data-value-yaml`
|
||||
templatingOptions.DataValuesFlags.KVsFromYAML = dvs
|
||||
|
||||
// for in-memory use, pipe output to "/dev/null"
|
||||
noopUI := yttui.NewCustomWriterTTY(false, noopWriter{}, noopWriter{})
|
||||
|
||||
// Evaluate the template given the configured data values...
|
||||
output := templatingOptions.RunWithFiles(input, noopUI)
|
||||
if output.Err != nil {
|
||||
return nil, output.Err
|
||||
}
|
||||
|
||||
return output.DocSet.AsBytes()
|
||||
}
|
||||
|
||||
// templatesAsInput conveniently wraps one or more strings, each in a files.File, into a template.Input.
|
||||
func templatesAsInput(tpl ...string) (yttcmd.Input, error) {
|
||||
var files []*yttfiles.File
|
||||
for i, t := range tpl {
|
||||
// to make this less brittle, you'll probably want to use well-defined names for `path`, here, for each input.
|
||||
// this matters when you're processing errors which report based on these paths.
|
||||
file, err := yttfiles.NewFileFromSource(yttfiles.NewBytesSource(fmt.Sprintf("tpl%d.yml", i), []byte(t)))
|
||||
if err != nil {
|
||||
return yttcmd.Input{}, err
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return yttcmd.Input{Files: files}, nil
|
||||
}
|
||||
|
||||
func mapToKeyValueSlice(m map[string]interface{}) []string {
|
||||
var result []string
|
||||
for k, v := range m {
|
||||
y, _ := yaml.Marshal(v)
|
||||
result = append(result, fmt.Sprintf("%s=%s", k, strings.TrimSpace(string(y))))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type noopWriter struct{}
|
||||
|
||||
func (w noopWriter) Write(data []byte) (int, error) { return len(data), nil }
|
||||
@@ -7,16 +7,12 @@ import (
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/openapi"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/swagger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider/http"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider/list"
|
||||
"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/generators"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
|
||||
configTypes "github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
)
|
||||
@@ -78,8 +74,6 @@ type InputProvider interface {
|
||||
type InputOptions struct {
|
||||
// Options for global config
|
||||
Options *configTypes.Options
|
||||
// TempDir is the temporary directory for storing files
|
||||
TempDir string
|
||||
// NotFoundCallback is the callback to call when input is not found
|
||||
// only supported in list input provider
|
||||
NotFoundCallback func(template string) bool
|
||||
@@ -113,58 +107,18 @@ func NewInputProvider(opts InputOptions) (InputProvider, error) {
|
||||
Options: opts.Options,
|
||||
NotFoundCallback: opts.NotFoundCallback,
|
||||
})
|
||||
} else if len(opts.Options.Targets) > 0 &&
|
||||
(strings.EqualFold(opts.Options.InputFileMode, "openapi") || strings.EqualFold(opts.Options.InputFileMode, "swagger")) {
|
||||
|
||||
if len(opts.Options.Targets) > 1 {
|
||||
return nil, fmt.Errorf("only one target URL is supported in %s input mode", opts.Options.InputFileMode)
|
||||
}
|
||||
|
||||
target := opts.Options.Targets[0]
|
||||
if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
||||
var downloader formats.SpecDownloader
|
||||
var tempFile string
|
||||
var err error
|
||||
|
||||
// Get HttpClient from protocolstate if available
|
||||
var httpClient *retryablehttp.Client
|
||||
if opts.Options.ExecutionId != "" {
|
||||
dialers := protocolstate.GetDialersWithId(opts.Options.ExecutionId)
|
||||
if dialers != nil {
|
||||
httpClient = dialers.DefaultHTTPClient
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(opts.Options.InputFileMode) {
|
||||
case "openapi":
|
||||
downloader = openapi.NewDownloader()
|
||||
tempFile, err = downloader.Download(target, opts.TempDir, httpClient)
|
||||
case "swagger":
|
||||
downloader = swagger.NewDownloader()
|
||||
tempFile, err = downloader.Download(target, opts.TempDir, httpClient)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported input mode: %s", opts.Options.InputFileMode)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download %s spec from url %s: %w", opts.Options.InputFileMode, target, err)
|
||||
}
|
||||
|
||||
opts.Options.TargetsFilePath = tempFile
|
||||
}
|
||||
} else {
|
||||
// use HttpInputProvider
|
||||
return http.NewHttpInputProvider(&http.HttpMultiFormatOptions{
|
||||
InputFile: opts.Options.TargetsFilePath,
|
||||
InputMode: opts.Options.InputFileMode,
|
||||
Options: formats.InputFormatOptions{
|
||||
Variables: generators.MergeMaps(extraVars, opts.Options.Vars.AsMap()),
|
||||
SkipFormatValidation: opts.Options.SkipFormatValidation,
|
||||
RequiredOnly: opts.Options.FormatUseRequiredOnly,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return http.NewHttpInputProvider(&http.HttpMultiFormatOptions{
|
||||
InputFile: opts.Options.TargetsFilePath,
|
||||
InputMode: opts.Options.InputFileMode,
|
||||
Options: formats.InputFormatOptions{
|
||||
Variables: generators.MergeMaps(extraVars, opts.Options.Vars.AsMap()),
|
||||
SkipFormatValidation: opts.Options.SkipFormatValidation,
|
||||
RequiredOnly: opts.Options.FormatUseRequiredOnly,
|
||||
VarsTextTemplating: opts.Options.VarsTextTemplating,
|
||||
VarsFilePaths: opts.Options.VarsFilePaths,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SupportedInputFormats returns all supported input formats of nuclei
|
||||
|
||||
@@ -83,7 +83,7 @@ func UpdateIgnoreFile() error {
|
||||
}
|
||||
|
||||
func doVersionCheck(isSDK bool) error {
|
||||
// we use global retryablehttp client so its not immediately gc'd if any references are held
|
||||
// we use global retryablehttp client so its not immeditely gc'd if any references are held
|
||||
// and according our config we have idle connections which are shown as leaked by goleak in tests
|
||||
// i.e we close all idle connections after our use and it doesn't affect any other part of the code
|
||||
defer retryableHttpClient.HTTPClient.CloseIdleConnections()
|
||||
|
||||
@@ -9,7 +9,7 @@ The Very First before making any type of contribution to javascript runtime in n
|
||||
|
||||
## Documentation/Typo Contribution
|
||||
|
||||
Most of Javascript API Reference documentation is auto-generated with help of code-generation and [jsdocgen](./devtools/jsdocgen/README.md) and hence any type of documentation contribution are always welcome and can be done by editing [javascript jsdoc](./generated/js/) files
|
||||
Most of Javascript API Reference documentation is auto-generated with help of code-generation and [jsdocgen](./devtools/jsdocgen/README.md) and hence any type of documentation contribution are always welcome and can be done by editing [javscript jsdoc](./generated/js/) files
|
||||
|
||||
|
||||
## Improving Existing Libraries(aka node_modules)
|
||||
@@ -33,7 +33,7 @@ Libraries/node_modules represent adding new protocol or something similar and sh
|
||||
|
||||
## Adding Helper Objects/Types/Functions
|
||||
|
||||
Helper objects/types/functions can simply be understood as javascript utils to simplify writing javascript and reduce code duplication in javascript templates. Helper functions/objects are divided into two categories
|
||||
Helper objects/types/functions can simply be understood as javascript utils to simplify writing javscript and reduce code duplication in javascript templates. Helper functions/objects are divided into two categories
|
||||
|
||||
### javascript based helpers
|
||||
|
||||
@@ -47,7 +47,7 @@ go based helpers are written in go and can import any go library if required. Mi
|
||||
|
||||
### Updating / Publishing Docs
|
||||
|
||||
Javascript Protocol Documentation is auto-generated using [jsdoc] and is hosted at [js-proto-docs](https://projectdiscovery.github.io/js-proto-docs/). To update documentation, please follow steps mentioned at [projectdiscovery/js-proto-docs](https://github.com/projectdiscovery/js-proto-docs)
|
||||
Javscript Protocol Documentation is auto-generated using [jsdoc] and is hosted at [js-proto-docs](https://projectdiscovery.github.io/js-proto-docs/). To update documentation, please follow steps mentioned at [projectdiscovery/js-proto-docs](https://github.com/projectdiscovery/js-proto-docs)
|
||||
|
||||
|
||||
### Go Code Guidelines
|
||||
@@ -60,7 +60,7 @@ Javascript Protocol Documentation is auto-generated using [jsdoc] and is hosted
|
||||
return false, protocolstate.ErrHostDenied.Msgf(host)
|
||||
}
|
||||
```
|
||||
3. Keep exported package clean. Do not keep unnecessary global exports which the consumer of the API doesn't need to know about. Keep only user-exposed API public.
|
||||
3. Keep exported package clean. Do not keep unncessary global exports which the consumer of the API doesn't need to know about. Keep only user-exposed API public.
|
||||
4. Use timeouts and context cancellation when calling Network related stuff. Also make sure to close your connections or provide a mechanism to the user of the API to do so.
|
||||
5. Always try to return single types from inside javascript with an error like `(IsRDP, error)` instead of returning multiple values `(name, version string, err error)`. The second one will get converted to an array is much harder for consumers to deal with. Instead, try to return `Structures` which will be accessible natively.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrJSExecDeadline is the error returned when allotted time for script execution exceeds
|
||||
// ErrJSExecDeadline is the error returned when alloted time for script execution exceeds
|
||||
ErrJSExecDeadline = errkit.New("js engine execution deadline exceeded").SetKind(errkit.ErrKindDeadline).Build()
|
||||
)
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ func createNewRuntime() *goja.Runtime {
|
||||
// by default import below modules every time
|
||||
_ = runtime.Set("console", require.Require(runtime, console.ModuleName))
|
||||
|
||||
// Register embedded javascript helpers
|
||||
// Register embedded javacript helpers
|
||||
if err := global.RegisterNativeScripts(runtime); err != nil {
|
||||
gologger.Error().Msgf("Could not register scripts: %s\n", err)
|
||||
}
|
||||
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user