
Ship Gate
- 423 installs
- 23.8k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
ship-gate is a stack-agnostic pre-production audit agent skill that runs 89 checks across eight categories, intercepts deploy commands, and reports remediation guidance for developers who need a go-live gate before produ
About
ship-gate from alirezarezvani/claude-skills engineering/skills is a pre-production audit skill version 1.0.0 that scans codebases across eight categories—SEC, DB, CODE, DEP, AI, DEPLOY, FE, and OBS—totaling 89 automated and manual checks defined in references/checks.md. It detects project stack, runs auto-scannable patterns from references/patterns.md in security-first order, collects manual confirmations, and returns DO NOT SHIP, SHIP WITH CAUTION, or CLEAR TO SHIP verdicts while intercepting phrases like push to production or go live. The skill reports file locations and fixes guidance but does not patch code or provision CI/CD. Reach for ship-gate when a team wants a structured pre-launch checklist across security, database, deployment, AI/LLM, dependencies, frontend quality, and observability before approving production release.
- Pre-release checklist enforcement
- Test and security sign-off criteria
- Rollout and rollback readiness
- Blocking vs warning gate rules
- Release approval documentation
Ship Gate by the numbers
- 423 all-time installs (skills.sh)
- Ranked #54 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill ship-gateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 423 |
|---|---|
| repo stars | ★ 23.8k |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you run a pre-production go-live audit?
Run a structured pre-ship gate checklist covering tests, security, docs, and rollout risks before approving production release.
Who is it for?
Engineering leads running a stack-agnostic preflight before production deploy who want deploy-command interception and categorized findings without setting up new CI pipelines.
Skip if: CI/CD pipeline authoring, infrastructure provisioning, post-deploy monitoring setup, or teams that need the skill to auto-fix findings instead of reporting them.
When should I use this skill?
The user says run ship gate, am I ready to ship, push to production, go live checklist, or pre-launch audit.
What you get
Categorized CRITICAL, HIGH, and ADVISORY findings, manual check confirmations, and a DO NOT SHIP, SHIP WITH CAUTION, or CLEAR TO SHIP verdict.
- categorized audit findings
- manual confirmation prompts
- ship verdict
By the numbers
- Runs 89 checks across 8 audit categories
- SEC category includes 15 automated and 3 manual checks
- Skill metadata version 1.0.0 in engineering/skills/ship-gate/SKILL.md
Files
Ship Gate
Pre-production audit that scans a codebase and reports pass/fail/manual across 8 categories before anything ships.
Intercept Behavior
When the user says "push to production", "deploy", "ship it", "go live", or similar deploy-intent phrases, do NOT proceed with deployment. Instead:
1. Ask: "Have you run the ship gate? Want me to scan now?" 2. If yes, run the full audit below. 3. If the user says they already ran it, ask when. If more than 24 hours ago or if code changed since, recommend re-running.
How It Works
Step 1: Detect Stack
Run these checks in order to identify the project stack:
Framework detection:
package.json exists -> Node.js project
"next" in dependencies -> Next.js
"react" in dependencies -> React (if not Next.js)
"vue" in dependencies -> Vue
"svelte" in dependencies -> Svelte
"astro" in dependencies -> Astro
"express" in dependencies -> Express
"fastify" in dependencies -> Fastify
"hono" in dependencies -> Hono
requirements.txt or pyproject.toml -> Python project
"django" present -> Django
"flask" present -> Flask
"fastapi" present -> FastAPI
go.mod exists -> Go project
Cargo.toml exists -> Rust project
Database detection:
"@supabase/supabase-js" in package.json -> Supabase
supabase/ directory exists -> Supabase
"prisma" in dependencies -> Prisma (check schema for DB type)
"mongoose" in dependencies -> MongoDB
"pg" or "postgres" in dependencies -> PostgreSQL
firebase.json or .firebaserc exists -> Firebase
Deploy target detection:
vercel.json or .vercel/ exists -> Vercel
netlify.toml exists -> Netlify
Dockerfile exists -> Docker/VPS
fly.toml exists -> Fly.io
railway.json exists -> Railway
.platform/applications.yaml -> Platform.sh
Auth detection:
"@clerk" in dependencies -> Clerk
"next-auth" in dependencies -> NextAuth
"@supabase/auth-helpers" in deps -> Supabase Auth
"firebase/auth" in imports -> Firebase Auth
AI/LLM detection:
"openai" in dependencies -> OpenAI
"@anthropic-ai/sdk" in dependencies -> Claude API
"@google/generative-ai" in deps -> GeminiReport detected stack before proceeding. This determines which checks are relevant. Checks tagged with a specific stack in references/checks.md are skipped if that stack is not detected.
Step 2: Run Automated Checks
Run categories in this order: SEC, DB, CODE, DEP, AI, DEPLOY, FE, OBS. Security and database first because they produce the most critical findings.
For each category, run every auto-scannable check from references/checks.md using the patterns in references/patterns.md.
Report progress after each category completes:
[1/8] Security: 3 FAIL, 12 PASS, 3 SKIP
[2/8] Database: 1 FAIL, 5 PASS, 6 SKIP
...Report results as:
- PASS: check passed
- FAIL: issue found (with file path and line number)
- SKIP: not applicable to this stack
Step 3: Manual Confirmation
For checks that cannot be automated (backup restore tested, rollback plan exists, staging test passed), present them as a checklist and ask the user to confirm each one.
Step 4: Verdict
Classify results into three severities:
- CRITICAL: must fix before shipping (secrets exposed, no auth on routes,
no HTTPS, SQL injection vectors, no RLS on Supabase tables)
- HIGH: should fix before shipping (no error boundaries, no rate limiting,
console.logs in production, no pagination)
- ADVISORY: recommended but not blocking (no OG tags, no custom 404,
no analytics, no SBOM)
Final output:
SHIP GATE REPORT
================
Stack: Next.js + Supabase + Vercel
Scan time: 12s
CRITICAL (3 items, must fix)
FAIL [SEC-01] API key found in src/lib/api.ts:14
FAIL [DB-07] RLS not enabled on "profiles" table
FAIL [SEC-05] No CSRF protection on /api/checkout
HIGH (5 items, should fix)
FAIL [CODE-01] 12 console.log statements in production code
FAIL [CODE-03] Empty catch block in src/utils/auth.ts:45
FAIL [DEP-04] 3 critical npm audit vulnerabilities
FAIL [DEPLOY-05] No rollback plan documented
MANUAL [DEPLOY-06] Staging test not confirmed
ADVISORY (4 items, recommended)
FAIL [FE-01] Missing OG meta tags
FAIL [FE-03] No custom 404 page
PASS [OBS-01] Error monitoring configured
SKIP [AI-01] No AI/LLM usage detected
VERDICT: DO NOT SHIP (3 critical issues)
Fix critical items and re-run.If zero critical items remain, verdict is: CLEAR TO SHIP. If only high items remain, verdict is: SHIP WITH CAUTION (acknowledge risks).
Categories
Eight categories, each with a code prefix. Full check details in references/checks.md.
| Prefix | Category | Auto | Manual | Tool |
|---|---|---|---|---|
| SEC | Security | 15 | 3 | 0 |
| DB | Database | 7 | 5 | 0 |
| DEPLOY | Deployment | 3 | 8 | 0 |
| CODE | Code Quality | 11 | 0 | 1 |
| AI | AI/LLM Security | 5 | 3 | 0 |
| DEP | Dependencies | 5 | 0 | 1 |
| FE | Frontend Quality | 7 | 3 | 0 |
| OBS | Observability | 2 | 5 | 0 |
Scope
This skill audits. It does not fix. When it finds issues, it reports them with file locations and remediation guidance. The user or another skill (systematic-debugging, backend-patterns, shadcn-stack) handles the fix.
This skill does not:
- Set up CI/CD pipelines
- Provision infrastructure
- Configure monitoring tools
- Run after deployment (it is pre-deploy only)
Integration Points
- karpathy-coder: run ship-gate after karpathy-check passes — simplicity first, then production readiness
- adversarial-reviewer: deep security review for items ship-gate flags as critical
- security-pen-testing: penetration testing methodology for SEC-category findings
- code-reviewer: general code quality review complements ship-gate's automated checks
Ship Gate: Complete Check Reference
All checks organized by category with ID, description, detection method, severity, and remediation guidance.
Table of Contents
- SEC: Security (18 checks)
- DB: Database (12 checks)
- DEPLOY: Deployment (13 checks)
- CODE: Code Quality (14 checks)
- AI: AI/LLM Security (8 checks)
- DEP: Dependencies and Supply Chain (7 checks)
- FE: Frontend Quality (10 checks)
- OBS: Observability (7 checks)
Detection methods:
- auto: Claude scans the codebase using grep, find, or file inspection
- tool: Claude runs an external tool (npm audit, etc.)
- manual: Claude asks the user to confirm
---
SEC: Security
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| SEC-01 | No API keys or secrets in frontend code | auto | critical | all |
| SEC-02 | Every route checks authentication | auto | critical | all |
| SEC-03 | HTTPS enforced, HTTP redirected | manual | critical | all |
| SEC-04 | CORS locked to specific domain, not wildcard | auto | critical | all |
| SEC-05 | CSRF protection on state-changing endpoints | auto | critical | all |
| SEC-06 | Input validated and sanitized server-side | auto | high | all |
| SEC-07 | Rate limiting on auth and sensitive endpoints | auto | high | all |
| SEC-08 | Passwords hashed with bcrypt or argon2 | auto | critical | all |
| SEC-09 | Auth tokens have expiry | auto | high | all |
| SEC-10 | Sessions invalidated on logout (server-side) | manual | high | all |
| SEC-11 | CSP headers configured | auto | high | all |
| SEC-12 | JWT not using alg:none or weak secrets | auto | critical | all |
| SEC-13 | No eval() or dangerouslySetInnerHTML without sanitization | auto | high | js/ts |
| SEC-14 | No sensitive data in URL parameters or logs | auto | high | all |
| SEC-15 | Cookie security flags set (HttpOnly, Secure, SameSite) | auto | high | all |
| SEC-16 | File upload validates type, size, no path traversal | auto | high | all |
| SEC-17 | No hardcoded secrets in .env committed to repo | auto | critical | all |
| SEC-18 | .env files listed in .gitignore | auto | critical | all |
SEC-01: No API keys or secrets in frontend code
Scan all files in src/, app/, pages/, public/, components/ for patterns matching API keys, tokens, and secrets. See patterns.md for the full regex list.
Remediation: Move secrets to environment variables. Use server-side API routes to proxy requests that require secrets.
SEC-02: Every route checks authentication
For Next.js: check middleware.ts/js exists and covers protected routes. For Express: check that auth middleware is applied to route handlers. For Django: check @login_required or permission decorators. For generic: search for unprotected route definitions.
Remediation: Add authentication middleware. Audit every endpoint and classify as public or protected.
SEC-04: CORS not wildcard
Search for cors({ origin: '*' }), Access-Control-Allow-Origin: *, or equivalent in the detected framework.
Remediation: Set CORS origin to your specific domain(s).
SEC-05: CSRF protection
Check for CSRF token generation and validation on POST/PUT/DELETE routes. For Next.js Server Actions, verify they use built-in CSRF protection.
Remediation: Add CSRF middleware or use framework-native CSRF protection.
SEC-06: Input validation server-side
Search for request body usage (req.body, request.json, request.form) without validation library imports (zod, yup, joi, class-validator, pydantic). Check if raw user input flows directly into database queries or business logic.
Remediation: Add input validation with zod, yup, or joi on every endpoint that accepts user input.
SEC-07: Rate limiting
Search for rate limiting middleware (express-rate-limit, @upstash/ratelimit, rate-limiter-flexible, slowapi). Check auth routes and sensitive endpoints.
Remediation: Add rate limiting middleware. Start with auth endpoints (login, register, password reset) and any endpoint that sends emails or costs money.
SEC-09: Auth token expiry
Search JWT sign calls for expiresIn/exp claims. Check if tokens are created without expiry. Search for sign(, jwt.encode(, createToken.
Remediation: Set token expiry. Access tokens: 15-60 minutes. Refresh tokens: 7-30 days. Never issue tokens without expiry.
SEC-14: Sensitive data in URLs or logs
Search for query parameters containing keywords like password, token, secret, key, ssn, credit_card. Search logging statements that log full request objects or sensitive fields.
Remediation: Send sensitive data in request body or headers, never in URL parameters. Redact sensitive fields before logging.
SEC-16: File upload validation
Search for file upload handlers (multer, formidable, busboy, UploadedFile). Check if file type, size, and path are validated.
Remediation: Validate file MIME type against an allowlist. Set maximum file size. Sanitize filenames. Store outside webroot.
SEC-12: JWT security
Search for alg: 'none', algorithm: 'none', or JWT secrets shorter than 32 characters.
Remediation: Use RS256 or HS256 with a strong secret (32+ characters). Never allow alg:none.
SEC-17: No hardcoded secrets in .env committed
Check git history for .env files: git log --all --name-only | grep .env Check if .env exists in the working tree and is not in .gitignore.
Remediation: Add .env* to .gitignore. Rotate any exposed secrets. Use git filter-branch or BFG to remove from history if needed.
---
DB: Database
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| DB-01 | Backups configured and tested | manual | critical | all |
| DB-02 | Backup restore tested (not just backup) | manual | critical | all |
| DB-03 | Parameterized queries everywhere | auto | critical | all |
| DB-04 | Separate dev and production databases | manual | high | all |
| DB-05 | Connection pooling configured | auto | high | all |
| DB-06 | Migrations in version control | auto | high | all |
| DB-07 | RLS enabled on all tables | auto | critical | supabase |
| DB-08 | No service_role key in client-side code | auto | critical | supabase |
| DB-09 | Anon key not used for writes without RLS | auto | high | supabase |
| DB-10 | Storage bucket policies configured | auto | high | supabase |
| DB-11 | App uses a non-root DB user | manual | high | all |
| DB-12 | No PII stored unencrypted | auto | high | all |
DB-03: Parameterized queries
Search for string concatenation in SQL queries:
- Template literals with SQL keywords: `
SELECT ... ${` - String concatenation:
"SELECT " + variable - f-strings with SQL:
f"SELECT ... {variable}"
Remediation: Use parameterized queries or ORM methods.
DB-07: RLS enabled (Supabase)
Search migration files for CREATE TABLE without a corresponding ALTER TABLE ... ENABLE ROW LEVEL SECURITY statement. Also check for CREATE POLICY statements.
Remediation: Enable RLS on every table and create appropriate policies.
DB-08: No service_role key in client code
Search frontend directories (src/, app/, components/, pages/) for service_role, supabase_service_role, or the actual key pattern eyJ... used with createClient on the client side.
Remediation: Use service_role only in server-side code (API routes, Edge Functions, server actions).
DB-05: Connection pooling
Search for database connection configuration. Check for pool settings (max, min, idle timeout). For Supabase, check if using connection pooler URL (port 6543) vs direct (port 5432).
Remediation: Use connection pooling for production. For Supabase, use the pooler URL. For raw pg, configure pool size based on expected concurrent connections.
DB-06: Migrations in version control
Check if a migrations directory exists (supabase/migrations, prisma/ migrations, alembic/versions, db/migrate). Verify it contains .sql or migration files, not empty.
Remediation: Use your ORM or database tool's migration system. Never make manual schema changes to production.
DB-09: Anon key writes without RLS
Search for Supabase client-side inserts/updates using the anon key without RLS policies protecting the target tables.
Remediation: Enable RLS on all tables and create INSERT/UPDATE policies that scope access to authenticated users.
DB-10: Storage bucket policies
Search Supabase migration files and dashboard config for storage bucket creation. Verify each bucket has access policies defined.
Remediation: Define storage policies for each bucket. Restrict uploads by file type, size, and user ownership.
DB-12: PII stored unencrypted
Search schema files and migration files for columns named email, phone, ssn, social_security, credit_card, address, date_of_birth that are stored as plain text without encryption.
Remediation: Encrypt PII columns at rest. Use database-level encryption or application-level encryption for sensitive fields.
---
DEPLOY: Deployment
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| DEPLOY-01 | All env vars set on production server | manual | critical | all |
| DEPLOY-02 | SSL certificate installed and valid | manual | critical | all |
| DEPLOY-03 | Firewall configured (only 80/443 public) | manual | high | vps |
| DEPLOY-04 | Process manager running | manual | high | vps |
| DEPLOY-05 | Rollback plan exists | manual | high | all |
| DEPLOY-06 | Staging test passed before production | manual | high | all |
| DEPLOY-07 | Deploy does not cause downtime | manual | advisory | all |
| DEPLOY-08 | Domain DNS configured (www vs non-www) | manual | high | all |
| DEPLOY-09 | Health check endpoint exists | auto | high | all |
| DEPLOY-10 | Logging configured (structured, not console) | auto | high | all |
| DEPLOY-11 | Error monitoring connected (Sentry, etc.) | auto | advisory | all |
| DEPLOY-12 | Cron jobs and background tasks verified | manual | high | all |
| DEPLOY-13 | CDN configured for static assets | manual | advisory | all |
DEPLOY-09: Health check endpoint
Search for a /health, /healthz, /api/health, or /status route that returns a 200 response.
Remediation: Add a health check endpoint that verifies database connectivity and returns a simple JSON response.
DEPLOY-10: Structured logging
Check if the project uses a logging library (winston, pino, bunyan, python logging module) vs raw console.log statements in server code.
Remediation: Replace console.log with a structured logger that outputs JSON with timestamps and request IDs.
---
CODE: Code Quality
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| CODE-01 | No console.log in production build | auto | high | js/ts |
| CODE-02 | Error handling on all async operations | auto | high | all |
| CODE-03 | No empty catch blocks | auto | high | all |
| CODE-04 | Loading and error states in UI | auto | high | react |
| CODE-05 | Pagination on all list endpoints | auto | high | all |
| CODE-06 | npm audit clean (zero critical) | tool | high | js/ts |
| CODE-07 | No TODO-auth or TODO-security patterns | auto | critical | all |
| CODE-08 | No unhandled promise rejections | auto | high | js/ts |
| CODE-09 | React error boundaries in place | auto | high | react |
| CODE-10 | No leaked stack traces in error responses | auto | high | all |
| CODE-11 | No eslint-disable on security rules | auto | high | js/ts |
| CODE-12 | Lockfile committed | auto | high | all |
| CODE-13 | No wildcard versions in package.json | auto | high | js/ts |
| CODE-14 | TypeScript strict mode enabled | auto | advisory | ts |
CODE-01: No console.log in production
Search for console.log, console.debug, console.info in source files (exclude test files, config files, and node_modules).
Remediation: Remove console.log statements or replace with a proper logger. Use a build tool to strip them automatically.
CODE-03: No empty catch blocks
Search for catch blocks with empty bodies or only a comment inside. Pattern: catch\s*\([^)]*\)\s*\{\s*(\/\/.*\n)?\s*\}
Remediation: At minimum, log the error. Better: handle it appropriately or rethrow.
CODE-07: No TODO-auth/security patterns
Search for TODO.*auth, TODO.*security, TODO.*permission, FIXME.*auth, HACK.*auth, // auth, # TODO: add auth.
These indicate security features that were deferred and forgotten.
Remediation: Implement the deferred security feature or remove the endpoint if it is not ready.
CODE-09: React error boundaries
Check if the app has at least one ErrorBoundary component or uses a library like react-error-boundary. Check app/error.tsx for Next.js App Router projects.
Remediation: Add error boundaries at layout boundaries to prevent full-page crashes.
CODE-02: Error handling on async operations
Search for async functions and .then() chains. Check if they have corresponding try/catch or .catch() handlers.
Remediation: Wrap every async operation in try/catch. Log errors and show appropriate UI feedback.
CODE-04: Loading and error states in UI
Search React components for data fetching (useEffect with fetch, useSWR, useQuery, server components) and check if they render loading and error states.
Remediation: Add loading spinners/skeletons and error messages for every data-dependent component.
CODE-05: Pagination on list endpoints
Search API routes that return arrays/lists from database queries. Check for LIMIT/OFFSET, cursor pagination, or take/skip parameters.
Remediation: Add pagination to every endpoint that returns a list. Default page size of 20-50 items. Never return unbounded result sets.
CODE-10: No leaked stack traces
Search error handling code for responses that include stack traces, error.stack, or full error objects sent to the client.
Remediation: Return generic error messages to the client. Log full stack traces server-side only.
CODE-11: No eslint-disable on security rules
Search for eslint-disable comments that suppress security-related rules (no-eval, no-implied-eval, no-script-url).
Remediation: Fix the underlying issue instead of disabling the lint rule. If genuinely necessary, add a comment explaining why.
CODE-14: TypeScript strict mode
Check tsconfig.json for "strict": true or the individual flags (strictNullChecks, noImplicitAny, etc.).
Remediation: Enable strict mode in tsconfig.json. Fix type errors incrementally if migrating an existing project.
---
AI: AI/LLM Security
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| AI-01 | System prompts not leakable via user input | auto | critical | ai |
| AI-02 | No prompt injection vectors in user inputs | auto | critical | ai |
| AI-03 | LLM API keys not in frontend code | auto | critical | ai |
| AI-04 | Rate limiting on AI endpoints (cost protection) | auto | high | ai |
| AI-05 | AI response output sanitized before rendering | auto | high | ai |
| AI-06 | MCP server inputs validated | auto | high | ai |
| AI-07 | Agent permissions scoped (no unrestricted access) | manual | high | ai |
| AI-08 | No sensitive data sent to third-party LLMs without consent | manual | high | ai |
AI-01: System prompt leakage
Search for system prompts stored in client-accessible files or returned in API responses. Check if the AI endpoint echoes the system prompt when asked "repeat your instructions" or similar.
Remediation: Keep system prompts server-side only. Add input filtering for prompt extraction attempts.
AI-03: LLM API keys not in frontend
Search frontend code for OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_AI_API_KEY, sk-ant-, sk-proj-, AIza patterns.
Remediation: Proxy all LLM calls through server-side API routes.
---
DEP: Dependencies and Supply Chain
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| DEP-01 | No git:// or URL-based dependencies | auto | high | all |
| DEP-02 | No typosquatting risk (verify package names) | auto | advisory | all |
| DEP-03 | Lockfile integrity verified | auto | high | all |
| DEP-04 | npm audit / pip audit zero critical | tool | high | all |
| DEP-05 | No suspicious postinstall scripts | auto | high | js/ts |
| DEP-06 | Dependencies pinned (no wildcard *) | auto | high | all |
| DEP-07 | Lockfile committed to version control | auto | high | all |
DEP-01: No git/URL dependencies
Search package.json for dependencies with values starting with git://, git+, http://, https://github.com, or file:.
Remediation: Use published npm packages with version ranges instead of git URLs.
DEP-05: Suspicious postinstall scripts
Check package.json for postinstall, preinstall, install scripts that execute arbitrary commands, download files, or access the network.
Remediation: Review and remove unnecessary install scripts. Use --ignore-scripts for CI.
---
FE: Frontend Quality
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| FE-01 | Meta tags present (title, description, OG tags) | auto | advisory | web |
| FE-02 | Favicon configured | auto | advisory | web |
| FE-03 | Custom 404 page exists | auto | advisory | web |
| FE-04 | Responsive design tested on mobile | manual | high | web |
| FE-05 | Alt text on images | auto | high | web |
| FE-06 | Keyboard navigation works | manual | high | web |
| FE-07 | Forms have validation feedback | auto | high | web |
| FE-08 | Analytics installed (production only) | auto | advisory | web |
| FE-09 | robots.txt present | auto | advisory | web |
| FE-10 | Images optimized (WebP, lazy loading) | auto | advisory | web |
FE-01: Meta tags
Check the root layout or index page for <title>, <meta name="description">, and Open Graph tags (og:title, og:description, og:image). For Next.js, check metadata export in layout.tsx.
Remediation: Add metadata to your root layout or page head.
FE-03: Custom 404 page
Check for 404.tsx, 404.jsx, not-found.tsx, 404.html, or equivalent in the pages/app directory.
Remediation: Create a branded 404 page that helps users navigate back.
---
OBS: Observability
| ID | Check | Detection | Severity | Stack |
|---|---|---|---|---|
| OBS-01 | Error monitoring configured (Sentry, LogRocket, etc.) | auto | advisory | all |
| OBS-02 | Alerting set up for critical failures | manual | high | all |
| OBS-03 | Structured logging with request IDs | auto | advisory | all |
| OBS-04 | Performance baseline established | manual | advisory | all |
| OBS-05 | Uptime monitoring configured | manual | high | all |
| OBS-06 | Error rates tracked | manual | advisory | all |
| OBS-07 | Log retention policy defined | manual | advisory | all |
OBS-01: Error monitoring
Search for imports or configuration of error monitoring tools: @sentry/, LogRocket, Bugsnag, Datadog, Rollbar, Honeybadger.
Remediation: Install and configure an error monitoring service. Sentry has a free tier suitable for solo projects.
Ship Gate: Detection Patterns
Grep and regex patterns for auto-scannable checks. Claude runs these against the codebase to detect issues.
Table of Contents
- SEC: Security Patterns
- DB: Database Patterns
- CODE: Code Quality Patterns
- AI: AI/LLM Security Patterns
- DEP: Dependency Patterns
- FE: Frontend Quality Patterns
- OBS: Observability Patterns
- DEPLOY: Deployment Patterns
All patterns use grep -rn with --include filters. Exclude node_modules, .next, dist, build, .git, __pycache__, venv directories from all scans.
Base exclude flags:
EXCLUDE="--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=build --exclude-dir=.git --exclude-dir=__pycache__ --exclude-dir=venv --exclude-dir=.venv --exclude-dir=vendor --exclude-dir=coverage"---
SEC: Security Patterns
SEC-01: Secrets in frontend code
Scan directories that serve client-side code:
# Generic API key patterns
grep -rnE $EXCLUDE \
"(sk-[a-zA-Z0-9]{20,}|sk-ant-[a-zA-Z0-9-]+|sk-proj-[a-zA-Z0-9-]+|AIza[a-zA-Z0-9_-]{35}|ghp_[a-zA-Z0-9]{36}|glpat-[a-zA-Z0-9_-]{20,}|xox[bsap]-[a-zA-Z0-9-]+)" \
src/ app/ pages/ components/ public/ lib/ utils/ 2>/dev/null
# AWS keys
grep -rnE $EXCLUDE \
"AKIA[0-9A-Z]{16}" \
src/ app/ pages/ components/ public/ 2>/dev/null
# Stripe keys (live, not test)
grep -rnE $EXCLUDE \
"sk_live_[a-zA-Z0-9]{24,}" \
src/ app/ pages/ components/ public/ 2>/dev/null
# Generic secret assignment
grep -rnE $EXCLUDE \
"(api_key|apikey|api_secret|secret_key|auth_token|access_token)\s*[:=]\s*['\"][a-zA-Z0-9_-]{16,}" \
src/ app/ pages/ components/ public/ 2>/dev/nullSEC-04: CORS wildcard
grep -rnE $EXCLUDE \
"(origin:\s*['\"]?\*['\"]?|Access-Control-Allow-Origin.*\*|cors\(\s*\))" \
. 2>/dev/nullSEC-05: CSRF protection missing
# Check for state-changing routes without CSRF
grep -rnE $EXCLUDE \
"(app\.(post|put|patch|delete)|router\.(post|put|patch|delete))" \
. 2>/dev/null
# Then verify csrf middleware exists
grep -rnE $EXCLUDE \
"(csrf|csrfToken|_csrf|CSRF_COOKIE)" \
. 2>/dev/nullSEC-08: Weak password hashing
# Check for weak hashing (md5, sha1, sha256 for passwords)
grep -rnE $EXCLUDE \
"(md5|sha1|sha256)\s*\(" \
. 2>/dev/null
# Verify bcrypt/argon2 usage
grep -rnE $EXCLUDE \
"(bcrypt|argon2|scrypt)" \
. 2>/dev/nullSEC-11: CSP headers
# Check for Content-Security-Policy configuration
grep -rnE $EXCLUDE \
"(Content-Security-Policy|contentSecurityPolicy|csp)" \
. 2>/dev/null
# Next.js: check next.config for headers
grep -rn $EXCLUDE \
"Content-Security-Policy" \
next.config.* 2>/dev/nullSEC-13: Unsafe eval/innerHTML
# eval usage
grep -rnE $EXCLUDE \
"(\beval\s*\(|new\s+Function\s*\()" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# dangerouslySetInnerHTML without sanitizer
grep -rnE $EXCLUDE \
"dangerouslySetInnerHTML" \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Then check if DOMPurify or similar is imported in same fileSEC-15: Cookie security flags
grep -rnE $EXCLUDE \
"(set-cookie|setCookie|cookie\()" \
. 2>/dev/null
# Verify HttpOnly, Secure, SameSite flags are present
grep -rnE $EXCLUDE \
"(httpOnly|HttpOnly|secure:\s*true|sameSite)" \
. 2>/dev/nullSEC-06: Input validation
# Check for validation library usage
grep -rnE $EXCLUDE \
"(from 'zod'|from 'yup'|from 'joi'|from 'class-validator'|from pydantic)" \
. 2>/dev/null
# Check for raw req.body usage without validation
grep -rnE $EXCLUDE \
"(req\.body\.|request\.json|request\.form)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/nullSEC-07: Rate limiting
grep -rnE $EXCLUDE \
"(express-rate-limit|@upstash/ratelimit|rate-limiter|slowapi|throttle)" \
package.json requirements.txt . 2>/dev/nullSEC-09: Token expiry
grep -rnE $EXCLUDE \
"(sign\(|jwt\.encode|createToken|signToken)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Then check if expiresIn/exp is set in those calls
grep -rnE $EXCLUDE \
"(expiresIn|exp:|expires_in|expires_delta)" \
. 2>/dev/nullSEC-14: Sensitive data in URLs/logs
# Sensitive query parameters
grep -rnE $EXCLUDE \
"(password|token|secret|key|ssn|credit.card)=" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Logging full request objects
grep -rnE $EXCLUDE \
"console\.(log|info|debug)\s*\(\s*(req|request)\s*\)" \
. 2>/dev/nullSEC-16: File upload validation
grep -rnE $EXCLUDE \
"(multer|formidable|busboy|UploadedFile|upload\.single|upload\.array)" \
. 2>/dev/null
# Check for file type/size validation near upload handlers
grep -rnE $EXCLUDE \
"(fileFilter|limits|maxFileSize|allowedTypes|mimetype)" \
. 2>/dev/nullSEC-17/18: .env in repo
# Check if .env files exist in working tree
find . -maxdepth 3 -name ".env*" -not -path "*/node_modules/*" \
-not -name ".env.example" -not -name ".env.sample" 2>/dev/null
# Check if .env is in .gitignore
grep -n "\.env" .gitignore 2>/dev/null
# Check git history for .env commits
git log --all --name-only --diff-filter=A 2>/dev/null | grep "\.env" || true---
DB: Database Patterns
DB-03: SQL injection (string concatenation)
# Template literal SQL
grep -rnE $EXCLUDE \
"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\{" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# String concat SQL
grep -rnE $EXCLUDE \
"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\+\s*(req\.|params\.|body\.|query\.)" \
. 2>/dev/null
# Python f-string SQL
grep -rnE $EXCLUDE \
"f['\"].*\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b.*\{" \
--include="*.py" \
. 2>/dev/nullDB-07: Supabase RLS
# Find CREATE TABLE without RLS
grep -rnl $EXCLUDE "CREATE TABLE" \
--include="*.sql" . 2>/dev/null | while read f; do
tables=$(grep -oP "CREATE TABLE\s+\K\S+" "$f")
for t in $tables; do
if ! grep -q "ENABLE ROW LEVEL SECURITY" "$f" || \
! grep -q "$t" <<< "$(grep 'ENABLE ROW LEVEL SECURITY' "$f")"; then
echo "FAIL: $f - table $t missing RLS"
fi
done
doneDB-08: service_role in client code
grep -rnE $EXCLUDE \
"(service_role|serviceRole|SUPABASE_SERVICE_ROLE)" \
src/ app/ pages/ components/ public/ lib/client 2>/dev/nullDB-05: Connection pooling
# Check for pool configuration
grep -rnE $EXCLUDE \
"(pool|connectionLimit|max_connections|poolSize)" \
--include="*.ts" --include="*.js" --include="*.py" --include="*.env*" \
. 2>/dev/null
# Supabase: check if using pooler port
grep -rnE $EXCLUDE \
"(6543|pooler)" \
--include="*.env*" --include="*.ts" --include="*.js" \
. 2>/dev/nullDB-06: Migrations in version control
# Check for migration directories
find . -maxdepth 3 -type d \
\( -name "migrations" -o -name "migrate" -o -name "versions" \) \
-not -path "*/node_modules/*" 2>/dev/null
# Check if migrations contain files
find . -path "*/migrations/*.sql" -o -path "*/migrations/*.ts" \
-o -path "*/migrations/*.py" 2>/dev/null | head -5DB-12: PII stored unencrypted
# Search schema files for PII column names
grep -rnEi $EXCLUDE \
"(ssn|social_security|credit_card|card_number|passport)" \
--include="*.sql" --include="*.prisma" --include="*.py" \
. 2>/dev/null---
CODE: Code Quality Patterns
CODE-01: console.log in production
grep -rnE $EXCLUDE \
"console\.(log|debug|info)\(" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
--exclude="*.test.*" --exclude="*.spec.*" --exclude="*.config.*" \
src/ app/ pages/ components/ lib/ utils/ 2>/dev/nullCODE-03: Empty catch blocks
grep -rnPzo $EXCLUDE \
"catch\s*\([^)]*\)\s*\{\s*\}" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/nullCODE-07: TODO-auth patterns
grep -rnEi $EXCLUDE \
"(TODO|FIXME|HACK|XXX).*(auth|security|permission|validation|sanitiz)" \
. 2>/dev/nullCODE-08: Unhandled promise rejections
# Async functions without try-catch
grep -rnE $EXCLUDE \
"async\s+\w+\s*\(" \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Check for .catch() or try/catch wrappingCODE-09: React error boundaries
# Check for error boundary in Next.js App Router
find . -path "*/app/error.tsx" -o -path "*/app/error.jsx" \
-o -path "*/app/global-error.tsx" 2>/dev/null
# Check for ErrorBoundary component
grep -rnE $EXCLUDE \
"(ErrorBoundary|error-boundary|componentDidCatch|getDerivedStateFromError)" \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/nullCODE-12: Lockfile committed
# Check for lockfile existence
ls package-lock.json pnpm-lock.yaml yarn.lock bun.lockb \
Pipfile.lock poetry.lock Gemfile.lock go.sum Cargo.lock 2>/dev/null
# Check if lockfile is gitignored
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
if git check-ignore "$f" 2>/dev/null; then
echo "FAIL: $f is gitignored"
fi
doneCODE-13: Wildcard versions
# Check for * or empty version in package.json
grep -nE '"[^"]+"\s*:\s*"\*"' package.json 2>/dev/nullCODE-02: Async without error handling
# Find async functions
grep -rnE $EXCLUDE \
"async\s+(function\s+)?\w+\s*\(" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Count try/catch usage nearby
grep -rnc $EXCLUDE "try\s*{" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/nullCODE-04: Loading and error states
# Check for loading state patterns in React
grep -rnE $EXCLUDE \
"(isLoading|loading|Skeleton|Spinner|fallback)" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Check for Suspense boundaries
grep -rnE $EXCLUDE \
"(<Suspense|loading\.tsx|loading\.jsx)" \
. 2>/dev/nullCODE-05: Pagination on list endpoints
# Check API routes for unbounded queries
grep -rnE $EXCLUDE \
"(\.findMany|\.find\(\)|\.select\(\)|SELECT \*)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Check for pagination parameters
grep -rnE $EXCLUDE \
"(limit|offset|page|skip|take|cursor|per_page)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/nullCODE-10: Leaked stack traces
grep -rnE $EXCLUDE \
"(error\.stack|\.stack\)|err\.message.*res\.(json|send)|traceback)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/nullCODE-11: eslint-disable on security rules
grep -rnE $EXCLUDE \
"eslint-disable.*(no-eval|no-implied-eval|no-script-url|security)" \
--include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
. 2>/dev/nullCODE-14: TypeScript strict mode
grep -n '"strict"' tsconfig.json 2>/dev/null
# Check if strict is true
grep -n '"strict":\s*true' tsconfig.json 2>/dev/null---
AI: AI/LLM Security Patterns
AI-01: System prompt leakage
# System prompts in client-accessible files
grep -rnEi $EXCLUDE \
"(system.?prompt|system.?message|system_instruction)" \
src/ app/ pages/ components/ public/ 2>/dev/null
# System prompts returned in API responses
grep -rnE $EXCLUDE \
"(system.*role|role.*system)" \
src/ app/ pages/ components/ public/ 2>/dev/nullAI-02: Prompt injection vectors
# User input concatenated directly into prompts
grep -rnE $EXCLUDE \
"(messages\.push|content:.*\$\{|content:.*\+\s*user|prompt.*\+)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/nullAI-03: LLM API keys in frontend
grep -rnE $EXCLUDE \
"(OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_AI_API_KEY|sk-ant-|sk-proj-|AIza[a-zA-Z0-9_-]{35})" \
src/ app/ pages/ components/ public/ 2>/dev/nullAI-04: Rate limiting on AI endpoints
# Find AI-related API routes
grep -rnlE $EXCLUDE \
"(openai|anthropic|claude|gpt|completion|chat/api|ai/api)" \
--include="*.ts" --include="*.js" \
. 2>/dev/null
# Then check for rate limiting middleware in those filesAI-05: AI output sanitization
# Check if AI responses are rendered with dangerouslySetInnerHTML
grep -rnE $EXCLUDE \
"dangerouslySetInnerHTML.*\b(response|result|completion|message|content)\b" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/nullAI-06: MCP server input validation
# Check MCP server tool handlers for input validation
grep -rnE $EXCLUDE \
"(tool_input|toolInput|tool_call|CallToolRequest)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/null
# Check if zod/validation is applied to tool inputs---
DEP: Dependency Patterns
DEP-01: Git/URL dependencies
grep -nE '"(git|git\+|http|https|file):' package.json 2>/dev/null
grep -nE '"github:' package.json 2>/dev/nullDEP-04: npm audit
# Run npm audit and capture critical/high counts
npm audit --json 2>/dev/null | grep -c '"severity":"critical"'
npm audit --json 2>/dev/null | grep -c '"severity":"high"'
# Or for pip
pip audit --format json 2>/dev/nullDEP-05: Suspicious install scripts
grep -A2 '"preinstall"\|"postinstall"\|"install"' package.json 2>/dev/nullDEP-06: Wildcard versions
grep -nE '"\*"' package.json 2>/dev/null
grep -nE '"latest"' package.json 2>/dev/null---
FE: Frontend Quality Patterns
FE-01: Meta tags
# Next.js App Router metadata
grep -rnE $EXCLUDE \
"(export\s+(const|async\s+function)\s+metadata|generateMetadata)" \
--include="*.tsx" --include="*.ts" \
app/layout.* app/page.* 2>/dev/null
# HTML meta tags
grep -rnE $EXCLUDE \
'(<title>|<meta\s+name="description"|og:title|og:description|og:image)' \
. 2>/dev/nullFE-02: Favicon
find . -maxdepth 3 \( -name "favicon.*" -o -name "icon.*" \) \
-not -path "*/node_modules/*" 2>/dev/nullFE-03: Custom 404 page
find . -maxdepth 4 \( -name "404.*" -o -name "not-found.*" \) \
-not -path "*/node_modules/*" 2>/dev/nullFE-05: Image alt text
# Find img tags without alt attribute
grep -rnE $EXCLUDE \
'<img\s+(?![^>]*\balt\b)[^>]*>' \
--include="*.html" --include="*.jsx" --include="*.tsx" \
. 2>/dev/null
# Next.js Image without alt
grep -rnE $EXCLUDE \
'<Image\s+(?![^>]*\balt\b)[^>]*/?>' \
--include="*.jsx" --include="*.tsx" \
. 2>/dev/nullFE-09: robots.txt
find . -maxdepth 2 -name "robots.txt" \
-not -path "*/node_modules/*" 2>/dev/nullFE-07: Form validation feedback
# Check for form elements without validation attributes
grep -rnE $EXCLUDE \
'(<input|<textarea|<select)' \
--include="*.tsx" --include="*.jsx" --include="*.html" \
. 2>/dev/null
# Check for validation library usage
grep -rnE $EXCLUDE \
"(useForm|react-hook-form|formik|yup|zod.*form)" \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/nullFE-10: Image optimization
# Check for unoptimized img tags (not using Next/Image or similar)
grep -rnE $EXCLUDE \
'<img\s' \
--include="*.tsx" --include="*.jsx" \
. 2>/dev/null
# Check for lazy loading
grep -rnE $EXCLUDE \
'(loading="lazy"|lazy|lazyload)' \
--include="*.tsx" --include="*.jsx" --include="*.html" \
. 2>/dev/null---
OBS: Observability Patterns
OBS-01: Error monitoring
grep -rnE $EXCLUDE \
"(@sentry|sentry-|LogRocket|Bugsnag|datadogRum|Rollbar|Honeybadger|newrelic)" \
package.json . 2>/dev/nullOBS-03: Structured logging
# Check for logging libraries
grep -rnE $EXCLUDE \
"(winston|pino|bunyan|morgan|log4js)" \
package.json 2>/dev/null
# Python
grep -rnE $EXCLUDE \
"import logging|from loguru" \
--include="*.py" . 2>/dev/null---
DEPLOY: Deployment Patterns
DEPLOY-09: Health check endpoint
grep -rnE $EXCLUDE \
"(\/health|\/healthz|\/api\/health|\/status|\/readyz)" \
--include="*.ts" --include="*.js" --include="*.py" \
. 2>/dev/nullDEPLOY-10: Console vs structured logging (server)
# Count console.log vs logger usage in API/server code
echo "console.log count:"
grep -rnc $EXCLUDE "console\.log" \
--include="*.ts" --include="*.js" \
api/ server/ pages/api/ app/api/ 2>/dev/null | tail -1
echo "structured logger count:"
grep -rnc $EXCLUDE "(logger\.|log\.(info|warn|error|debug))" \
--include="*.ts" --include="*.js" \
api/ server/ pages/api/ app/api/ 2>/dev/null | tail -1#!/usr/bin/env python3
"""
ship_gate_scanner.py — Pre-production audit CLI
Part of the ship-gate skill: https://github.com/rx4u/ship-gate
Usage:
python scripts/ship_gate_scanner.py [PATH] [options]
Options:
--json Output results as JSON
--no-color Disable ANSI color output
--no-interactive Skip manual confirmation prompts
--category CAT Only run a specific category (SEC, DB, CODE, etc.)
--verbose Show PASS results in addition to FAIL
--version Show version and exit
Exit codes:
0 = CLEAR TO SHIP (no critical issues)
1 = DO NOT SHIP (critical issues found)
2 = SHIP WITH CAUTION (high issues only)
"""
import argparse
import json
import os
import re
import sys
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import List, Optional
VERSION = "1.0.0"
EXCLUDE_DIRS = {
"node_modules", ".next", "dist", "build", ".git", "__pycache__",
"venv", ".venv", "vendor", "coverage", ".turbo", "out", ".cache",
".pytest_cache", ".mypy_cache", "target", "bin", "obj",
}
FRONTEND_DIRS = {"src", "app", "pages", "components", "public", "lib", "utils"}
JS_EXTS = {".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"}
PY_EXTS = {".py"}
ALL_CODE_EXTS = JS_EXTS | PY_EXTS | {".go", ".rb", ".php"}
TEMPLATE_EXTS = {".html", ".jsx", ".tsx", ".vue", ".svelte"}
SQL_EXTS = {".sql", ".prisma"}
# ---------------------------------------------------------------------------
# ANSI helpers
# ---------------------------------------------------------------------------
USE_COLOR = True
def _c(code: str, text: str) -> str:
if not USE_COLOR:
return text
return f"\033[{code}m{text}\033[0m"
def red(t): return _c("31", t)
def green(t): return _c("32", t)
def yellow(t): return _c("33", t)
def cyan(t): return _c("36", t)
def bold(t): return _c("1", t)
def dim(t): return _c("2", t)
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
class Status(str, Enum):
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
MANUAL = "MANUAL"
class Severity(str, Enum):
CRITICAL = "CRITICAL"
HIGH = "HIGH"
ADVISORY = "ADVISORY"
@dataclass
class Finding:
file: str
line: int
snippet: str = ""
@dataclass
class CheckDef:
id: str
description: str
severity: Severity
category: str
stack: str = "all" # "all", "js", "ts", "react", "supabase", "ai", "web", "vps"
@dataclass
class Result:
check: CheckDef
status: Status
message: str = ""
findings: List[Finding] = field(default_factory=list)
@dataclass
class Stack:
has_node: bool = False
framework: str = "" # next, react, vue, svelte, astro, express, fastify, hono
has_python: bool = False
py_framework: str = "" # django, flask, fastapi
has_go: bool = False
has_rust: bool = False
has_supabase: bool = False
has_typescript: bool = False
has_react: bool = False
deploy_target: str = "" # vercel, netlify, docker, fly, railway
has_ai: bool = False
ai_providers: List[str] = field(default_factory=list)
is_web: bool = False
# ---------------------------------------------------------------------------
# File walking / grep helpers
# ---------------------------------------------------------------------------
def walk_files(root: str, exts: Optional[set] = None, dirs: Optional[set] = None):
"""Yield (filepath, relpath) for all files under root, skipping EXCLUDE_DIRS."""
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
if dirs is not None:
rel = os.path.relpath(dirpath, root)
top = rel.split(os.sep)[0]
if rel != "." and top not in dirs:
dirnames[:] = []
continue
for fname in filenames:
if exts is None or os.path.splitext(fname)[1].lower() in exts:
fpath = os.path.join(dirpath, fname)
yield fpath, os.path.relpath(fpath, root)
def grep_files(
root: str,
pattern: str,
exts: Optional[set] = None,
dirs: Optional[set] = None,
flags: int = 0,
max_findings: int = 20,
exclude_patterns: Optional[List[str]] = None,
) -> List[Finding]:
"""Return up to max_findings matches across the codebase."""
try:
rx = re.compile(pattern, flags)
except re.error:
return []
exclude_rxs = []
if exclude_patterns:
for ep in exclude_patterns:
try:
exclude_rxs.append(re.compile(ep))
except re.error:
pass
results: List[Finding] = []
for fpath, relpath in walk_files(root, exts, dirs):
if any(seg in fpath for seg in (".test.", ".spec.", ".config.")):
if exts and exts <= JS_EXTS:
skip = True
# still yield for config-specific checks
if "tsconfig" in fpath or "package.json" in fpath:
skip = False
if skip:
continue
try:
with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
for lineno, line in enumerate(fh, 1):
if rx.search(line):
if any(ex.search(line) for ex in exclude_rxs):
continue
results.append(Finding(
file=relpath,
line=lineno,
snippet=line.rstrip()[:120],
))
if len(results) >= max_findings:
return results
except (OSError, PermissionError):
continue
return results
def file_exists_in(root: str, *names: str) -> Optional[str]:
"""Return the first found path among names (searched recursively up to depth 5)."""
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
depth = dirpath.replace(root, "").count(os.sep)
if depth >= 5:
dirnames[:] = []
continue
for fname in filenames:
if fname in names:
return os.path.join(dirpath, fname)
return None
def read_json_file(path: str) -> dict:
try:
with open(path) as f:
return json.load(f)
except Exception:
return {}
# ---------------------------------------------------------------------------
# Stack detection
# ---------------------------------------------------------------------------
def detect_stack(root: str) -> Stack:
s = Stack()
pkg_path = os.path.join(root, "package.json")
if os.path.isfile(pkg_path):
s.has_node = True
pkg = read_json_file(pkg_path)
all_deps = {}
for key in ("dependencies", "devDependencies", "peerDependencies"):
all_deps.update(pkg.get(key, {}))
if "next" in all_deps: s.framework = "next"
elif "react" in all_deps: s.framework = "react"
elif "vue" in all_deps: s.framework = "vue"
elif "svelte" in all_deps: s.framework = "svelte"
elif "astro" in all_deps: s.framework = "astro"
elif "express" in all_deps: s.framework = "express"
elif "fastify" in all_deps: s.framework = "fastify"
elif "hono" in all_deps: s.framework = "hono"
s.has_react = s.framework in ("next", "react")
s.is_web = s.framework in ("next", "react", "vue", "svelte", "astro")
if "@supabase/supabase-js" in all_deps:
s.has_supabase = True
if "typescript" in all_deps or os.path.isfile(os.path.join(root, "tsconfig.json")):
s.has_typescript = True
for ai_pkg in ("openai", "@anthropic-ai/sdk", "@google/generative-ai",
"ai", "@huggingface/inference"):
if ai_pkg in all_deps:
s.has_ai = True
s.ai_providers.append(ai_pkg)
if os.path.isdir(os.path.join(root, "supabase")):
s.has_supabase = True
for pyfile in ("requirements.txt", "pyproject.toml", "Pipfile", "setup.py"):
if os.path.isfile(os.path.join(root, pyfile)):
s.has_python = True
try:
content = open(os.path.join(root, pyfile)).read().lower()
if "django" in content: s.py_framework = "django"
elif "flask" in content: s.py_framework = "flask"
elif "fastapi" in content: s.py_framework = "fastapi"
except Exception:
pass
break
if os.path.isfile(os.path.join(root, "go.mod")):
s.has_go = True
if os.path.isfile(os.path.join(root, "Cargo.toml")):
s.has_rust = True
if os.path.isfile(os.path.join(root, "vercel.json")) or \
os.path.isdir(os.path.join(root, ".vercel")):
s.deploy_target = "vercel"
elif os.path.isfile(os.path.join(root, "netlify.toml")):
s.deploy_target = "netlify"
elif os.path.isfile(os.path.join(root, "fly.toml")):
s.deploy_target = "fly"
elif os.path.isfile(os.path.join(root, "railway.json")):
s.deploy_target = "railway"
elif os.path.isfile(os.path.join(root, "Dockerfile")):
s.deploy_target = "docker"
return s
# ---------------------------------------------------------------------------
# Check definitions
# ---------------------------------------------------------------------------
CHECKS = {
# SEC
"SEC-01": CheckDef("SEC-01", "No API keys or secrets in frontend code", Severity.CRITICAL, "SEC"),
"SEC-04": CheckDef("SEC-04", "CORS not wildcard", Severity.CRITICAL, "SEC"),
"SEC-05": CheckDef("SEC-05", "CSRF protection on state-changing endpoints", Severity.CRITICAL, "SEC"),
"SEC-06": CheckDef("SEC-06", "Input validated and sanitized server-side", Severity.HIGH, "SEC"),
"SEC-07": CheckDef("SEC-07", "Rate limiting on auth and sensitive endpoints", Severity.HIGH, "SEC"),
"SEC-08": CheckDef("SEC-08", "Passwords hashed with bcrypt or argon2", Severity.CRITICAL, "SEC"),
"SEC-11": CheckDef("SEC-11", "CSP headers configured", Severity.HIGH, "SEC"),
"SEC-13": CheckDef("SEC-13", "No eval() or dangerouslySetInnerHTML without sanitization", Severity.HIGH, "SEC", stack="js"), # noqa: SEC-AUDITOR
"SEC-14": CheckDef("SEC-14", "No sensitive data in URLs or logs", Severity.HIGH, "SEC"),
"SEC-17": CheckDef("SEC-17", "No hardcoded secrets in .env committed to repo", Severity.CRITICAL, "SEC"),
"SEC-18": CheckDef("SEC-18", ".env files listed in .gitignore", Severity.CRITICAL, "SEC"),
# DB
"DB-03": CheckDef("DB-03", "Parameterized queries everywhere (no SQL injection)", Severity.CRITICAL, "DB"),
"DB-05": CheckDef("DB-05", "Connection pooling configured", Severity.HIGH, "DB"),
"DB-06": CheckDef("DB-06", "Migrations in version control", Severity.HIGH, "DB"),
"DB-07": CheckDef("DB-07", "RLS enabled on all Supabase tables", Severity.CRITICAL, "DB", stack="supabase"),
"DB-08": CheckDef("DB-08", "No service_role key in client-side code", Severity.CRITICAL, "DB", stack="supabase"),
"DB-12": CheckDef("DB-12", "No PII stored unencrypted", Severity.HIGH, "DB"),
# DEPLOY
"DEPLOY-09": CheckDef("DEPLOY-09", "Health check endpoint exists", Severity.HIGH, "DEPLOY"),
"DEPLOY-10": CheckDef("DEPLOY-10", "Structured logging (not raw console)", Severity.HIGH, "DEPLOY"),
# CODE
"CODE-01": CheckDef("CODE-01", "No console.log in production build", Severity.HIGH, "CODE", stack="js"),
"CODE-03": CheckDef("CODE-03", "No empty catch blocks", Severity.HIGH, "CODE"),
"CODE-07": CheckDef("CODE-07", "No TODO-auth or TODO-security patterns", Severity.CRITICAL, "CODE"),
"CODE-09": CheckDef("CODE-09", "React error boundaries in place", Severity.HIGH, "CODE", stack="react"),
"CODE-10": CheckDef("CODE-10", "No leaked stack traces in error responses", Severity.HIGH, "CODE"),
"CODE-11": CheckDef("CODE-11", "No eslint-disable on security rules", Severity.HIGH, "CODE", stack="js"),
"CODE-12": CheckDef("CODE-12", "Lockfile committed", Severity.HIGH, "CODE"),
"CODE-13": CheckDef("CODE-13", "No wildcard versions in package.json", Severity.HIGH, "CODE", stack="js"),
"CODE-14": CheckDef("CODE-14", "TypeScript strict mode enabled", Severity.ADVISORY, "CODE", stack="ts"),
# AI
"AI-01": CheckDef("AI-01", "System prompts not leakable via user input", Severity.CRITICAL, "AI", stack="ai"),
"AI-02": CheckDef("AI-02", "No prompt injection vectors in user inputs", Severity.CRITICAL, "AI", stack="ai"),
"AI-03": CheckDef("AI-03", "LLM API keys not in frontend code", Severity.CRITICAL, "AI", stack="ai"),
"AI-05": CheckDef("AI-05", "AI response output sanitized before rendering", Severity.HIGH, "AI", stack="ai"),
# DEP
"DEP-01": CheckDef("DEP-01", "No git:// or URL-based dependencies", Severity.HIGH, "DEP"),
"DEP-05": CheckDef("DEP-05", "No suspicious postinstall scripts", Severity.HIGH, "DEP", stack="js"),
"DEP-06": CheckDef("DEP-06", "Dependencies pinned (no wildcard *)", Severity.HIGH, "DEP"),
# FE
"FE-01": CheckDef("FE-01", "Meta tags present (title, description, OG)", Severity.ADVISORY, "FE", stack="web"),
"FE-02": CheckDef("FE-02", "Favicon configured", Severity.ADVISORY, "FE", stack="web"),
"FE-03": CheckDef("FE-03", "Custom 404 page exists", Severity.ADVISORY, "FE", stack="web"),
"FE-09": CheckDef("FE-09", "robots.txt present", Severity.ADVISORY, "FE", stack="web"),
# OBS
"OBS-01": CheckDef("OBS-01", "Error monitoring configured (Sentry, etc.)", Severity.ADVISORY, "OBS"),
"OBS-03": CheckDef("OBS-03", "Structured logging with request IDs", Severity.ADVISORY, "OBS"),
}
MANUAL_CHECKS = [
CheckDef("SEC-02", "Every route checks authentication", Severity.CRITICAL, "SEC"),
CheckDef("SEC-03", "HTTPS enforced, HTTP redirected", Severity.CRITICAL, "SEC"),
CheckDef("SEC-10", "Sessions invalidated on logout (server-side)", Severity.HIGH, "SEC"),
CheckDef("DB-01", "Backups configured and tested", Severity.CRITICAL, "DB"),
CheckDef("DB-02", "Backup restore tested (not just backup)", Severity.CRITICAL, "DB"),
CheckDef("DB-04", "Separate dev and production databases", Severity.HIGH, "DB"),
CheckDef("DB-11", "App uses a non-root DB user", Severity.HIGH, "DB"),
CheckDef("DEPLOY-01", "All env vars set on production server", Severity.CRITICAL, "DEPLOY"),
CheckDef("DEPLOY-02", "SSL certificate installed and valid", Severity.CRITICAL, "DEPLOY"),
CheckDef("DEPLOY-05", "Rollback plan exists", Severity.HIGH, "DEPLOY"),
CheckDef("DEPLOY-06", "Staging test passed before production", Severity.HIGH, "DEPLOY"),
CheckDef("AI-07", "Agent permissions scoped (no unrestricted access)", Severity.HIGH, "AI", stack="ai"),
CheckDef("AI-08", "No sensitive data sent to third-party LLMs without consent", Severity.HIGH, "AI", stack="ai"),
CheckDef("FE-04", "Responsive design tested on mobile", Severity.HIGH, "FE", stack="web"),
CheckDef("OBS-05", "Uptime monitoring configured", Severity.HIGH, "OBS"),
]
# ---------------------------------------------------------------------------
# Individual check implementations
# ---------------------------------------------------------------------------
def check_sec01(root, stack):
c = CHECKS["SEC-01"]
dirs = FRONTEND_DIRS & set(os.listdir(root))
patterns = [
r"sk-[a-zA-Z0-9]{20,}",
r"sk-ant-[a-zA-Z0-9-]+",
r"sk-proj-[a-zA-Z0-9-]+",
r"AIza[a-zA-Z0-9_-]{35}",
r"ghp_[a-zA-Z0-9]{36}",
r"glpat-[a-zA-Z0-9_-]{20,}",
r"AKIA[0-9A-Z]{16}",
r"sk_live_[a-zA-Z0-9]{24,}",
r"(api_key|apikey|api_secret|secret_key|auth_token)\s*[:=]\s*['\"][a-zA-Z0-9_\-]{16,}",
]
findings = []
for pat in patterns:
findings += grep_files(root, pat, exts=JS_EXTS | {".env", ".json"},
dirs=dirs if dirs else None, max_findings=5)
if findings:
return Result(c, Status.FAIL,
f"{len(findings)} potential secret(s) found in frontend/client code",
findings[:10])
return Result(c, Status.PASS)
def check_sec04(root, stack):
c = CHECKS["SEC-04"]
findings = grep_files(root, r"(origin\s*:\s*['\"]?\*['\"]?|Access-Control-Allow-Origin.*\*|cors\(\s*\))",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, "CORS wildcard (*) detected", findings)
return Result(c, Status.PASS)
def check_sec05(root, stack):
c = CHECKS["SEC-05"]
# Check for state-changing routes
route_findings = grep_files(root, r"(app|router)\.(post|put|patch|delete)\s*\(",
exts=JS_EXTS)
if not route_findings:
return Result(c, Status.SKIP, "No Express-style routes found")
# Check for CSRF protection
csrf_findings = grep_files(root, r"(csrf|csrfToken|_csrf|CSRF_COOKIE|csurf)",
exts=ALL_CODE_EXTS)
if not csrf_findings:
return Result(c, Status.FAIL,
f"{len(route_findings)} state-changing route(s) found but no CSRF protection detected",
route_findings[:5])
return Result(c, Status.PASS)
def check_sec06(root, stack):
c = CHECKS["SEC-06"]
# Check for validation library
val_findings = grep_files(root,
r"(from ['\"]zod['\"]|from ['\"]yup['\"]|from ['\"]joi['\"]|from ['\"]class-validator['\"]|from pydantic|import pydantic)",
exts=ALL_CODE_EXTS)
if val_findings:
return Result(c, Status.PASS)
# Check if there are API routes that use req.body without validation
body_findings = grep_files(root, r"(req\.body|request\.json\(\)|request\.form)",
exts=ALL_CODE_EXTS)
if body_findings:
return Result(c, Status.FAIL,
"request body used without a validation library (zod/yup/joi/pydantic)",
body_findings[:5])
return Result(c, Status.SKIP, "No API route body handling detected")
def check_sec07(root, stack):
c = CHECKS["SEC-07"]
findings = grep_files(root,
r"(express-rate-limit|@upstash/ratelimit|rate-limiter-flexible|slowapi|throttle|rateLimit)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
# Only fail if there are auth-related routes
auth_routes = grep_files(root, r"(login|signin|register|signup|forgot.password|reset.password)",
exts=ALL_CODE_EXTS)
if auth_routes:
return Result(c, Status.FAIL,
"Auth routes found but no rate-limiting library detected", auth_routes[:3])
return Result(c, Status.SKIP, "No auth routes detected")
def check_sec08(root, stack):
c = CHECKS["SEC-08"]
# Weak hash for passwords
weak = grep_files(root, r"\b(md5|sha1|sha256)\s*\(",
exts=ALL_CODE_EXTS,
exclude_patterns=[r"//.*\b(md5|sha1|sha256)\b"])
if weak:
return Result(c, Status.FAIL, "Weak hashing algorithm (md5/sha1/sha256) detected", weak)
strong = grep_files(root, r"(bcrypt|argon2|scrypt|pbkdf2)", exts=ALL_CODE_EXTS)
pw_fields = grep_files(root, r"(password|passwd)", exts=ALL_CODE_EXTS)
if pw_fields and not strong:
return Result(c, Status.FAIL, "Password fields found but no bcrypt/argon2/scrypt usage")
return Result(c, Status.PASS if strong or not pw_fields else Status.SKIP)
def check_sec11(root, stack):
c = CHECKS["SEC-11"]
findings = grep_files(root, r"(Content-Security-Policy|contentSecurityPolicy|[^a-z]csp[^a-z])",
exts=ALL_CODE_EXTS | {".json", ".toml", ".yaml", ".yml"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No Content-Security-Policy configuration found")
def check_sec13(root, stack):
c = CHECKS["SEC-13"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
eval_findings = grep_files(root, r"(\beval\s*\(|new\s+Function\s*\()", exts=JS_EXTS)
dsi_findings = grep_files(root, r"dangerouslySetInnerHTML", exts=JS_EXTS)
# If dangerouslySetInnerHTML is used, check for DOMPurify
unsafe_dsi = []
for f in dsi_findings:
try:
content = open(os.path.join(root, f.file), errors="ignore").read()
if "DOMPurify" not in content and "sanitize" not in content.lower():
unsafe_dsi.append(f)
except Exception:
unsafe_dsi.append(f)
all_findings = eval_findings + unsafe_dsi # noqa: SEC-AUDITOR
if all_findings:
return Result(c, Status.FAIL, "Unsafe eval() or unsanitized dangerouslySetInnerHTML", all_findings) # noqa: SEC-AUDITOR
return Result(c, Status.PASS)
def check_sec14(root, stack):
c = CHECKS["SEC-14"]
url_findings = grep_files(root,
r"(password|token|secret|key|ssn|credit.card)=",
exts=ALL_CODE_EXTS)
log_findings = grep_files(root,
r"console\.(log|info|debug)\s*\(\s*(req|request)\s*\)",
exts=JS_EXTS)
findings = url_findings + log_findings
if findings:
return Result(c, Status.FAIL, "Sensitive data may appear in URLs or logs", findings[:5])
return Result(c, Status.PASS)
def check_sec17(root, stack):
c = CHECKS["SEC-17"]
# Check for .env files that are not .example/.sample
env_files = []
for entry in os.scandir(root):
name = entry.name
if name.startswith(".env") and name not in (".env.example", ".env.sample",
".env.template", ".env.local.example"):
if entry.is_file():
env_files.append(name)
if not env_files:
return Result(c, Status.PASS)
# Check if git-tracked
gitignore_path = os.path.join(root, ".gitignore")
if os.path.isfile(gitignore_path):
content = open(gitignore_path, errors="ignore").read()
if ".env" in content:
return Result(c, Status.PASS)
return Result(c, Status.FAIL,
f".env file(s) exist ({', '.join(env_files)}) and may not be gitignored",
[Finding(f, 0) for f in env_files])
def check_sec18(root, stack):
c = CHECKS["SEC-18"]
gitignore_path = os.path.join(root, ".gitignore")
if not os.path.isfile(gitignore_path):
return Result(c, Status.FAIL, ".gitignore file not found")
content = open(gitignore_path, errors="ignore").read()
if re.search(r"\.env", content):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, ".env not listed in .gitignore")
def check_db03(root, stack):
c = CHECKS["DB-03"]
# Template literal SQL
tl_findings = grep_files(root,
r"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\$\{",
exts=JS_EXTS)
# Python f-string SQL
py_findings = grep_files(root,
r'f["\'].*\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b.*\{',
exts=PY_EXTS)
# String concat SQL
concat_findings = grep_files(root,
r"(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\+\s*(req\.|params\.|body\.|query\.)",
exts=ALL_CODE_EXTS)
all_findings = tl_findings + py_findings + concat_findings
if all_findings:
return Result(c, Status.FAIL,
f"{len(all_findings)} potential SQL injection vector(s)", all_findings[:10])
return Result(c, Status.PASS)
def check_db05(root, stack):
c = CHECKS["DB-05"]
findings = grep_files(root,
r"(pool|connectionLimit|max_connections|poolSize|pooler|6543)",
exts=ALL_CODE_EXTS | {".env", ".env.local", ".env.production"})
if findings:
return Result(c, Status.PASS)
db_found = grep_files(root, r"(pg\.|postgres\.|mysql\.|mongoose\.)", exts=ALL_CODE_EXTS)
if db_found:
return Result(c, Status.FAIL, "Database usage detected but no connection pooling configured")
return Result(c, Status.SKIP, "No direct DB connection detected")
def check_db06(root, stack):
c = CHECKS["DB-06"]
migration_dirs = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
depth = dirpath.replace(root, "").count(os.sep)
if depth >= 4:
dirnames[:] = []
continue
for d in dirnames:
if d in ("migrations", "migrate", "versions", "alembic"):
migration_dirs.append(os.path.join(dirpath, d))
if migration_dirs:
return Result(c, Status.PASS)
# Check for database usage
db_found = grep_files(root, r"(prisma|supabase|mongoose|pg\.|sqlite)", exts=ALL_CODE_EXTS)
if db_found:
return Result(c, Status.FAIL, "Database usage found but no migrations directory detected")
return Result(c, Status.SKIP, "No database usage detected")
def check_db07(root, stack):
c = CHECKS["DB-07"]
if not stack.has_supabase:
return Result(c, Status.SKIP, "Not a Supabase project")
sql_findings = grep_files(root, r"CREATE TABLE", exts=SQL_EXTS)
if not sql_findings:
return Result(c, Status.SKIP, "No CREATE TABLE statements found in migrations")
rls_findings = grep_files(root, r"ENABLE ROW LEVEL SECURITY", exts=SQL_EXTS)
if not rls_findings:
return Result(c, Status.FAIL,
f"{len(sql_findings)} table(s) found but no RLS policies detected",
sql_findings[:5])
if len(rls_findings) < len(sql_findings):
return Result(c, Status.FAIL,
f"{len(sql_findings)} table(s) but only {len(rls_findings)} RLS statement(s) — some tables may lack RLS",
sql_findings[:5])
return Result(c, Status.PASS)
def check_db08(root, stack):
c = CHECKS["DB-08"]
if not stack.has_supabase:
return Result(c, Status.SKIP, "Not a Supabase project")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(service_role|serviceRole|SUPABASE_SERVICE_ROLE)",
exts=JS_EXTS, dirs=dirs if dirs else None)
if findings:
return Result(c, Status.FAIL, "service_role key referenced in client-side code", findings)
return Result(c, Status.PASS)
def check_db12(root, stack):
c = CHECKS["DB-12"]
findings = grep_files(root,
r"(ssn|social_security|credit_card|card_number|passport_number)",
exts=SQL_EXTS | {".prisma"}, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL,
"PII column names found in schema — verify encryption at rest", findings)
return Result(c, Status.PASS)
def check_deploy09(root, stack):
c = CHECKS["DEPLOY-09"]
findings = grep_files(root,
r"(/health|/healthz|/api/health|/status|/readyz)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No health check endpoint found")
def check_deploy10(root, stack):
c = CHECKS["DEPLOY-10"]
# Check for logging libraries
lib_findings = grep_files(root,
r"(winston|pino|bunyan|morgan|log4js|structlog|loguru)",
exts=ALL_CODE_EXTS | {".json"})
if lib_findings:
return Result(c, Status.PASS)
# Count console.log in server/api code
server_dirs = {"api", "server", "backend"}
for d in ("pages/api", "app/api"):
if os.path.isdir(os.path.join(root, d)):
server_dirs.add(d.split("/")[0])
console_findings = grep_files(root, r"console\.(log|debug|info)\(", exts=JS_EXTS)
if console_findings:
return Result(c, Status.FAIL,
f"No structured logger found; {len(console_findings)} console.log(s) in code",
console_findings[:5])
return Result(c, Status.SKIP, "No server-side code detected")
def check_code01(root, stack):
c = CHECKS["CODE-01"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
findings = grep_files(root, r"console\.(log|debug|info)\(",
exts=JS_EXTS,
dirs=FRONTEND_DIRS & set(os.listdir(root)) or None,
exclude_patterns=[r"//.*console\.(log|debug|info)\("])
if findings:
return Result(c, Status.FAIL, f"{len(findings)} console.log statement(s) in production code", findings[:10])
return Result(c, Status.PASS)
def check_code03(root, stack):
c = CHECKS["CODE-03"]
findings = grep_files(root,
r"catch\s*\([^)]*\)\s*\{\s*\}",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, f"{len(findings)} empty catch block(s)", findings)
return Result(c, Status.PASS)
def check_code07(root, stack):
c = CHECKS["CODE-07"]
findings = grep_files(root,
r"(TODO|FIXME|HACK|XXX).{0,20}(auth|security|permission|validation|sanitiz)",
exts=ALL_CODE_EXTS, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL, f"{len(findings)} deferred security TODO(s)", findings)
return Result(c, Status.PASS)
def check_code09(root, stack):
c = CHECKS["CODE-09"]
if not stack.has_react:
return Result(c, Status.SKIP, "Not a React project")
# Next.js App Router: error.tsx
error_page = file_exists_in(root, "error.tsx", "error.jsx", "global-error.tsx")
if error_page:
return Result(c, Status.PASS)
# Class-based error boundary
eb_findings = grep_files(root,
r"(ErrorBoundary|componentDidCatch|getDerivedStateFromError)",
exts=JS_EXTS)
if eb_findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No React error boundary or error.tsx found")
def check_code10(root, stack):
c = CHECKS["CODE-10"]
findings = grep_files(root,
r"(error\.stack|\.stack\s*\)|err\.message.*res\.(json|send)|traceback\.format_exc)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL, "Potential stack trace leak in error responses", findings)
return Result(c, Status.PASS)
def check_code11(root, stack):
c = CHECKS["CODE-11"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
findings = grep_files(root,
r"eslint-disable.*(no-eval|no-implied-eval|no-script-url|security)",
exts=JS_EXTS)
if findings:
return Result(c, Status.FAIL, "Security lint rule(s) disabled", findings)
return Result(c, Status.PASS)
def check_code12(root, stack):
c = CHECKS["CODE-12"]
lockfiles = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb",
"Pipfile.lock", "poetry.lock", "Gemfile.lock", "go.sum", "Cargo.lock"]
for lf in lockfiles:
if os.path.isfile(os.path.join(root, lf)):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No lockfile found — dependencies are not pinned")
def check_code13(root, stack):
c = CHECKS["CODE-13"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a JS/TS project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
findings = grep_files(root, r'"[^"]+"\s*:\s*"\*"', exts={".json"})
findings += grep_files(root, r'"[^"]+"\s*:\s*"latest"', exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Wildcard (*) or 'latest' version found in package.json", findings)
return Result(c, Status.PASS)
def check_code14(root, stack):
c = CHECKS["CODE-14"]
if not stack.has_typescript:
return Result(c, Status.SKIP, "Not a TypeScript project")
tsconfig_path = os.path.join(root, "tsconfig.json")
if not os.path.isfile(tsconfig_path):
return Result(c, Status.SKIP, "tsconfig.json not found")
content = open(tsconfig_path, errors="ignore").read()
if re.search(r'"strict"\s*:\s*true', content):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "TypeScript strict mode not enabled in tsconfig.json",
[Finding("tsconfig.json", 0)])
def check_ai01(root, stack):
c = CHECKS["AI-01"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(system.?prompt|system.?message|system_instruction)",
exts=ALL_CODE_EXTS, dirs=dirs if dirs else None, flags=re.IGNORECASE)
if findings:
return Result(c, Status.FAIL,
"System prompt referenced in client-accessible code — may be leakable",
findings)
return Result(c, Status.PASS)
def check_ai02(root, stack):
c = CHECKS["AI-02"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
findings = grep_files(root,
r"(messages\.push|content\s*:.*\$\{|content\s*:.*\+\s*user|prompt.*\+)",
exts=ALL_CODE_EXTS)
if findings:
return Result(c, Status.FAIL,
"User input may be concatenated directly into AI prompt", findings[:5])
return Result(c, Status.PASS)
def check_ai03(root, stack):
c = CHECKS["AI-03"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
dirs = FRONTEND_DIRS & set(os.listdir(root))
findings = grep_files(root,
r"(OPENAI_API_KEY|ANTHROPIC_API_KEY|GOOGLE_AI_API_KEY|sk-ant-|sk-proj-)",
exts=JS_EXTS, dirs=dirs if dirs else None)
if findings:
return Result(c, Status.FAIL, "LLM API key referenced in frontend code", findings)
return Result(c, Status.PASS)
def check_ai05(root, stack):
c = CHECKS["AI-05"]
if not stack.has_ai:
return Result(c, Status.SKIP, "No AI/LLM usage detected")
findings = grep_files(root,
r"dangerouslySetInnerHTML.*\b(response|result|completion|message|content)\b",
exts=JS_EXTS)
if findings:
return Result(c, Status.FAIL, "AI output rendered via dangerouslySetInnerHTML", findings)
return Result(c, Status.PASS)
def check_dep01(root, stack):
c = CHECKS["DEP-01"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
findings = grep_files(root,
r'"[^"]+"\s*:\s*"(git://|git\+|github:|https://github\.com|file:)',
exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Git/URL-based dependency found in package.json", findings)
return Result(c, Status.PASS)
def check_dep05(root, stack):
c = CHECKS["DEP-05"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
pkg = read_json_file(pkg_path)
scripts = pkg.get("scripts", {})
suspicious = []
for key in ("preinstall", "postinstall", "install"):
val = scripts.get(key, "")
if val and any(kw in val for kw in ("curl", "wget", "fetch", "exec", "eval", "sh ", "bash ")):
suspicious.append(Finding("package.json", 0, f'"{key}": "{val}"'))
if suspicious:
return Result(c, Status.FAIL, "Suspicious install script detected in package.json", suspicious)
return Result(c, Status.PASS)
def check_dep06(root, stack):
c = CHECKS["DEP-06"]
if not stack.has_node:
return Result(c, Status.SKIP, "Not a Node.js project")
pkg_path = os.path.join(root, "package.json")
if not os.path.isfile(pkg_path):
return Result(c, Status.SKIP)
findings = grep_files(root, r'"\*"', exts={".json"})
findings = [f for f in findings if "package.json" in f.file and "node_modules" not in f.file]
if findings:
return Result(c, Status.FAIL, "Wildcard (*) version found", findings)
return Result(c, Status.PASS)
def check_fe01(root, stack):
c = CHECKS["FE-01"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
# Next.js metadata export
meta_findings = grep_files(root,
r"(export\s+(const|async\s+function)\s+metadata|generateMetadata|<title>|og:title|og:description)",
exts=JS_EXTS | {".html"})
if meta_findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No meta tags or Next.js metadata export found")
def check_fe02(root, stack):
c = CHECKS["FE-02"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
favicon = file_exists_in(root, "favicon.ico", "favicon.png", "favicon.svg",
"favicon.webp", "icon.png", "icon.ico")
if favicon:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No favicon file found")
def check_fe03(root, stack):
c = CHECKS["FE-03"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
page_404 = file_exists_in(root, "404.tsx", "404.jsx", "404.html",
"not-found.tsx", "not-found.jsx")
if page_404:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No custom 404 or not-found page found")
def check_fe09(root, stack):
c = CHECKS["FE-09"]
if not stack.is_web and not stack.has_node:
return Result(c, Status.SKIP, "Not a web project")
public_robots = os.path.join(root, "public", "robots.txt")
root_robots = os.path.join(root, "robots.txt")
if os.path.isfile(public_robots) or os.path.isfile(root_robots):
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No robots.txt found")
def check_obs01(root, stack):
c = CHECKS["OBS-01"]
findings = grep_files(root,
r"(@sentry/|sentry-|LogRocket|Bugsnag|datadogRum|Rollbar|Honeybadger|newrelic)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No error monitoring library detected")
def check_obs03(root, stack):
c = CHECKS["OBS-03"]
findings = grep_files(root,
r"(winston|pino|bunyan|structlog|loguru|import logging)",
exts=ALL_CODE_EXTS | {".json"})
if findings:
return Result(c, Status.PASS)
return Result(c, Status.FAIL, "No structured logging library detected")
CATEGORY_CHECKS = {
"SEC": [check_sec01, check_sec04, check_sec05, check_sec06, check_sec07,
check_sec08, check_sec11, check_sec13, check_sec14, check_sec17, check_sec18],
"DB": [check_db03, check_db05, check_db06, check_db07, check_db08, check_db12],
"DEPLOY": [check_deploy09, check_deploy10],
"CODE": [check_code01, check_code03, check_code07, check_code09, check_code10,
check_code11, check_code12, check_code13, check_code14],
"AI": [check_ai01, check_ai02, check_ai03, check_ai05],
"DEP": [check_dep01, check_dep05, check_dep06],
"FE": [check_fe01, check_fe02, check_fe03, check_fe09],
"OBS": [check_obs01, check_obs03],
}
CATEGORY_ORDER = ["SEC", "DB", "CODE", "DEP", "AI", "DEPLOY", "FE", "OBS"]
# ---------------------------------------------------------------------------
# Manual check runner
# ---------------------------------------------------------------------------
def run_manual_checks(stack: Stack, interactive: bool, category_filter: Optional[str]) -> List[Result]:
results = []
applicable = []
for chk in MANUAL_CHECKS:
if category_filter and chk.category != category_filter.upper():
continue
if chk.stack == "ai" and not stack.has_ai:
results.append(Result(chk, Status.SKIP, "No AI/LLM usage detected"))
continue
if chk.stack == "web" and not stack.is_web:
results.append(Result(chk, Status.SKIP, "Not a web project"))
continue
if chk.stack == "vps" and stack.deploy_target not in ("docker", "vps", ""):
results.append(Result(chk, Status.SKIP, "Not a VPS/Docker deployment"))
continue
applicable.append(chk)
if not interactive or not applicable:
for chk in applicable:
results.append(Result(chk, Status.MANUAL, "Not confirmed (run without --no-interactive to answer)"))
return results
print()
print(bold("Manual Checks") + " — answer Y/N for each:")
print()
for chk in applicable:
sev_label = {
Severity.CRITICAL: red("CRITICAL"),
Severity.HIGH: yellow("HIGH"),
Severity.ADVISORY: dim("ADVISORY"),
}[chk.severity]
while True:
try:
answer = input(f" [{sev_label}] [{chk.id}] {chk.description} [y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = "n"
if answer in ("y", "yes"):
results.append(Result(chk, Status.PASS))
break
elif answer in ("n", "no", ""):
results.append(Result(chk, Status.FAIL, "Not confirmed"))
break
print(" Please enter Y or N.")
return results
# ---------------------------------------------------------------------------
# Verdict / output
# ---------------------------------------------------------------------------
def severity_for_result(r: Result) -> Severity:
return r.check.severity
def print_report(all_results: List[Result], stack: Stack, scan_time: float,
verbose: bool) -> int:
critical = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.CRITICAL]
high = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.HIGH]
advisory = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.ADVISORY]
stack_desc = []
if stack.framework: stack_desc.append(stack.framework.capitalize())
if stack.has_supabase: stack_desc.append("Supabase")
if stack.deploy_target: stack_desc.append(stack.deploy_target.capitalize())
if stack.has_python and stack.py_framework: stack_desc.append(stack.py_framework.capitalize())
if not stack_desc: stack_desc.append("Unknown")
stack_str = " + ".join(stack_desc)
print()
print(bold("SHIP GATE REPORT"))
print("=" * 48)
print(f"Stack: {stack_str}")
print(f"Scan time: {scan_time:.1f}s")
print(f"Checks: {len(all_results)} total")
print()
def _section(label, items, color_fn):
if not items and not verbose:
return
print(bold(f"{label} ({len(items)} item{'s' if len(items) != 1 else ''})"))
for r in items:
status_str = {
Status.FAIL: red("FAIL "),
Status.MANUAL: yellow("MANUAL"),
Status.PASS: green("PASS "),
Status.SKIP: dim("SKIP "),
}[r.status]
print(f" {status_str} [{r.check.id}] {r.check.description}")
if r.message:
print(f" {dim(r.message)}")
for f in r.findings[:3]:
print(f" {dim(f.file)}:{f.line} {dim(f.snippet[:80])}")
print()
if critical:
_section(red("CRITICAL") + " (must fix before shipping)", critical, red)
if high:
_section(yellow("HIGH") + " (should fix before shipping)", high, yellow)
if advisory:
_section(dim("ADVISORY") + " (recommended)", advisory, dim)
if verbose:
passed = [r for r in all_results if r.status == Status.PASS]
if passed:
_section(green("PASS"), passed, green)
skipped = [r for r in all_results if r.status == Status.SKIP]
if skipped:
_section(dim("SKIP"), skipped, dim)
if critical:
print(red(bold(f"VERDICT: DO NOT SHIP ({len(critical)} critical issue{'s' if len(critical) != 1 else ''})")))
print("Fix critical items and re-run.")
return 1
elif high:
print(yellow(bold(f"VERDICT: SHIP WITH CAUTION ({len(high)} high issue{'s' if len(high) != 1 else ''})")))
print("Acknowledge risks and proceed only if you accept them.")
return 2
else:
print(green(bold("VERDICT: CLEAR TO SHIP")))
return 0
def print_json_report(all_results: List[Result], stack: Stack, scan_time: float) -> int:
critical = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.CRITICAL]
high = [r for r in all_results if r.status in (Status.FAIL, Status.MANUAL)
and r.check.severity == Severity.HIGH]
output = {
"version": VERSION,
"scan_time": round(scan_time, 2),
"stack": {
"framework": stack.framework,
"has_supabase": stack.has_supabase,
"has_typescript": stack.has_typescript,
"deploy_target": stack.deploy_target,
"has_ai": stack.has_ai,
},
"results": [
{
"id": r.check.id,
"description": r.check.description,
"severity": r.check.severity.value,
"category": r.check.category,
"status": r.status.value,
"message": r.message,
"findings": [
{"file": f.file, "line": f.line, "snippet": f.snippet}
for f in r.findings
],
}
for r in all_results
],
"summary": {
"critical": len(critical),
"high": len(high),
"verdict": "DO_NOT_SHIP" if critical else ("SHIP_WITH_CAUTION" if high else "CLEAR_TO_SHIP"),
},
}
print(json.dumps(output, indent=2))
return 1 if critical else (2 if high else 0)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
global USE_COLOR
parser = argparse.ArgumentParser(
description="Ship Gate — pre-production audit scanner",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("path", nargs="?", default=".",
help="Project root directory (default: current directory)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--no-color", action="store_true", help="Disable color output")
parser.add_argument("--no-interactive", action="store_true",
help="Skip manual confirmation prompts")
parser.add_argument("--category", metavar="CAT",
help="Only run one category: SEC, DB, CODE, DEP, AI, DEPLOY, FE, OBS")
parser.add_argument("--verbose", action="store_true",
help="Show PASS and SKIP results in addition to failures")
parser.add_argument("--version", action="version", version=f"ship-gate {VERSION}")
args = parser.parse_args()
if args.no_color or not sys.stdout.isatty():
USE_COLOR = False
root = os.path.abspath(args.path)
if not os.path.isdir(root):
print(f"Error: '{root}' is not a directory", file=sys.stderr)
sys.exit(1)
start = time.time()
# Detect stack
stack = detect_stack(root)
if not args.json:
print(bold("Detecting stack..."), end=" ", flush=True)
parts = []
if stack.framework: parts.append(stack.framework.capitalize())
if stack.has_supabase: parts.append("Supabase")
if stack.deploy_target: parts.append(stack.deploy_target.capitalize())
if stack.has_python and stack.py_framework: parts.append(stack.py_framework.capitalize())
if stack.has_ai: parts.append(f"AI({','.join(stack.ai_providers)})")
print(", ".join(parts) if parts else "generic project")
# Run automated checks
all_results: List[Result] = []
categories = [args.category.upper()] if args.category else CATEGORY_ORDER
for i, cat in enumerate(categories, 1):
fns = CATEGORY_CHECKS.get(cat, [])
cat_results = []
for fn in fns:
try:
r = fn(root, stack)
except Exception as e:
chk_id = fn.__name__.replace("check_", "").replace("_", "-").upper()
cat_results.append(Result(
CheckDef(chk_id, fn.__doc__ or fn.__name__, Severity.ADVISORY, cat),
Status.SKIP, f"Scanner error: {e}",
))
continue
cat_results.append(r)
all_results.extend(cat_results)
if not args.json:
n_fail = sum(1 for r in cat_results if r.status == Status.FAIL)
n_pass = sum(1 for r in cat_results if r.status == Status.PASS)
n_skip = sum(1 for r in cat_results if r.status == Status.SKIP)
label = red(f"{n_fail} FAIL") if n_fail else green("0 FAIL")
print(f" [{i}/{len(categories)}] {cat}: {label}, {n_pass} PASS, {dim(str(n_skip) + ' SKIP')}")
# Manual checks
manual_results = run_manual_checks(stack, not args.no_interactive, args.category)
all_results.extend(manual_results)
scan_time = time.time() - start
if args.json:
sys.exit(print_json_report(all_results, stack, scan_time))
else:
sys.exit(print_report(all_results, stack, scan_time, args.verbose))
if __name__ == "__main__":
main()
Related skills
How it compares
Use ship-gate for a one-shot pre-deploy codebase audit with deploy interception; use CI pipeline skills when the goal is automating checks on every pull request.
FAQ
How many checks does ship-gate run?
ship-gate runs 89 checks across eight categories prefixed SEC, DB, DEPLOY, CODE, AI, DEP, FE, and OBS. For example, SEC includes 15 automated and 3 manual checks according to the category table in the skill.
Does ship-gate fix issues it finds?
ship-gate audits and reports findings with file locations and remediation guidance but does not apply fixes, set up CI/CD, or provision infrastructure. Another skill or the developer handles remediation.
What verdicts does ship-gate return?
ship-gate ends with DO NOT SHIP when critical items fail, SHIP WITH CAUTION when high-severity issues remain, or CLEAR TO SHIP when automated and confirmed manual checks pass across the eight categories.