CLI GUIDESMarch 26, 2026·8 min read

GitHub Actions Security Scanner:
Block Vulnerable Code
Before It Ships

The best time to catch a vulnerability is before it merges. This guide walks you through adding OWASP Top 10 + OWASP LLM Top 10 scanning to your GitHub Actions pipeline — with a complete, production-ready YAML template and PR blocking on critical findings.

Bottom Line Up Front

Add a .github/workflows/security.yml file with three steps: checkout, install Custodia CLI, run scan. Use --fail-on critical to block the PR on critical findings. The workflow covers OWASP Top 10, OWASP LLM Top 10, EU AI Act, and NIST AI RMF in a single pass — no additional tools required. Scan results are uploaded as a workflow artifact and linked in a PR comment automatically.

Why Shift-Left Security in CI/CD?

Fixing a vulnerability in production costs 30× more than fixing it at commit time — in engineering hours, incident response, and potential breach costs. The CI pipeline is the last automated gate before code reaches customers. Every critical OWASP finding that passes through is a breach waiting to happen.

For AI-powered applications, the case is stronger: OWASP LLM Top 10 vulnerabilities like prompt injection and excessive agency are invisible to traditional CI tools. Without a dedicated AI security scan in your pipeline, LLM-specific vulnerabilities have no automated detection gate.

Comparing CI/CD Security Scanner Options

Most teams end up running 3–4 security tools in CI to get reasonable coverage. Custodia is the first single-tool option that covers traditional web security, AI security, and compliance frameworks.

ToolOWASP Top 10OWASP LLMEU AI ActDependenciesPR CommentsBlock Mode
Custodia
Snyk Action
CodeQL
OWASP ZAP
Semgrep CI
Trivy

Setting Up Custodia in GitHub Actions

01
Add your API key as a GitHub Secret

Settings → Secrets and variables → Actions → New repository secret. Name: CUSTODIA_API_KEY

02
Create .github/workflows/security.yml

The easiest way is the Custodia Security Scan action on the GitHub Marketplace — two steps, no manual CLI install needed:

# .github/workflows/security.yml
name: Security Scan

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  security:
    name: OWASP + AI Security Scan
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: contactdavidpersonal-code/custodia-scan-action@v1
        with:
          api-key: ${{ secrets.CUSTODIA_API_KEY }}
03
Add PR comment with findings summary
      # Add this step after the scan step:
      - name: Comment findings on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const report = JSON.parse(fs.readFileSync('report.json', 'utf8'));
            const { score, findings } = report;

            const critical = findings.filter(f => f.severity === 'CRITICAL');
            const high     = findings.filter(f => f.severity === 'HIGH');

            const body = [
              '## 🔐 Custodia Security Scan',
              `**Score:** ${score}/100`,
              `**Critical:** ${critical.length}  **High:** ${high.length}`,
              '',
              critical.length > 0
                ? '⛔ **Deploy blocked** — critical findings must be resolved.'
                : '✅ No critical findings. Review high severity before merge.',
              '',
              `[View full report](https://custodia.dev/reports/${report.reportId})`,
            ].join('\n');

            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body,
            });
04
Speed up CI with incremental scanning (optional)
      # Only scan files changed in this PR
      - name: Get changed files
        id: changed
        run: |
          echo "files=$(git diff --name-only origin/main HEAD             | grep -E '^src/' | tr '\n' ' ')" >> $GITHUB_OUTPUT

      - name: Run incremental scan
        if: steps.changed.outputs.files != ''
        run: custodia scan ${{ steps.changed.outputs.files }} --fail-on critical
        env:
          CUSTODIA_API_KEY: ${{ secrets.CUSTODIA_API_KEY }}

Setting the Right Fail Conditions

Overly aggressive blocking causes security fatigue — developers start bypassing checks. Overly lenient blocking means critical vulnerabilities flow to production. The right policy:

CRITICAL
Block CI entirely
SQL injection, IDOR, prompt injection enabling RCE — non-negotiable
HIGH
Block + PR comment
Significant risk but may have business context for exceptions
MEDIUM
PR comment only
Track and close before next sprint, not a blocker
LOW/INFO
Report artifact only
Informational — never block deploys on these
# Custodia --fail-on flag maps exactly to this policy:
custodia scan . --fail-on critical          # block on CRITICAL only
custodia scan . --fail-on high             # block on HIGH+
custodia scan . --fail-on medium           # block on MEDIUM+ (strict)
custodia scan .                            # never block, always report
CI PIPELINE OUTPUT

What Your GitHub Actions
Workflow Output Looks Like

// GitHub Actions · security.yml · PR #47
Run custodia scan . --fail-on critical

  [TRIAGE]   Scanning 1,247 files (312 changed in this PR)...
  [TRIAGE]   LLM usage detected: openai@4.28.0
  [AI AGENT] Activating OWASP LLM Top 10 checks...

  [A01] CRITICAL  Broken Access Control
          src/api/scans/[id]/route.ts:31  ← NEW in this PR

  [LLM01] CRITICAL  Prompt Injection
          src/api/chat/route.ts:18  ← NEW in this PR

  [A07] HIGH     Auth Failure — jwt.decode() used
          src/middleware/verify.ts:44

  ───────────────────────────────────────────────────────
  ⛔ CRITICAL findings detected — deployment blocked
  Exit code: 1

  Full report: custodia.dev/reports/ci_8mKx2p
  AI fix prompts ready in dashboard
Set Up CI Scanning FreeRead the Docs

Frequently Asked Questions

How do I add a security scanner to GitHub Actions?

Add a .github/workflows/security.yml file. Install Custodia CLI, run custodia scan . --fail-on critical, and the workflow blocks the PR if any critical vulnerabilities are found. Full YAML template is in this guide.

What is the difference between Custodia, Snyk, and CodeQL for CI/CD?

CodeQL performs deep semantic analysis but requires per-language configuration and does not cover OWASP LLM Top 10. Snyk focuses on dependency vulnerabilities and some SAST. Custodia covers OWASP Top 10, OWASP LLM Top 10, EU AI Act, and NIST AI RMF in one pass — no additional tools required for teams shipping AI features.

Should I block deployments on security findings?

Block on critical and high severity. Warn on medium. Never block on informational findings. Overly aggressive gates cause security fatigue and get bypassed. The right policy: fail CI on CRITICAL, post PR comments for HIGH/MEDIUM, and upload full report as an artifact for all findings.

How do I store my Custodia API key securely in GitHub Actions?

Add it as a GitHub repository secret: Settings → Secrets and variables → Actions → New repository secret. Name: CUSTODIA_API_KEY. Reference in workflow as ${{ secrets.CUSTODIA_API_KEY }}. Never hardcode keys in YAML files — they are stored in version control.

How long does a Custodia scan take in CI?

Under 90 seconds for most codebases under 100k lines. Custodia uses SHA-256 codebase caching — unchanged files are skipped on subsequent scans. Large monorepos can use git diff to scope scans to changed directories and reduce CI scan time to under 30 seconds.

Related Articles

OWASP LLM Top 10 Scanner
Full OWASP LLM coverage — all 10 AI vulnerability categories and why Snyk misses every one of them.
OWASP Top 10 Code Review Guide
Find SQL injection, broken access control, and XSS with vulnerable/safe code pairs for every category.
Shift Left. Start Today.

Catch Vulnerabilities
At The PR Gate

Free tier includes CI scanning. Pro plan ($39/mo) unlocks OWASP LLM Top 10, AI fix prompts, and compliance reports.

Get Started FreeView CLI Docs →