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 ScanEach stage catches different issues. Multiple layers = comprehensive protection.
Stage 1: Pre-Commit Hooks
Catch obvious issues before they enter git history.
Setup with Husky
npm install husky --save-dev
npx husky install
npx husky add .husky/pre-commit "npm run security:check"Basic Secret Detection
{
"scripts": {
"security:check": "node scripts/check-secrets.js"
}
}// 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
js tsx jsx json
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
# .github/workflows/security.yml
name: Security Scanon:
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:
- Settings → Branches → Add rule
- Require status checks: "Security Scan"
- Block merge on failure
Stage 3: Build-Time Validation
Validate configuration before deployment.
// 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:
{
"scripts": {
"prebuild": "node scripts/validate-build.js",
"build": "next build"
}
}Semgrep Rules for AI Code
Custom rules for common AI-generated vulnerabilities:
# .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
- name: Security audit
run: |
npm audit --audit-level=critical
if [ $? -ne 0 ]; then
echo "Critical vulnerabilities found"
exit 1
fiAutomated Updates
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
security:
applies-to: security-updatesScan Result Handling
Blocking vs. Warning
# 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: trueReporting
- 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
# .github/workflows/deploy.yml
name: Deployon:
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 onlyPR 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.