id: npm-audit-ci-gate
namespace: company.team
description: |
Run npm audit in production mode, count vulnerabilities by severity, and
fail the gate plus alert Slack when critical or high findings appear.
triggers:
- id: pre_deploy_webhook
type: io.kestra.plugin.core.trigger.Webhook
description: Call from the deploy pipeline right before shipping; the gate runs
before artifacts move.
key: npm-audit
- id: nightly_dependency_scan
type: io.kestra.plugin.core.trigger.Schedule
description: Nightly audit catches newly published CVEs against the same
lockfile; shipped disabled.
cron: "0 2 * * *"
disabled: true
inputs:
- id: repo_url
type: STRING
defaults: "https://github.com/owner/repo.git"
description: Repo containing package.json and package-lock.json.
- id: max_critical
type: INT
defaults: 0
description: Critical vulnerabilities tolerated before the gate fails.
tasks:
- id: run_audit
type: io.kestra.plugin.scripts.shell.Commands
description: Clone, npm ci, then npm audit --json; counts come out via the
stdout outputs protocol. A broken audit reports worst case.
containerImage: node:20-alpine
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
commands:
- |
apk add --no-cache git >/dev/null 2>&1
git clone --depth 1 --quiet "{{ inputs.repo_url }}" app
cd app
npm ci --no-audit --no-fund >/dev/null 2>&1
npm audit --omit=dev --json > audit.json 2>/dev/null || true
cat > count.py <<'PYEOF'
import json
try:
report = json.load(open("audit.json"))
counts = report.get("metadata", {}).get("vulnerabilities", {})
critical = counts.get("critical", 0)
high = counts.get("high", 0)
except Exception:
critical, high = 1, 0
print(f"critical {critical}, high {high}")
print("::" + json.dumps({"outputs": {"critical": critical, "high": high}}) + "::")
PYEOF
(apk add --no-cache python3 >/dev/null 2>&1; python3 count.py) || node -e "try{const r=require('./audit.json');console.log('::'+JSON.stringify({outputs:{critical:r.metadata.vulnerabilities.critical,high:r.metadata.vulnerabilities.high}})+'::')}catch(e){console.log('::'+JSON.stringify({outputs:{critical:1,high:0}})+'::')}"
- id: check_gate
type: io.kestra.plugin.core.flow.If
description: One branch for the counts - any breach alerts Slack with the numbers.
condition: "{{ outputs.run_audit.vars.critical > inputs.max_critical }}"
then:
- id: alert_breach
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Post both counts so the message says how bad it is.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": ":rotating_light: npm audit gate BREACH on {{ inputs.repo_url }} - critical {{ outputs.run_audit.vars.critical }} (max {{ inputs.max_critical }}), high {{ outputs.run_audit.vars.high }}. Pin or patch before shipping. Execution {{ execution.id }}."
}
else:
- id: log_clean
type: io.kestra.plugin.core.log.Log
description: Record the pass so the execution history is your dependency trend.
message: "npm audit within budget: critical {{ outputs.run_audit.vars.critical
}}, high {{ outputs.run_audit.vars.high }}."
errors:
- id: alert_audit_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert when the audit fails - no result must ever read as a clean tree.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "npm audit FAILED in flow {{ flow.id }} (execution {{ execution.id }}). Check the lockfile and registry access."
}
outputs:
- id: vuln_counts
type: JSON
description: 'Vulnerability counts, e.g. {"critical": 0, "high": 3}'
value: "{{ outputs.run_audit.vars | toJson }}"