CI integration: blocking PRs on score regression
Security findings are a moving target. A clean main branch today doesn't help if a Friday merge introduces a CSP regression you don't notice until next quarter. The fix is to scan in CI and block PRs that would degrade your security posture.
UnveilScan ships a CLI binary (unveilscan-cli) and a packaged GitHub
Action that does this in 6 lines of YAML. Here's the setup.
The CLI
The CLI launches a scan via your Bearer API token, polls until completion, and exits with a code based on the worst severity found:
$ unveilscan-cli -domain example.com -token $UNVEILSCAN_TOKEN \
-profile basic -fail-on high
✔ Scan completed: 89/100 (A)
✔ 0 critical, 0 high, 2 medium, 5 low findings
$ echo $?
0
Exit codes:
| Exit code | Meaning |
|---|---|
| 0 | Scan succeeded; nothing at or above -fail-on severity |
| 1 | Scan succeeded but found findings at or above -fail-on |
| 2 | API or network error (couldn't reach UnveilScan) |
| 3 | CLI argument invalid (bad domain, missing token) |
Most CIs interpret a non-zero exit as failure. Plug into .gitlab-ci.yml,
Jenkins pipelines, CircleCI, etc. — same pattern.
The GitHub Action
Drop this in .github/workflows/security.yml:
name: UnveilScan
on:
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: unveiltech/unveilscan-action@v1
with:
domain: example.com
token: ${{ secrets.UNVEILSCAN_TOKEN }}
profile: extended
fail-on: high
On every PR the Action runs an Extended scan against your production domain. If any finding is HIGH or worse, the check fails. The PR can't merge (assuming you required the check in branch protection rules).
What "production domain" means in this context
The first wrinkle: you can't scan code that hasn't shipped yet. Two options:
- Scan staging. Most teams have a
staging.example.comthat gets the PR's code via a Vercel preview / Netlify preview / dedicated CI environment. Point the Action at staging. - Scan main after deploy. The Action runs on push to main, blocks the deploy if it'd worsen the posture. Closer to "tripwire" than "gate".
Best with both: PR scan against staging (catches regressions early) + main scan as tripwire (catches what slipped through staging differences).
Common configuration patterns
Strict — fail on any new HIGH
fail-on: high
Permissive — only fail on regression
You want to ship a new feature even if it has 1 LOW finding. Block only if the score drops compared to baseline. Requires a custom step:
- uses: unveiltech/unveilscan-action@v1
with:
domain: example.com
token: ${{ secrets.UNVEILSCAN_TOKEN }}
output: json
output-path: scan.json
- name: Compare score
run: |
SCORE=$(jq .score.global scan.json)
BASELINE=85 # set per-team
if [ "$SCORE" -lt "$BASELINE" ]; then
echo "Score $SCORE below baseline $BASELINE"
exit 1
fi
Active scan in CI
- uses: unveiltech/unveilscan-action@v1
with:
domain: staging.example.com
token: ${{ secrets.UNVEILSCAN_TOKEN }}
profile: active
ack-active: true # required gate
fail-on: high
Triple-gated (ownership + ack + non-destructive probes — see CLAUDE.md §2). Costs a license slot per domain on UnveilScan but covers CVE probing.
Token management
Generate a Bearer token from /me/tokens in the UnveilScan SPA. Scope:
Bearer tokens currently scope to "all the user's owned domains" (no per-domain
scoping yet — coming in v2). Treat the token like a deploy key:
- Store in GitHub Actions secrets, not in code.
- Rotate quarterly.
- Use a dedicated CI service account (not a personal account), so revoking it doesn't kill the human user.
Cost considerations
Every PR triggers a scan. On the Free tier (3 Extended scans / month) you'll exhaust the budget after 3 PRs. For active CI use, a license is the right model — it gives unlimited Extended scans on pinned domains. Pricing.
Reading the output
Job logs show a structured summary. For richer diff (which findings appeared / were
fixed since last scan), the JSON output includes a delta block when
compared against the previous scan on the same domain.
Set up CI scanning
UnveilScan CLI + GitHub Action. License covers unlimited CI scans on pinned domains.
See pricing