Supabase rls audit
@shipcheck/supabase-rls-audit
Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.
- Activity
- New: be the first to try it
- Installs
- 0 installs
- Likes
- 0 likes
- Clones
- 0 clones
- 1{
- 2 "name": "supabase-rls-audit",
- 3 "owner": "shipcheck",
- 4 "version": "1.0.0",
- 5 "type": "skill",
- 6 "description": "Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.",
- 7 "category": "devops",
- 8 "license": "MIT",
- 9 "compat": ["claude-code"],
- 10 "permissions": {
- 11 "shell": ["node scripts/rls-audit.mjs"],
- 12 "files": {
- 13 "read": ["./supabase/migrations/**", "./supabase/schema.sql"],
- 14 "write": ["./out/**", "./supabase/migrations/**"]
- 15 }
- 16 },
- 17 "tests": {
- 18 "cases": [
- 19 {
- 20 "name": "flags a table with RLS off and a policy that lets any user write",
- 21 "inputs": {
- 22 "supabase/migrations/0001_init.sql": "create table public.todos (\n id bigint generated always as identity primary key,\n user_id uuid not null,\n title text\n);\nalter table public.todos enable row level security;\ncreate policy \"todos any user\" on public.todos for all to authenticated using (auth.uid() is not null);\n\ncreate table public.payments (id bigint primary key, user_id uuid, amount_cents int);\n\ncreate table public.posts (id bigint primary key, body text);\nalter table public.posts enable row level security;\ncreate policy \"public posts\" on public.posts for select using (true);\n"
- 23 },
- 24 "checks": [
- 25 {
- 26 "type": "command_exits_zero",
- 27 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md"
- 28 },
- 29 {
- 30 "type": "output_matches_schema",
- 31 "path": "out/rls-audit.json",
- 32 "schema": {
- 33 "type": "object",
- 34 "required": ["summary", "tables", "findings"],
- 35 "properties": {
- 36 "summary": {
- 37 "type": "object",
- 38 "properties": {
- 39 "tables": { "const": 3 },
- 40 "rls_enabled": { "const": 2 },
- 41 "high": { "const": 2 },
- 42 "medium": { "const": 1 }
- 43 }
- 44 }
- 45 }
- 46 }
- 47 },
- 48 { "type": "file_contains", "path": "out/rls-audit.md", "text": "| public.payments | **OFF** | 0 |" },
- 49 { "type": "file_contains", "path": "out/rls-audit.md", "text": "policy-any-user-write" }
- 50 ]
- 51 },
- 52 {
- 53 "name": "replays later migrations: a fixed table and a dropped policy come out clean",
- 54 "inputs": {
- 55 "supabase/migrations/0001_init.sql": "create table public.notes (id bigint primary key, user_id uuid not null, body text);\nalter table public.notes enable row level security;\ncreate policy \"open notes\" on public.notes for all using (true) with check (true);\n",
- 56 "supabase/migrations/0002_fix.sql": "-- tighten notes\ndrop policy if exists \"open notes\" on public.notes;\ncreate policy \"own notes\" on public.notes for all to authenticated\n using ((select auth.uid()) = user_id)\n with check ((select auth.uid()) = user_id);\n"
- 57 },
- 58 "checks": [
- 59 {
- 60 "type": "command_exits_zero",
- 61 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json"
- 62 },
- 63 {
- 64 "type": "output_matches_schema",
- 65 "path": "out/rls-audit.json",
- 66 "schema": {
- 67 "type": "object",
- 68 "properties": {
- 69 "summary": {
- 70 "type": "object",
- 71 "properties": {
- 72 "tables": { "const": 1 },
- 73 "high": { "const": 0 },
- 74 "medium": { "const": 0 },
- 75 "low": { "const": 0 }
- 76 }
- 77 },
- 78 "findings": { "type": "array", "maxItems": 0 }
- 79 }
- 80 }
- 81 }
- 82 ]
- 83 },
- 84 {
- 85 "name": "catches user_metadata trust, definer views, and unpinned SECURITY DEFINER functions",
- 86 "inputs": {
- 87 "supabase/migrations/0001.sql": "create table public.orders (id bigint primary key, user_id uuid);\nalter table public.orders enable row level security;\ncreate policy \"admins\" on public.orders for select to authenticated using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin');\ncreate view public.order_totals as select user_id, count(*) from public.orders group by user_id;\ncreate function public.whoami() returns uuid language sql security definer as $$ select auth.uid(); $$;\n"
- 88 },
- 89 "checks": [
- 90 {
- 91 "type": "command_exits_zero",
- 92 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md"
- 93 },
- 94 { "type": "file_contains", "path": "out/rls-audit.md", "text": "policy-user-metadata" },
- 95 { "type": "file_contains", "path": "out/rls-audit.md", "text": "view-security-definer" },
- 96 { "type": "file_contains", "path": "out/rls-audit.md", "text": "security-definer-function" }
- 97 ]
- 98 }
- 99 ]
- 100 }
- 101}
