All articles
Deployment10 min readJanuary 13, 2026
CI/CDGitHub ActionsAutomationPre-commit

Pre-Deployment Security Scans: Catch Vulnerabilities Before They Hit Production

How to integrate security scanning into your CI/CD pipeline. Automated protection for AI-generated code.

Security Guide

Ship Fast, Ship Secure

AI tools let you build faster than ever. That speed is useless if you ship vulnerabilities. Pre-deployment scanning catches issues before they reach users.

The Security Pipeline

Code → Commit → PR → Build → Deploy
       ↑       ↑      ↑
   Pre-commit  PR   Build
    Hooks     Scan   Scan

Each stage catches different issues. Multiple layers = comprehensive protection.

Stage 1: Pre-Commit Hooks

Catch obvious issues before they enter git history.

Setup with Husky

bash
npm install husky --save-dev
npx husky install
npx husky add .husky/pre-commit "npm run security:check"

Basic Secret Detection

json
{
  "scripts": {
    "security:check": "node scripts/check-secrets.js"
  }
}
javascript
// scripts/check-secrets.js
const { execSync } = require('child_process')

const patterns = [ /sk_live_[A-Za-z0-9]{24,}/, // Stripe live key /sk_test_[A-Za-z0-9]{24,}/, // Stripe test key /AKIA[0-9A-Z]{16}/, // AWS access key /ghp_[A-Za-z0-9]{36}/, // GitHub token /sk-[A-Za-z0-9]{48}/, // OpenAI key /password\s*=\s*['"][^'"]+['"]/, // Hardcoded password ]

const stagedFiles = execSync('git diff --cached --name-only') .toString() .split('\n') .filter(f => f.match(/\.(ts

jstsxjsxjson
env)$/))

let hasSecrets = false

for (const file of stagedFiles) { if (!file) continue

try { const content = require('fs').readFileSync(file, 'utf8') for (const pattern of patterns) { if (pattern.test(content)) { console.error(⚠️ Potential secret found in \${file}) hasSecrets = true } } } catch (e) { // File might be deleted } }

if (hasSecrets) { console.error('\n❌ Commit blocked: Remove secrets before committing') process.exit(1) }

console.log('✅ No secrets detected')

Stage 2: PR Security Checks

Run comprehensive scans when code is ready for review.

GitHub Actions Workflow

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

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

jobs: security-scan: runs-on: ubuntu-latest

steps: - uses: actions/checkout@v4

- name: Setup Node uses: actions/setup-node@v4 with: node-version: '20'

- name: Install dependencies run: npm ci

- name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- p/security-audit p/secrets p/owasp-top-ten

- name: Dependency audit run: npm audit --audit-level=high

- name: Check for vulnerabilities run: npx shipready scan --ci

Required Status Checks

In GitHub:

  1. Settings → Branches → Add rule
  2. Require status checks: "Security Scan"
  3. Block merge on failure

Stage 3: Build-Time Validation

Validate configuration before deployment.

javascript
// scripts/validate-build.js
const requiredEnvVars = [
  'DATABASE_URL',
  'NEXTAUTH_SECRET',
]

const publicEnvVars = Object.keys(process.env) .filter(key => key.startsWith('NEXT_PUBLIC_'))

// Check for sensitive data in public vars const sensitivePatterns = [ /password/i, /secret/i, /private/i, /credential/i, ]

for (const key of publicEnvVars) { for (const pattern of sensitivePatterns) { if (pattern.test(key)) { console.error(⚠️ Warning: \${key} contains sensitive keyword but is public) } } }

// Verify required vars exist for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(❌ Missing required: \${envVar}) process.exit(1) } }

console.log('✅ Build validation passed')

Add to build:

json
{
  "scripts": {
    "prebuild": "node scripts/validate-build.js",
    "build": "next build"
  }
}

Semgrep Rules for AI Code

Custom rules for common AI-generated vulnerabilities:

yaml
# .semgrep/ai-code.yml
rules:
  - id: sql-template-literal
    patterns:
      - pattern: |
          $DB.query(...\${...$VAR}...)
    message: "SQL injection: Use parameterized queries"
    severity: ERROR
    languages: [typescript, javascript]

- id: dangerous-innerhtml pattern: dangerouslySetInnerHTML={{__html: $VAR}} message: "XSS risk: Ensure content is sanitized" severity: WARNING languages: [typescript, javascript]

- id: hardcoded-stripe-key pattern-regex: sk_(live|test)_[A-Za-z0-9]{24,} message: "Hardcoded Stripe key detected" severity: ERROR languages: [generic]

Dependency Scanning

npm audit in CI

yaml
- name: Security audit
  run: |
    npm audit --audit-level=critical
    if [ $? -ne 0 ]; then
      echo "Critical vulnerabilities found"
      exit 1
    fi

Automated Updates

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    groups:
      security:
        applies-to: security-updates

Scan Result Handling

Blocking vs. Warning

yaml
# Block deployment
  • name: Critical scan
run: npx scan --severity=critical --fail-on-error

# Warn but allow

  • name: Full scan
run: npx scan --all-severities continue-on-error: true

Reporting

yaml
- name: Upload scan results
  uses: actions/upload-artifact@v4
  with:
    name: security-report
    path: security-report.json

  • name: Comment on PR
uses: actions/github-script@v6 with: script: | const report = require('./security-report.json') const summary = Found \${report.critical} critical, \${report.high} high issues github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: summary })

Complete Pipeline Example

yaml
# .github/workflows/deploy.yml
name: Deploy

on: push: branches: [main]

jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm ci - run: npm audit --audit-level=high - uses: returntocorp/semgrep-action@v1 with: config: p/security-audit

test: needs: security runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: npm test

deploy: needs: [security, test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: '--prod'

Pipeline Checklist

PRE-COMMIT
==========
[ ] Husky installed
[ ] Secret detection script
[ ] Runs on staged files only

PR CHECKS ========= [ ] GitHub Actions workflow [ ] Semgrep scanning [ ] npm audit [ ] Required status check enabled

BUILD ===== [ ] Environment validation [ ] No public secrets check [ ] Build fails on critical issues

MONITORING ========== [ ] Scan results saved [ ] PR comments for visibility [ ] Alerts for failures

The Bottom Line

Automated scanning catches what humans miss. Add scanning at every stage: pre-commit for quick checks, PR for comprehensive analysis, build for final validation.

The best security bug is the one that never reaches production.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free