Integrating Nuclei into a CI/CD GitHub Actions Pipeline for Continuous Scanning
A step-by-step guide to Nuclei CI/CD GitHub Actions integration: post-deploy job, tight scoping, targeted alerting, and template management past week one.
A one-off security audit captures a snapshot at a single point in time. Three weeks later, a deployment introduces a poorly protected endpoint, a vulnerable dependency, or a missing security header, and the snapshot is worthless. For a small team without a dedicated pentester, the real question is not "did we run an audit", it is "do we know, at every production deployment, whether the attack surface just got worse". That is exactly what Nuclei wired into CI/CD delivers: a scan that runs after every deployment, on staging and on production, without adding friction to the pipeline.
This guide covers the full integration: the GitHub Actions job, scoping the scan so it stays fast, alerting that separates the critical from the noise, and the ongoing management of the template set (the part most tutorials skip, and the one that quietly kills the integration within a couple of months).
The GitHub Actions job: post-deploy scanning
The principle: Nuclei does not run on every commit to a development branch (too early, the target URL does not exist yet). It runs once a deployment has succeeded, against the environment that is actually exposed.
name: post-deploy-scan
on:
workflow_run:
workflows: ["deploy-staging"]
types: [completed]
jobs:
nuclei-scan:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Nuclei templates
uses: actions/cache@v4
with:
path: ~/.local/nuclei-templates
key: nuclei-templates-${{ github.run_id }}
restore-keys: |
nuclei-templates-
- name: Run Nuclei
uses: projectdiscovery/nuclei-action@main
with:
target: "https://staging.example.com"
templates: "cves/,exposed-panels/,misconfiguration/"
severity: "critical,high"
output: nuclei-results.json
json-export: nuclei-results.json
- name: Publish the report
uses: actions/upload-artifact@v4
with:
name: nuclei-report
path: nuclei-results.jsonThe workflow_run trigger is deliberate: it decouples the scan from the deploy workflow itself. If the deployment fails, the scan does not run, there is no point scanning a version that was never shipped. Caching the template set (actions/cache) avoids re-downloading tens of megabytes on every run, which directly affects job duration and therefore runner cost.
Scoping the scan: keeping CI from turning into a bottleneck
An unbounded Nuclei scan, with the full template set and no severity filter, can take dozens of minutes. On a pipeline that needs to render a verdict in a few minutes, that is unacceptable. Three levers bring the scan back to a duration compatible with CI:
- Tags. Restrict to the categories relevant to the application (
cves,exposed-panels,misconfiguration,takeovers) instead of running the entire template repository, which covers technology stacks you likely do not run. - Severity.
-severity critical,highstrips out informational noise at the source, before the report is even generated. Lower severities belong in a more thorough weekly scan, not in the deployment's critical path. - Targeting. Scan the exposed staging URL, not the entire internal network. Nuclei accepts a precise target list (
-l targets.txt) rather than a whole range, which mechanically shortens execution time.
Always attach a timeout at the GitHub Actions job level (timeout-minutes: 10): a scan that hangs blocks the whole pipeline behind it, including the deployments queued after it.
timeout-minutes: 10Alerting: separating what needs attention from what just needs archiving
A job that drops a JSON file into an artifact nobody opens is not alerting, it is archiving. Nuclei's structured output exists precisely to automate that triage.
nuclei -u https://staging.example.com \
-severity critical,high \
-jsonl -o results.jsonlFrom that file, a small script (bash, or the github-script action) decides what happens next: open a GitHub issue if critical or high results appear, notify a Slack channel through a webhook, or fail the job (a gate) if the threshold is crossed.
- name: Gate on critical results
run: |
COUNT=$(jq '. | select(.info.severity=="critical")' results.jsonl | jq -s 'length')
if [ "$COUNT" -gt 0 ]; then
echo "::error::$COUNT critical vulnerability(ies) detected"
exit 1
fi
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "Nuclei scan: critical vulnerability detected on staging, see the job artifact."}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}The gate logic is deliberately asymmetric: critical and high block (or at minimum alert loudly), everything else is archived for periodic review. Without that hierarchy, a team either ends up ignoring every alert (the scan becomes an empty ritual) or blocking deployment over a minor finding (the scan becomes an obstacle to route around).
Managing and versioning the template set inside the pipeline
This is the part most integration guides leave out, and it is the one that decides whether the integration lasts. Two habits to put in place from day one:
Pin the template version the pipeline uses. A scan that silently pulls "the latest version" on every run introduces an uncontrolled variable: a template updated yesterday can surface a new false positive this morning, with no change on the application side at all. Fixing a reference (a version tag, a commit of the template repository) and only moving it forward through a planned, tested update produces reproducible behavior.
Schedule the update separately from the deployment scan. A distinct, weekly cron job updates the template set, runs it against a reference environment, and only promotes the new version if it does not degrade the known false-positive rate.
on:
schedule:
- cron: "0 3 * * 1"A pipeline scanning with a template set frozen six months ago misses recent CVEs. A pipeline that absorbs every update unchecked accumulates false positives until the team disables the job. The answer is neither extreme: it is a maintained, versioned, upstream-validated template feed that the pipeline consumes with confidence.
Avoiding false positives that break the build
A scan that fails on a Friday evening over a false positive, blocking an urgent deployment, kills the team's trust in the tool in a single occurrence. Two safeguards limit that risk:
- An explicit allowlist. Some known and accepted findings (a header intentionally absent on an internal endpoint, for example) need to be excludable from the gate without editing the template itself. A versioned list of excluded template IDs, kept in the repository, documents that choice instead of hiding it inside a silent exception.
- Strict matchers at the template level. A template that declares a vulnerability from a status code alone, without checking the response body, generates noise the moment a generic error page returns the same code. Rigor happens upstream, in the quality of the template itself, not only in the configuration of the pipeline that runs it.
In summary
Wiring Nuclei into a GitHub Actions pipeline takes three things: a job that runs at the right moment (after deployment, not on every commit), scoping that keeps CI fast (tags, severity, precise targets, timeout), and alerting that prioritizes rather than surfacing everything at the same level. But the technical integration is not enough on its own: a tutorial that stops there loses its value within two months, as soon as the template set goes stale or false positives pile up unchecked.
That continuity is exactly what the ThreatClaw Nuclei feed covers: a template set validated against the real engine, prioritized by active exploitation and exploit-probability scoring, updated on a controlled schedule, ready for your pipeline to consume without your team having to track every new CVE itself.
Related articles
A Sigma pipeline CI/CD detection as code setup: validate, translate, test, deploy, with ATT&CK fixtures and experimental-to-stable governance for untested rules.
Deserialization of untrusted data yields RCE on on-premise SharePoint. In the KEV, exploited by Storm-2603. Here is the Sigma rule on w3wp and Nuclei detection.
A poorly validated override cookie opens an unauthorized GlobalProtect session. Score raised to 7.8, in the KEV, exploited. Nuclei detection and mitigation.
An unsigned OIDC token grants technician access to SimpleHelp RMM. CVSS 10, in the KEV, exploited to deliver stealers. Nuclei detection and accounts to watch.