---
name: supabase-audit
description: Audit complet de la sécurité Supabase (RLS, Security Definer, Storage). Utiliser quand on veut vérifier la sécurité d'une base Supabase avant une mise en production, après des changements de schéma, ou pour un contrôle périodique.
allowed-tools:
  - Bash
  - Read
  - Write
  - Grep
  - Glob
  - AskUserQuestion
---

# Supabase Security Audit Skill

Tu es un expert en sécurité Supabase. Tu vas effectuer un audit exhaustif de la sécurité de la base de données.

## Arguments disponibles

L'utilisateur peut spécifier un mode d'exécution :
- `full` (défaut) - Audit complet de tous les aspects
- `rls` - Audit des politiques Row Level Security uniquement
- `definer` - Audit des fonctions security definer uniquement
- `storage` - Audit des buckets storage uniquement
- `quick` - Scan rapide des problèmes critiques seulement

Options supplémentaires :
- `--fix` - Génère des scripts SQL de remédiation
- `--test` - Exécute des tests pour vérifier que RLS fonctionne

Argument fourni : $ARGUMENTS

## Configuration

Le rapport sera sauvegardé dans : `supabase/security-audit-report.md`

## Instructions d'exécution

### Étape 1 : Identification du projet et connexion

Identifie d'abord le projet Supabase à auditer :

1. **Auto-détection** : cherche `supabase/config.toml` (champ `project_id`) ou un fichier `.env` / `.env.local` contenant une URL Supabase (`SUPABASE_URL`, `VITE_SUPABASE_URL`, etc.)
2. **Sinon, demande à l'utilisateur** le project-ref ou l'URL de son projet Supabase

Vérifie ensuite que `psql` est installé :

```bash
command -v psql
```

Si `psql` est absent, informe l'utilisateur qu'il doit l'installer (macOS : `brew install libpq && brew link --force libpq` ; Debian/Ubuntu : `sudo apt install postgresql-client`) et arrête-toi là.

Récupère ensuite la connection string du projet : Dashboard > Project Settings > Database > Connection string. Si elle n'est disponible nulle part (fichier `.env`, etc.), demande-la à l'utilisateur. Stocke-la dans une variable d'environnement le temps de la session, puis teste la connexion :

```bash
export DATABASE_URL="postgresql://..."
psql "$DATABASE_URL" -c "SELECT 1;"
```

Si la connexion échoue, demande à l'utilisateur de vérifier la connection string avant de continuer.

### Étape 2 : Exécution des requêtes d'audit

Exécute chaque requête SQL via `psql`. Exemple :

```bash
psql "$DATABASE_URL" -c "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
```

### Étape 3 : Analyses à effectuer selon le mode

#### Mode FULL ou RLS
Exécute ces vérifications RLS :

1. **Tables sans RLS activé (CRITICAL)**
```sql
SELECT t.tablename
FROM pg_tables t
JOIN pg_class c ON c.relname = t.tablename
JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.schemaname
WHERE t.schemaname = 'public'
AND c.relkind = 'r'
AND NOT c.relrowsecurity;
```

2. **Tables avec RLS mais sans politiques (HIGH)**
```sql
SELECT c.relname as table_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_policy p ON p.polrelid = c.oid
WHERE n.nspname = 'public'
AND c.relkind = 'r'
AND c.relrowsecurity = true
GROUP BY c.relname
HAVING COUNT(p.polname) = 0;
```

3. **Politiques trop permissives (HIGH)**
```sql
SELECT schemaname, tablename, policyname, permissive, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
AND (qual = 'true' OR with_check = 'true');
```

4. **Politiques utilisant user_metadata (MEDIUM)**
```sql
SELECT schemaname, tablename, policyname, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
AND (qual LIKE '%user_metadata%' OR with_check LIKE '%user_metadata%');
```

5. **Couverture des opérations par table**

`pg_policy.polcmd` prend les valeurs : `'r'` SELECT, `'a'` INSERT, `'w'` UPDATE, `'d'` DELETE, `'*'` ALL. Une politique ALL (`polcmd = '*'`) couvre les 4 opérations, d'où sa prise en compte dans chaque décompte :

```sql
SELECT
    c.relname as table_name,
    COUNT(*) FILTER (WHERE p.polcmd IN ('r', '*')) as select_policies,
    COUNT(*) FILTER (WHERE p.polcmd IN ('a', '*')) as insert_policies,
    COUNT(*) FILTER (WHERE p.polcmd IN ('w', '*')) as update_policies,
    COUNT(*) FILTER (WHERE p.polcmd IN ('d', '*')) as delete_policies
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_policy p ON p.polrelid = c.oid
WHERE n.nspname = 'public' AND c.relkind = 'r'
GROUP BY c.relname
ORDER BY c.relname;
```

#### Mode FULL ou DEFINER
Exécute ces vérifications Security Definer :

1. **Fonctions security definer dans schemas exposés (CRITICAL)**
```sql
SELECT n.nspname as schema, p.proname as function_name,
    pg_get_function_identity_arguments(p.oid) as args
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.prosecdef = true
AND n.nspname IN ('public', 'storage', 'graphql_public');
```

2. **Fonctions security definer sans search_path (CRITICAL)**
```sql
SELECT n.nspname as schema, p.proname as function_name,
    pg_get_function_identity_arguments(p.oid) as args,
    p.proconfig as config
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.prosecdef = true
AND (p.proconfig IS NULL OR NOT EXISTS (
    SELECT 1 FROM unnest(p.proconfig) AS conf WHERE conf LIKE 'search_path=%'
));
```

3. **Vues sans security_invoker (MEDIUM)**
```sql
SELECT c.relname as view_name,
    pg_get_viewdef(c.oid) as definition
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'v'
AND n.nspname = 'public'
AND NOT EXISTS (
    SELECT 1 FROM pg_options_to_table(c.reloptions)
    WHERE option_name = 'security_invoker' AND option_value = 'true'
);
```

#### Mode FULL ou STORAGE
Exécute ces vérifications Storage :

1. **Buckets storage et leur configuration**
```sql
SELECT id, name, public, avif_autodetection, file_size_limit, allowed_mime_types
FROM storage.buckets;
```

2. **Politiques storage** (les politiques Storage sont des politiques RLS sur `storage.objects`)
```sql
SELECT policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'storage' AND tablename = 'objects';
```

#### Mode QUICK
Exécute uniquement les vérifications CRITICAL :
- Tables sans RLS
- Security definer dans schemas exposés
- Security definer sans search_path

### Étape 4 : Calcul du score de sécurité

Score sur 100 points :
- -20 points par problème CRITICAL
- -10 points par problème HIGH
- -5 points par problème MEDIUM
- -2 points par problème LOW
- Minimum : 0

### Étape 5 : Génération du rapport

Crée le fichier `supabase/security-audit-report.md` avec ce format :

```markdown
# Rapport d'audit de sécurité Supabase

**Date :** [Date actuelle]
**Mode :** [Mode d'exécution]

## Synthèse

| Niveau de risque | Nombre |
|------------------|--------|
| CRITICAL         | X      |
| HIGH             | X      |
| MEDIUM           | X      |
| LOW              | X      |

**Score de sécurité :** [Score]/100

## Problèmes critiques

[Pour chaque problème critique, inclure :]
### [Nom du problème]
- **Table/Fonction :** [Nom]
- **Risque :** [Description du risque]
- **Remédiation :**
```sql
[Code SQL de correction]
```

## Problèmes de priorité haute
[Liste des problèmes HIGH]

## Problèmes de priorité moyenne
[Liste des problèmes MEDIUM]

## Problèmes de priorité basse
[Liste des problèmes LOW]

## Analyse des tables

| Table | RLS | SELECT | INSERT | UPDATE | DELETE | Statut |
|-------|-----|--------|--------|--------|--------|--------|
[Tableau avec statut par table]

## Fonctions Security Definer

| Schéma | Fonction | search_path | Statut |
|--------|----------|-------------|--------|
[Tableau avec statut par fonction]

## Buckets Storage

| Bucket | Public | Politiques | Statut |
|--------|--------|------------|--------|
[Tableau avec statut par bucket]

## Recommandations

### Priorité 1 - Critique
[Actions immédiates requises]

### Priorité 2 - Important
[Actions à planifier]

### Priorité 3 - Amélioration
[Bonnes pratiques à implémenter]

---
*Rapport généré par Claude Code - /supabase-audit*
```

### Étape 6 : Option --fix

Si l'option `--fix` est présente, génère un fichier `supabase/security-fixes.sql` contenant tous les scripts de remédiation SQL organisés par priorité.

### Étape 7 : Option --test

Si l'option `--test` est présente, exécute ces tests dans des transactions rollback :

1. Test qu'un utilisateur anon ne peut pas lire les tables protégées
2. Test qu'un utilisateur authentifié ne peut accéder qu'à ses propres données
3. Test que les fonctions security definer fonctionnent correctement

Squelette SQL à utiliser pour chaque test :

```sql
BEGIN;
SET LOCAL ROLE anon;
SET LOCAL request.jwt.claims = '{}';
-- la requête à tester, ex. SELECT * FROM ma_table LIMIT 1;
ROLLBACK;
```

## Règles de sécurité à vérifier

1. **RLS obligatoire** sur toutes les tables dans schemas exposés
2. **Jamais utiliser `user_metadata`** dans les politiques RLS (modifiable par les users)
3. **Toujours `SET search_path`** pour les fonctions security definer
4. **Jamais créer security definer** dans schemas exposés (public, storage, graphql_public)
5. **Index sur colonnes RLS** (auth.uid() = user_id) pour performances
6. **Utiliser `authenticated` explicitement**, pas juste `public`
7. **Vues avec `security_invoker = true`** (Postgres 15+)
8. **Jamais `service_role` côté client** - uniquement côté serveur

## Résumé à afficher

À la fin de l'audit, affiche un résumé concis dans le terminal avec :
- Le score de sécurité
- Le nombre de problèmes par sévérité
- Les 3 actions prioritaires
- Le chemin vers le rapport complet
