Nuclei is a request engine driven by YAML templates. It ships with thousands of community templates covering known CVEs, default credentials, exposed admin panels and misconfigurations — and it is fast enough to run against every environment on every deploy.
It is also the clearest demonstration in this track of a rule that has come up in every chapter: a scanner finds what somebody wrote a rule for. Nuclei just makes the boundary unusually visible, because the rules are files you can read and write yourself.
Verified against Nuclei 3.9.0, against the Range app. Output copied from a real run.
Step 1 — Run the community templates
brew install nuclei
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range
docker build -f docker/Dockerfile -t range-app:vulnerable .
docker run -d --name range-dast -p 127.0.0.1:3100:3000 range-app:vulnerable
nuclei -u http://127.0.0.1:3100[http-missing-security-headers:content-security-policy] [info] http://127.0.0.1:3100
[http-missing-security-headers:x-frame-options] [info] http://127.0.0.1:3100
[http-missing-security-headers:strict-transport-security][info] http://127.0.0.1:3100
[http-missing-security-headers:permissions-policy] [info] http://127.0.0.1:3100
[http-missing-security-headers:x-content-type-options] [info] http://127.0.0.1:3100
[tech-detect:express] [info] http://127.0.0.1:3100
[form-detection] [info] http://127.0.0.1:3100
[options-method] [info] http://127.0.0.1:3100 ["GET,HEAD"]Every finding is [info]. Missing headers, technology fingerprinting, a form detected.
Meanwhile this application has an unauthenticated endpoint that returns AWS credentials as JSON, and a reflected XSS. Nuclei reported neither.
Why, and it is not a criticism. The community templates encode known issues — CVE-2021-44228 at a known path, a default Grafana login, an exposed
.gitdirectory./debug/configis not a known path; it is a route somebody on your team wrote. No template exists because nobody outside your organisation has ever seen your application.
Filtering by tag does not change the answer:
nuclei -u http://127.0.0.1:3100 -tags exposure,misconfig,xssStill only the header findings. Ten thousand templates, zero coverage of your actual application.
Step 2 — Write the templates that do apply
A Nuclei template is a request and a set of matchers:
# templates/range-debug-endpoint.yaml
id: range-debug-config-exposure
info:
name: Debug endpoint leaking credentials
author: aiopsone
severity: critical
tags: exposure,config
http:
- method: GET
path:
- "{{BaseURL}}/debug/config"
matchers-condition: and
matchers:
- type: status
status: [200]
- type: word
part: body
words: ["awsAccessKeyId", "dbPassword"]
condition: and# templates/range-reflected-xss.yaml
id: range-reflected-xss
info:
name: Reflected XSS in greet parameter
author: aiopsone
severity: high
tags: xss
http:
- method: GET
path:
- "{{BaseURL}}/greet?name=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
matchers-condition: and
matchers:
- type: status
status: [200]
- type: word
part: body
words: ["<script>alert(1)</script>"]nuclei -u http://127.0.0.1:3100 -t templates/[range-reflected-xss] [http] [high] http://127.0.0.1:3100/greet?name=%3Cscript%3E…
[range-debug-config-exposure] [http] [critical] http://127.0.0.1:3100/debug/configCritical and high, where ten thousand community templates produced nothing but info.
Two things make these work. matchers-condition: and means both a 200 and the expected content — a 404 page containing the word "awsAccessKeyId" will not trigger it. And the XSS matcher looks for the payload reflected unencoded; if the app escaped it to <script>, the match fails and the template correctly reports nothing.
Step 3 — Where this fits against ZAP
The ZAP lesson found the SQL injection and missed the XSS, because its spider never discovered /greet. Nuclei has no spider at all — you tell it the path — so it found the XSS immediately and knows nothing about the SQL injection.
| ZAP active scan | Nuclei + custom templates | |
|---|---|---|
| Discovers endpoints | spiders, found 7 URLs | none — you supply paths |
| SQL injection | found | not tested |
Reflected XSS at /greet |
missed (never crawled) | found |
/debug/config exposure |
missed (never crawled) | found |
| Runtime | minutes | seconds |
They are complementary rather than competing. ZAP explores and attacks generically; Nuclei checks specific known things very fast. Nuclei's speed is what makes it the one you can run against production, because a GET on a path you own is not an attack.
Step 4 — Gate it
Nuclei finishes in seconds, so it fits in the pull request where ZAP's active scan does not:
# .github/workflows/dast.yml
- name: Nuclei (own templates only)
run: |
docker run -d --name app -p 127.0.0.1:3100:3000 app:${{ github.sha }}
sleep 10
nuclei -u http://127.0.0.1:3100 \
-t ./templates/ \
-severity critical,high \
-no-color -stats \
-exit-code 1-t ./templates/ runs only yours. The community set is worth running on a schedule against deployed environments, but in a pull-request gate it is thousands of requests to learn that you still have no CSP header — a finding that will not change between commits and will train people to ignore the job.
-severity critical,high keeps the info-level noise out of a blocking check.
Step 5 — Turn every incident into a template
The habit worth forming: when something is found in production — by a pentest, a bug bounty, or an outage — write the Nuclei template that detects it before you close the ticket.
id: internal-admin-panel-unauthenticated
info:
name: Admin panel reachable without auth
severity: critical
http:
- method: GET
path: ["{{BaseURL}}/admin", "{{BaseURL}}/internal/admin"]
matchers:
- type: status
status: [200]Now it is a regression test that runs on every deploy, in every environment, forever. That is the difference between fixing an issue and ensuring it stays fixed — and after a year of doing it, your template directory is a more accurate description of your actual risks than any vendor's ruleset.
Step 6 — Clean up
docker rm -f range-dastWhat you learned
- Thousands of community templates, all
[info], on an app leaking AWS credentials. They encode known issues, and your application is not one. - Two templates written in ten minutes found both real bugs, at
criticalandhigh. matchers-condition: andis what stops false positives: require the status and the content.- Nuclei and ZAP miss different things. ZAP found the SQLi and missed the XSS; Nuclei the reverse. Neither is a substitute.
- Gate on
-t ./templates/only. The community set on a pull request is thousands of requests confirming a finding that never changes. - Write a template for every incident. After a year that directory describes your real risk better than any vendor ruleset.