Documentation

LeastPriv

LeastPriv reads your JavaScript and TypeScript source code, finds every cloud, database, payment, auth, and messaging call it makes across thirteen providers, and maps each call to the permission it requires. It can compare two git branches to show exactly what permissions a pull request adds or removes, and it can gate your CI pipeline on the risky ones.

What is LeastPriv

Most permission scanning tools look at your infrastructure configuration, things like Terraform, CloudFormation, or IAM policy JSON. LeastPriv looks at your application code instead. It parses your source directly and detects the actual SDK calls your code makes at runtime, things like an S3Client deleting an object, a Supabase client removing a user, or a Firestore document being updated.

The output is a permission tree for each file you scan, and a permission diff between two branches. That same diff is what runs automatically on a pull request through GitHub Actions.

Why static analysis, not an LLM

LeastPriv is built on deterministic AST parsing using ts-morph, rather than an LLM reading the code and guessing. A security gate needs to produce the same result on the same input every time, and it needs to be auditable, meaning you can point at the exact line and node that triggered a finding. A probabilistic tool can miss things silently or give a different answer on different runs, and that rules it out for something that decides whether a pull request can merge.

Design principle Detection is 100% static analysis. Whether a permission is found never depends on model inference.

Architecture

Source file (.js / .ts) │ ▼ ts-morph parses into an AST │ ▼ Per-provider detector walks the tree (imports → variable provenance → call chains) │ ▼ Match against a rules table (SDK method → permission + category) │ ▼ Detection[] (structured, typed findings) │ ├──▶ renderTree() → terminal output ├──▶ diffPermissions() → branch comparison └──▶ --json → machine-readable output

Parser

ts-morph wraps the TypeScript compiler, which gives symbol aware traversal instead of raw text matching.

Provenance tracking

Tracks which variable came from which SDK or service, so cache.delete() is never confused with db.collection().delete().

Rules tables

Per-provider JSON mapping each SDK method to a permission string and a risk category. AWS rules are generated automatically from installed SDK packages.

Git diffing

Uses git worktree to materialize two branches into isolated temp directories without touching your working tree.


AWS

AWS coverage is generated from the SDK packages themselves. Every @aws-sdk/client-* package exports its full list of Command classes, and LeastPriv reads that list directly instead of relying on a hand maintained list.

ServiceIAM prefix
S3s3:
DynamoDBdynamodb:
Lambdalambda:
SQSsqs:
SNSsns:
IAMiam:
Secrets Managersecretsmanager:
CloudWatch Logslogs:
API Gatewayapigateway:
SESses:
Cognito Identity Providercognito-idp:
KMSkms:
STSsts:
EventBridgeevents:
SSMssm:
Kinesiskinesis:
Step Functionsstates:

A few services are intentionally left out: EC2, RDS, ALB, Route 53, CloudFront, and ECS. These are provisioning and infrastructure services, not services your application code calls at runtime.

SDK version note LeastPriv detects both AWS SDK generations. v3 covers the modular, Command based style shown above (new DeleteObjectCommand(...)). v2 covers the older namespaced style, using patterns like const AWS = require("aws-sdk"), new AWS.S3(), and s3.deleteObject(...) with no Command classes at all. v2 is officially in maintenance mode but is still common in older production code, which is why both are supported.

Firebase

Full surface coverage across both SDK generations:

  • v8 and Admin SDK: chained calls such as db.collection("users").doc(id).delete()
  • v9 modular SDK: a functional style such as deleteDoc(doc(db, "users", id)), including renamed destructured imports
  • Firestore, Storage, Auth, Realtime Database, Cloud Functions, Cloud Messaging, and Remote Config
  • Transactions (runTransaction callback binding) and batched writes

Detection relies on variable provenance tracking, a symbol table built during parsing that knows db came from admin.firestore(). Because of that, a plain Map or an unrelated object with a .delete() method is never mistaken for a real Firebase call.

Supabase

  • Postgrest (tables): select, insert, update, upsert, delete, rpc
  • Storage: upload, download, remove, list, move, copy, createSignedUrl
  • Auth Admin: createUser, deleteUser, updateUserById, listUsers
  • Edge Functions: functions.invoke()
  • Realtime: channel(), subscribe(), removeChannel()

Stripe

Covers the shared method pattern used across every Stripe resource (retrieve, list, create, update, del, cancel), plus specific overrides for methods whose risk depends on which resource they're called on.

MethodCategory
refunds.createdelete
customers.deldelete
subscriptions.canceldelete
payouts.createadmin
paymentIntents.canceldelete
webhookEndpoints.createadmin

refunds.create, payouts.create, and subscriptions.cancel move real money or stop active billing, so they're treated as the highest risk operations in this provider.

Auth0

Covers users, roles, clients, connections, organizations, Actions, attack protection settings, and tickets.

  • Users: create, update, delete, RBAC role and permission assignment, MFA enrollment management
  • Roles & Clients: creation, updates, deletion, and secret rotation
  • Organizations: membership, invitations, and role assignment within a tenant
  • Actions: the serverless code that runs inside Auth0's login pipeline, including deploy
  • Attack protection: brute force and suspicious IP throttling configuration

assignRoles, assignPermissions, and any Action deploy are categorized as admin, since they change what a user or a live login flow is authorized to do.

Clerk

Covers users, organizations, sessions, invitations, allowlist and blocklist identifiers, JWT templates, and redirect URLs.

  • Users: createUser, updateUser, banUser, lockUser, deleteUser
  • Organizations: membership and role changes, invitations, deletion
  • Sessions: token generation and revocation
  • Allowlist / blocklist: which identifiers are permitted or blocked from signing up

banUser and deleteUser are categorized as admin and delete respectively, since one revokes active access immediately and the other removes the account entirely.

Twilio

Covers messages, calls, verification (2FA/OTP), services, API keys, and regulatory addresses.

  • Messages & calls: sending, listing, and redacting SMS, voice, and WhatsApp communication
  • Verifications: dispatching and checking OTP codes
  • API keys: creation and revocation

keys.create is flagged as a high risk admin operation, since a leaked or over-broadly scoped API key can send messages or place calls on the account's behalf.

Resend

Covers emails, domains, API keys, audiences, contacts, and batch sending.

  • Emails: send, status lookups, canceling a scheduled send
  • Domains: registering and verifying a sending domain
  • Audiences & contacts: managing marketing subscriber lists

apiKeys.create is flagged as high risk, since a new key can send email on behalf of the domain the moment it's created.

Pinecone

Covers vector data operations and index level control.

  • Vector data: query, upsert, fetch, deleteOne, deleteMany, deleteAll
  • Index control: creation, configuration, and deletion of indexes and backup collections

deleteAll and deleteIndex are flagged as the highest severity operations in this provider, since both are irreversible and can wipe an entire namespace or index of embeddings used for retrieval.

Prisma

Covers standard model CRUD plus Prisma's raw SQL escape hatches.

  • Reads: findUnique, findFirst, findMany, count, aggregate, groupBy
  • Writes: create, createMany, update, updateMany, upsert
  • Deletes: delete, deleteMany
  • Raw SQL: $queryRaw, $queryRawUnsafe, $executeRaw, $executeRawUnsafe, $transaction

The raw SQL methods are categorized as admin regardless of what the underlying query does, since a static analyzer can't always see inside a raw SQL string to know if it's a read or a write.

Drizzle ORM

Covers the query builder's core methods: select, selectDistinct, insert, update, delete, execute, and transaction, along with the SQLite specific result methods run, all, get, and values.

execute is categorized as admin, since it runs a raw SQL statement or prepared query directly.

Mongoose

Covers model level operations built on top of the native MongoDB driver.

  • Reads: find, findOne, findById, aggregate, countDocuments
  • Writes: create, insertMany, updateOne, updateMany, save
  • Deletes: deleteOne, deleteMany, findByIdAndDelete
  • Schema/index management: syncIndexes, createIndexes, cleanIndexes

MongoDB (native driver)

Covers the collection, database, and client levels of the official driver.

  • Collection: the usual CRUD, plus index management and drop
  • Database: createCollection, dropCollection, dropDatabase, user management
  • Client: connection lifecycle and session handling

dropDatabase is the single highest risk operation covered anywhere in LeastPriv's rule sets. It's categorized as delete and destroys every collection in the database at once.

Raw SQL

Statement level detection across the SQL keywords used by Postgres, MySQL, and SQLite, covering everything from SELECT and INSERT through administrative statements like GRANT, VACUUM, and FLUSH.

CategoryExamples
readSELECT, WITH, SHOW, EXPLAIN
writeINSERT, UPDATE, UPSERT, MERGE, CALL
deleteDELETE, TRUNCATE, DROP
adminGRANT, REVOKE, VACUUM, FLUSH, KILL

Installation

npm install -g LeastPriv

Or run it without installing anything:

npx LeastPriv analyze ./src

LeastPriv analyze

Scans a single file or an entire directory and prints a permission tree.

LeastPriv analyze avatar.js
LeastPriv analyze ./src

Example output:

📄 avatar.js  HIGH RISK
════════════════════

  └── 🔶 AWS
      └── s3 (1 operations)
          └── DeleteObjectCommand  DELETE  (line 7)
              ├── 📦 bucketName
              ├── 🔑 s3:DeleteObject
              └── 📝 Permanently deletes a file from S3 storage

📊 Summary
═══════════════════════════════════════════
  Total Operations: 1
  By Category:
    DELETE  1
  ⚠️  Risk Assessment: High-risk operations detected

Directories are scanned recursively. node_modules, .git, and dist are skipped.

LeastPriv diff

Compares permissions between two git branches. This is the core command behind the pull request check.

LeastPriv diff main feature
LeastPriv diff main feature --repo ./path/to/repo
LeastPriv diff main feature --block

Example output:

📊 Permission diff: main → feature

+ Added (1):
  + aws:s3.DeleteObjectCommand (delete) — app.js:4

⚠️  WARNING: This PR adds 1 sensitive permission(s). Please review carefully:
   ⚠️  aws:s3.DeleteObjectCommand (delete) — app.js:4

   This is a warning only — merge is not blocked.
FlagEffect
--repo <path>Path to the git repository (defaults to the current directory)
--jsonOutputs the diff as JSON instead of formatted text
--blockExits with code 1 on sensitive additions (the default is to warn only, exiting 0)

JSON output

Both commands accept --json for machine readable output. This is what the GitHub Action uses internally, and it works just as well for any other tooling you want to build on top.

{
  "provider": "aws",
  "service": "s3",
  "operation": "DeleteObjectCommand",
  "category": "delete",
  "cloudPermission": "s3:DeleteObject",
  "description": "Permanently deletes a file from S3 storage",
  "resource": "bucketName",
  "location": { "file": "avatar.js", "line": 7, "column": 19 }
}

GitHub Actions integration

Add a workflow that runs on every pull request. It checks out both the pull request branch and LeastPriv's own source, then runs diff between main and the incoming branch.

name: LeastPriv Permission Check

on:
  pull_request:
    branches: [main]

jobs:
  permission-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout PR branch
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          path: target-repo

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

      - name: Install LeastPriv
        run: npm install -g LeastPriv

      - name: Run permission diff
        run: LeastPriv diff origin/main HEAD --repo ./target-repo

GitHub reads the exit code of the last step and shows a pass or fail status directly on the pull request. If you also add a branch protection rule requiring this check, a failing result can block the merge button until someone reviews it.

Blocking vs. warning

By default, diff always exits 0. Sensitive permissions are printed as a clear warning, but the check still passes and nothing is blocked. This is the recommended default for most teams, because a tool that blocks by default tends to get bypassed or disabled the first time it produces a false positive.

ModeCommandResult on sensitive change
Warn (default)LeastPriv diff main feature⚠️ Warning shown, exit 0, check passes
BlockLeastPriv diff main feature --block❌ Warning shown, exit 1, check fails
Recommendation Start in warn mode. Move to --block once you have confirmed the false positive rate on your actual codebase is low enough that a blocked merge is always meaningful.

Detection model

Every finding is a structured object with the same shape, regardless of provider:

FieldMeaning
provideraws, firebase, or supabase
servicefor example s3, firestore, or postgrest
operationThe SDK method or command name that was detected
categoryread write delete admin auth
cloudPermissionThe formal permission string, where one exists. This only applies to AWS, since Firebase and Supabase don't have an equivalent formal permission model.
resourceThe table, bucket, or document path, when it can be extracted from the call
locationThe file, line, and column of the detected call

category is the one field with a consistent meaning across all three providers. AWS is the only provider with a formal, published permission naming system, so Firebase and Supabase operations are categorized using LeastPriv's own taxonomy instead.

Limitations

  • It only sees SDK calls made directly in application code. Infrastructure defined in Terraform, CloudFormation, or the AWS, GCP, or Azure console is invisible to it, by design.
  • Cross file variable provenance, such as a client initialized in one file and used in another, is not yet tracked for every provider. This currently works reliably within a single file.
  • GCP and Azure are not covered.
  • Redis and other in-memory cache access (ioredis, redis) is not yet covered.

Roadmap

  1. Redis and ioredis detection, including flagging destructive commands like FLUSHALL
  2. Cross file provenance tracking across all providers, so a client initialized in one file is still recognized when used in another
  3. TypeORM and Sequelize, to round out ORM coverage alongside Prisma and Drizzle
  4. GCP and Azure providers
  5. Posting warnings as a PR comment through the GitHub API, so they're visible without opening the Action logs

Contact

Feel free to reach out with questions, feedback, or ideas.