From ba651e2f5133c7a5c707b6dd9915b67966b9603d Mon Sep 17 00:00:00 2001 From: rx4u Date: Mon, 13 Apr 2026 12:26:19 +0530 Subject: [PATCH 1/3] Add ship-gate: pre-production audit skill --- engineering/ship-gate/SKILL.md | 190 +++++ engineering/ship-gate/references/checks.md | 483 +++++++++++++ engineering/ship-gate/references/patterns.md | 687 +++++++++++++++++++ 3 files changed, 1360 insertions(+) create mode 100644 engineering/ship-gate/SKILL.md create mode 100644 engineering/ship-gate/references/checks.md create mode 100644 engineering/ship-gate/references/patterns.md diff --git a/engineering/ship-gate/SKILL.md b/engineering/ship-gate/SKILL.md new file mode 100644 index 000000000..0575c0659 --- /dev/null +++ b/engineering/ship-gate/SKILL.md @@ -0,0 +1,190 @@ +--- +name: ship-gate +description: > + Pre-production audit that scans a codebase for security, database, + deployment, code quality, AI/LLM, dependency, frontend, and observability + issues. Intercepts deploy commands and blocks until critical items pass. + Stack-agnostic. Use for "run ship gate", "am I ready to ship", + "pre-launch audit", "can I deploy", "push to production", "go live + checklist", "preflight check". Not for CI/CD setup or infra provisioning. +metadata: + author: Rajaraman Arumugam + version: 1.0.0 +--- + +# 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 -> Gemini +``` + +Report 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 + +- **app-planner**: ship-gate runs after the build plan is complete +- **subagent-orchestrator**: ship-gate is the final gate before deploy +- **backend-patterns**: fixes for DB and security findings +- **shadcn-stack / heroui-stack**: fixes for frontend findings +- **systematic-debugging**: deep investigation of flagged issues diff --git a/engineering/ship-gate/references/checks.md b/engineering/ship-gate/references/checks.md new file mode 100644 index 000000000..2859387c3 --- /dev/null +++ b/engineering/ship-gate/references/checks.md @@ -0,0 +1,483 @@ +# 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 ``, `<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. diff --git a/engineering/ship-gate/references/patterns.md b/engineering/ship-gate/references/patterns.md new file mode 100644 index 000000000..b103adc7d --- /dev/null +++ b/engineering/ship-gate/references/patterns.md @@ -0,0 +1,687 @@ +# 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: +```bash +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: + +```bash +# 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/null +``` + +### SEC-04: CORS wildcard + +```bash +grep -rnE $EXCLUDE \ + "(origin:\s*['\"]?\*['\"]?|Access-Control-Allow-Origin.*\*|cors\(\s*\))" \ + . 2>/dev/null +``` + +### SEC-05: CSRF protection missing + +```bash +# 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/null +``` + +### SEC-08: Weak password hashing + +```bash +# 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/null +``` + +### SEC-11: CSP headers + +```bash +# 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/null +``` + +### SEC-13: Unsafe eval/innerHTML + +```bash +# 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 file +``` + +### SEC-15: Cookie security flags + +```bash +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/null +``` + +### SEC-06: Input validation + +```bash +# 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/null +``` + +### SEC-07: Rate limiting + +```bash +grep -rnE $EXCLUDE \ + "(express-rate-limit|@upstash/ratelimit|rate-limiter|slowapi|throttle)" \ + package.json requirements.txt . 2>/dev/null +``` + +### SEC-09: Token expiry + +```bash +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/null +``` + +### SEC-14: Sensitive data in URLs/logs + +```bash +# 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/null +``` + +### SEC-16: File upload validation + +```bash +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/null +``` + +### SEC-17/18: .env in repo + +```bash +# 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) + +```bash +# 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/null +``` + +### DB-07: Supabase RLS + +```bash +# 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 +done +``` + +### DB-08: service_role in client code + +```bash +grep -rnE $EXCLUDE \ + "(service_role|serviceRole|SUPABASE_SERVICE_ROLE)" \ + src/ app/ pages/ components/ public/ lib/client 2>/dev/null +``` + +### DB-05: Connection pooling + +```bash +# 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/null +``` + +### DB-06: Migrations in version control + +```bash +# 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 -5 +``` + +### DB-12: PII stored unencrypted + +```bash +# 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 + +```bash +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/null +``` + +### CODE-03: Empty catch blocks + +```bash +grep -rnPzo $EXCLUDE \ + "catch\s*\([^)]*\)\s*\{\s*\}" \ + --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \ + . 2>/dev/null +``` + +### CODE-07: TODO-auth patterns + +```bash +grep -rnEi $EXCLUDE \ + "(TODO|FIXME|HACK|XXX).*(auth|security|permission|validation|sanitiz)" \ + . 2>/dev/null +``` + +### CODE-08: Unhandled promise rejections + +```bash +# 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 wrapping +``` + +### CODE-09: React error boundaries + +```bash +# 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/null +``` + +### CODE-12: Lockfile committed + +```bash +# 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 +done +``` + +### CODE-13: Wildcard versions + +```bash +# Check for * or empty version in package.json +grep -nE '"[^"]+"\s*:\s*"\*"' package.json 2>/dev/null +``` + +### CODE-02: Async without error handling + +```bash +# 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/null +``` + +### CODE-04: Loading and error states + +```bash +# 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/null +``` + +### CODE-05: Pagination on list endpoints + +```bash +# 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/null +``` + +### CODE-10: Leaked stack traces + +```bash +grep -rnE $EXCLUDE \ + "(error\.stack|\.stack\)|err\.message.*res\.(json|send)|traceback)" \ + --include="*.ts" --include="*.js" --include="*.py" \ + . 2>/dev/null +``` + +### CODE-11: eslint-disable on security rules + +```bash +grep -rnE $EXCLUDE \ + "eslint-disable.*(no-eval|no-implied-eval|no-script-url|security)" \ + --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \ + . 2>/dev/null +``` + +### CODE-14: TypeScript strict mode + +```bash +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 + +```bash +# 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/null +``` + +### AI-02: Prompt injection vectors + +```bash +# User input concatenated directly into prompts +grep -rnE $EXCLUDE \ + "(messages\.push|content:.*\$\{|content:.*\+\s*user|prompt.*\+)" \ + --include="*.ts" --include="*.js" --include="*.py" \ + . 2>/dev/null +``` + +### AI-03: LLM API keys in frontend + +```bash +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/null +``` + +### AI-04: Rate limiting on AI endpoints + +```bash +# 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 files +``` + +### AI-05: AI output sanitization + +```bash +# 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/null +``` + +### AI-06: MCP server input validation + +```bash +# 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 + +```bash +grep -nE '"(git|git\+|http|https|file):' package.json 2>/dev/null +grep -nE '"github:' package.json 2>/dev/null +``` + +### DEP-04: npm audit + +```bash +# 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/null +``` + +### DEP-05: Suspicious install scripts + +```bash +grep -A2 '"preinstall"\|"postinstall"\|"install"' package.json 2>/dev/null +``` + +### DEP-06: Wildcard versions + +```bash +grep -nE '"\*"' package.json 2>/dev/null +grep -nE '"latest"' package.json 2>/dev/null +``` + +--- + +## FE: Frontend Quality Patterns + +### FE-01: Meta tags + +```bash +# 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/null +``` + +### FE-02: Favicon + +```bash +find . -maxdepth 3 \( -name "favicon.*" -o -name "icon.*" \) \ + -not -path "*/node_modules/*" 2>/dev/null +``` + +### FE-03: Custom 404 page + +```bash +find . -maxdepth 4 \( -name "404.*" -o -name "not-found.*" \) \ + -not -path "*/node_modules/*" 2>/dev/null +``` + +### FE-05: Image alt text + +```bash +# 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/null +``` + +### FE-09: robots.txt + +```bash +find . -maxdepth 2 -name "robots.txt" \ + -not -path "*/node_modules/*" 2>/dev/null +``` + +### FE-07: Form validation feedback + +```bash +# 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/null +``` + +### FE-10: Image optimization + +```bash +# 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 + +```bash +grep -rnE $EXCLUDE \ + "(@sentry|sentry-|LogRocket|Bugsnag|datadogRum|Rollbar|Honeybadger|newrelic)" \ + package.json . 2>/dev/null +``` + +### OBS-03: Structured logging + +```bash +# 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 + +```bash +grep -rnE $EXCLUDE \ + "(\/health|\/healthz|\/api\/health|\/status|\/readyz)" \ + --include="*.ts" --include="*.js" --include="*.py" \ + . 2>/dev/null +``` + +### DEPLOY-10: Console vs structured logging (server) + +```bash +# 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 +``` From 23292e009213d8b00f988d111be7956b93e34f3f Mon Sep 17 00:00:00 2001 From: rx4u <rajaaraman@gmail.com> Date: Mon, 13 Apr 2026 15:07:51 +0530 Subject: [PATCH 2/3] fix: add license frontmatter, replace non-existent skill references --- engineering/ship-gate/SKILL.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/engineering/ship-gate/SKILL.md b/engineering/ship-gate/SKILL.md index 0575c0659..665ddb506 100644 --- a/engineering/ship-gate/SKILL.md +++ b/engineering/ship-gate/SKILL.md @@ -7,6 +7,7 @@ description: > Stack-agnostic. Use for "run ship gate", "am I ready to ship", "pre-launch audit", "can I deploy", "push to production", "go live checklist", "preflight check". Not for CI/CD setup or infra provisioning. +license: MIT metadata: author: Rajaraman Arumugam version: 1.0.0 @@ -183,8 +184,8 @@ This skill does not: ## Integration Points -- **app-planner**: ship-gate runs after the build plan is complete -- **subagent-orchestrator**: ship-gate is the final gate before deploy -- **backend-patterns**: fixes for DB and security findings -- **shadcn-stack / heroui-stack**: fixes for frontend findings -- **systematic-debugging**: deep investigation of flagged issues +- **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 +- **focused-fix**: deep investigation and systematic repair of flagged issues From dc1aa550df9a499e7fe2b106d2830d988e596dea Mon Sep 17 00:00:00 2001 From: rx4u <rajaaraman@gmail.com> Date: Mon, 13 Apr 2026 15:20:49 +0530 Subject: [PATCH 3/3] ship-gate: remove focused-fix substitution, add Python scanner script - Remove focused-fix from Integration Points (was a substitution for systematic-debugging which does not exist in this repo) - Add scripts/ship_gate_scanner.py: stdlib-only pre-production audit CLI covering all 8 categories (SEC, DB, CODE, DEP, AI, DEPLOY, FE, OBS) with JSON output, ANSI color, interactive manual checks, and exit codes --- engineering/ship-gate/SKILL.md | 1 - .../ship-gate/scripts/ship_gate_scanner.py | 1231 +++++++++++++++++ 2 files changed, 1231 insertions(+), 1 deletion(-) create mode 100644 engineering/ship-gate/scripts/ship_gate_scanner.py diff --git a/engineering/ship-gate/SKILL.md b/engineering/ship-gate/SKILL.md index 665ddb506..5243045b9 100644 --- a/engineering/ship-gate/SKILL.md +++ b/engineering/ship-gate/SKILL.md @@ -188,4 +188,3 @@ This skill does not: - **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 -- **focused-fix**: deep investigation and systematic repair of flagged issues diff --git a/engineering/ship-gate/scripts/ship_gate_scanner.py b/engineering/ship-gate/scripts/ship_gate_scanner.py new file mode 100644 index 000000000..c9f7a99f0 --- /dev/null +++ b/engineering/ship-gate/scripts/ship_gate_scanner.py @@ -0,0 +1,1231 @@ +#!/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"), + "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 + if all_findings: + return Result(c, Status.FAIL, "Unsafe eval() or unsanitized dangerouslySetInnerHTML", all_findings) + 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()