Skip to content

Repository files navigation

RateGuard

RateGuard

Local-first, offline static analysis CLI that finds API routes missing rate-limiting protection — and prints ready-to-apply fixes.

AI assistants ship backends without rate limiting all the time: login, signup, password-reset, checkout and webhook endpoints go straight to production with nothing in front of them. That's brute-force territory for auth endpoints, billing-abuse territory for AI/payment endpoints, and spam-queue territory for everything else. Existing scanners are hosted, paid, and probe a live URL. RateGuard scans the code itself.

  • 100% offline — no network calls, no telemetry, no API keys.
  • AST-based detection (with regex fallback for unparseable files).
  • Fast — static analysis only; runs as a pre-commit hook or on every PR.
  • Free and MIT-licensed.

Install & run

npx rateguard scan

or install once:

npm install -g rateguard
rateguard scan

No config file required. Point it at a repo and it figures the rest out.

What the output looks like

 CRITICAL (2)
┌────────────────────────────┬───────┬─────────────────────────┬───────────┬───────────────...
│ File                       │ Line  │ Route                   │ Framework │ Reason          │
├────────────────────────────┼───────┼─────────────────────────┼───────────┼───────────────...
│ src/routes/auth.ts         │ 12    │ POST /login             │ express   │ critical route  │
│ src/routes/auth.ts         │ 24    │ POST /password-reset    │ express   │ critical route  │

 MEDIUM (1)
...

2 critical, 1 medium, 0 low findings across 14 routes scanned in 6 files.

CLI

rateguard scan [path] [--format table|json] [--fail-on critical|medium|none] [--ignore <glob>]
rateguard fix  [path] [--write] [--only critical|medium]
rateguard init-hook
rateguard --help

Flags

Flag Default Meaning
--format table table prints grouped, colorized findings; json prints { routesScanned, message, findings: [{file, line, method, path, framework, severity, protected, reason}] }. When no routes are detected, findings is empty and message explains the zero case so CI consumers don't have to guess from prose.
--fail-on critical Exit non-zero when findings at or above this level exist. none always exits 0.
--ignore Extra glob to skip; repeatable. node_modules, .next, dist, build, venv, __pycache__ are always skipped regardless.
--write (fix) off Apply the generated fixes. Files are backed up to .rateguard-backup/ first, and writes are temp-file-then-move.
--only (fix) Restrict fixes to critical or medium findings.

path defaults to the current directory. .gitignore is respected.

Supported frameworks (v1)

Framework Detects Protection recognised
Express app.METHOD(path, ...), router.METHOD(...) in *.ts/js express-rate-limit, rate-limiter-flexible, custom *limiter* middleware (route-level or app.use() before registration)
FastAPI @app.METHOD(path) / @router.METHOD(path) decorators in *.py slowapi, fastapi-limiter, or Depends(<rate-limiter-ish>)
Next.js Pages Router pages/api/** default export; App Router app/api/**/route.ts HTTP-verb exports @upstash/ratelimit (route file) or a middleware.ts whose matcher covers the route
Supabase Edge Functions Deno.serve(...) / serve(...) in supabase/functions/*/index.ts In-function counter logic only (Supabase has no built-in per-function limiter; unprotected functions are always flagged)

The GitHub Action

A companion action runs on pull_request and:

  1. Lists the PR's changed files via the GitHub API.
  2. Runs rateguard scan --format json on the repo.
  3. Posts (or updates) one PR comment with a markdown table of findings in the changed files.
  4. Fails the check if any critical finding is in a changed file.

Add it to your repo:

# .github/workflows/rateguard.yml
name: RateGuard
on: [pull_request]

jobs:
  rateguard:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      issues: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - uses: ./action

How detection works

  • Each discovered route is classified critical (login/signup/password-reset/OTP/payment/webhook/admin), medium (mutating POST/PUT/DELETE/PATCH, search, upload), or low (public read-only GET).
  • Static analysis tracks per-file: known rate-limit imports (express-rate-limit, slowapi, @upstash/ratelimit, …), custom identifiers matching *rateLimit* | *limiter* | throttle | slowdown, app.use(limiter) ordering (Express), middleware.ts matcher coverage (Next.js), Depends(limiter) (FastAPI), and in-function counters (Supabase).
  • Global middleware applied before route registration counts as coverage and is reported as such — protected routes are never double-flagged.
  • Unparseable files fall back to regex route discovery so a single broken file never hides routes.

Python parsing note

FastAPI scanning uses @lezer/python, a pure-JS tolerant Python parser — no Python runtime needed on the machine running the scan. It's a CST, not a full Python AST, so detection focuses on the decorator + function-definition shapes the FastAPI docs use. Unparseable files fall back to regex.

Known limitations & design notes

  • Next.js route definitions come in more than one shape. Plain export async function GET/POST is the common case, but real apps also use re-exported handlers (export { handler as GET, handler as POST } — tRPC's fetchRequestHandler) and destructured factory exports (export const { GET, POST, PUT } = serve({...}) — Inngest). All three are detected; all emit the correct URL path and per-verb severity. Anything even more exotic (e.g. a handler built dynamically) falls back to regex per-file.
  • Two kinds of "limiter" exist. The term is overloaded: some repos use rate-limiter-flexible's RateLimiterPrisma for business-logic quotas (credit consumption, per-user thresholds — e.g. a "usage" module that charges per generation). These are imported into a service module, never applied as HTTP middleware or route guards, and RateGuard correctly does NOT count them as protecting any route. Only a limiter wired into the actual request path (Express middleware, FastAPI Depends/decorator, Next middleware.ts matcher, or an in-function counter for Supabase) counts as coverage.
  • FastAPI parsing is CST-based (@lezer/python, see earlier note). A broken Python file still yields routes via the regex fallback, but exotic decorator stacking could theoretically be missed; the fallback covers it.
  • Global-middleware coverage is order-sensitive by design — Express app.use(limiter) only protects routes registered after it (which matches how the middleware actually behaves).

Contributing

See CONTRIBUTING.md.

License

MIT © 2026 RateGuard contributors

About

Offline static analysis CLI that finds API routes missing rate-limiting protection (Express, FastAPI, Next.js, Supabase)

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages