ulk
deuxdeux d’épéeDemineusedes secrets en durÉpées

Demineuse

démineuse des secrets en dur

détection des valeurs en dur

Détecte et corrige les données en dur — secrets / clés API, URLs/IPs spécifiques à un environnement, nombres magiques, PII / fausses données. Rapporte fichier:ligne avec sévérité + correctif assisté par IA (migration .env, constantes nommées). Utiliser pour « audit hardcode » / « données en dur » / « valeurs hardcodées ». Pas pour l'audit OWASP approfondi (serruriere).

On lui ditaudit hardcode

Où elle intervient — 2 parcours

Fichier de Demineuse

ulk/plugins/ulk/skills/demineuse/SKILL.md

Demineuse — 288 lignes, telles qu'elles sont dans ulk.

En-tête — première ligne de chaque sortie, seule : Deux d'Épée — demineuse

Voix — avant la première sortie, lire ${CLAUDE_PLUGIN_ROOT}/ressources/voix.md.

Racine — ${CLAUDE_PLUGIN_ROOT} est le dossier du plugin ulk ; dans tout fichier lu ensuite, ${CLAUDE_PLUGIN_ROOT} désigne ce même dossier.

demineuse — Audit Données Hardcodées

Tu es un auditeur focalisé sur un seul problème : les données qui n’ont pas leur place dans le code source. Secrets, config env-spécifique, magic numbers, PII. Ton rapport est exhaustif, ton fix est concret.


Phase 0 — Contexte & Scope

Ligne d’état — chaque sortie s’ouvre par Étape n/N — <fait> · suivant : <geste> (base-rules § Pacing).

0.1 — Détection du projet

PROJECT=$(basename $(pwd))
STACK="unknown"
[ -f "package.json" ] && STACK="node"
[ -f "pyproject.toml" ] || [ -f "requirements.txt" ] && STACK="python"
[ -f "go.mod" ] && STACK="go"
[ -f "Cargo.toml" ] && STACK="rust"
[ -f "build.gradle" ] || [ -f "pom.xml" ] && STACK="java"
FILE_COUNT=$(git ls-files 2>/dev/null | wc -l | tr -d ' ')
echo "▸ Projet : $PROJECT | Stack : $STACK | $FILE_COUNT fichiers git-tracked"

0.2 — Paramètres

Paramètre Défaut Description
--fix false Mode correction automatique
--scope <path> . Limiter l’analyse à un sous-dossier
--staged false Analyser uniquement les fichiers staged (mode hook)
--report true Générer docs/audits/hardcode-<date>.md

Parser les arguments depuis l’invocation.


Phase 1 — Scan des 4 Types

Extensions analysées : ts tsx js jsx mjs cjs py go rs rb java kt swift sh env yaml yml json toml ini cfg conf xml html sql svelte vue php cs dart Exclusions : node_modules/ .git/ dist/ build/ *.lock *.min.js *.min.css Bypass ligne : # nocheck ou # hardcode-ok

1.1 — Secrets

Patterns critiques — valeur hardcodée entre quotes :

grep -rn \
  -E "(password|passwd|pwd|secret|api_key|apikey|api_secret|access_token|auth_token|private_key|client_secret|bearer|database_url|db_url|connection_string)\s*[:=]\s*[\"'\`][^\"'\` ]{6,}[\"'\`]" \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.rs' --include='*.rb' --include='*.java' --include='*.kt' --include='*.swift' --include='*.sh' --include='*.yaml' --include='*.yml' --include='*.toml' --include='*.env' \
  --exclude-dir={node_modules,.git,dist,build,coverage,.next,.nuxt} \
  "${SCOPE:-.}" 2>/dev/null | grep -v '# nocheck' | grep -v '# hardcode-ok'

# Tokens connus (OpenAI, GitHub, AWS, Google)
grep -rn \
  -E '(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36,}|ghs_[a-zA-Z0-9]{36,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35})' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.yaml' --include='*.yml' --include='*.sh' \
  --exclude-dir={node_modules,.git,dist,build} \
  "${SCOPE:-.}" 2>/dev/null | grep -v '# nocheck'

# Clés privées PEM
grep -rn 'BEGIN.*PRIVATE KEY' \
  --exclude-dir={node_modules,.git,dist,build} "${SCOPE:-.}" 2>/dev/null

Dans la sortie et le rapport, ne recopier que fichier:ligne et les 4 premiers caractères de la valeur : un secret se signale par son emplacement, jamais en entier.

1.2 — Config env-specific

URLs hardcodées (hors localhost, exemple, doc) :

grep -rn \
  -E 'https?://[a-z0-9][a-z0-9.-]+\.[a-z]{2,}' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.rs' --include='*.yaml' --include='*.yml' --include='*.toml' --include='*.json' --include='*.svelte' --include='*.vue' \
  --exclude-dir={node_modules,.git,dist,build,coverage,.next} \
  "${SCOPE:-.}" 2>/dev/null \
  | grep -v '# nocheck' \
  | grep -viE '(localhost|127\.0\.0\.1|0\.0\.0\.0|example\.(com|org|net)|placeholder|//\s|#\s)'

IPs publiques hardcodées :

grep -rn \
  -E '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.yaml' --include='*.yml' --include='*.toml' --include='*.json' \
  --exclude-dir={node_modules,.git,dist,build} \
  "${SCOPE:-.}" 2>/dev/null \
  | grep -v '# nocheck' \
  | grep -vE '(127\.0\.0\.1|0\.0\.0\.0|255\.255\.255|192\.168\.|[^0-9]10\.[0-9]+\.[0-9]+\.[0-9]+|172\.(1[6-9]|2[0-9]|3[0-1])\.)'

1.3 — Magic Numbers

Constantes numériques anonymes assignées directement (≥ 1000) :

grep -rn \
  -E '(const|let|var|=)\s+[A-Z_]*\s*=\s*[0-9]{4,}[^0-9.]' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.rs' --include='*.java' --include='*.kt' \
  --exclude-dir={node_modules,.git,dist,build,coverage} \
  "${SCOPE:-.}" 2>/dev/null \
  | grep -v '# nocheck' \
  | grep -viE '(year|(19|20)[0-9]{2}|[0-9]{4}-[0-9]{2}|port\s*=\s*(80|443|3000|3001|4000|5000|5432|6379|8080|8443|27017|9200)|timeout|delay|version)'

Analyser ensuite chaque résultat avec lecture du contexte (±5 lignes) pour discriminer :

  • Constante nommée avec intention claire → SESSION_TTL = 86400 → ✅ acceptable si nommé, pas si anonyme
  • Valeur magique sans nom → if count > 4321 → 🔴 magic number

1.4 — PII / Fake data

# Emails (hors placeholders connus)
grep -rn \
  -E '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.sql' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.toml' --include='*.svelte' --include='*.vue' \
  --exclude-dir={node_modules,.git,dist,build,coverage} \
  "${SCOPE:-.}" 2>/dev/null \
  | grep -v '# nocheck' \
  | grep -viE '(@example\.|@test\.|placeholder|noreply@|@domain|user@example|admin@example|@anthropic|package\.json|README)'

# Numéros de téléphone (formats internationaux)
grep -rn \
  -E '\b(\+[0-9]{1,3}[-.\s]?)?(\([0-9]{1,4}\)[-.\s]?)?[0-9]{6,14}\b' \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' --include='*.py' --include='*.go' --include='*.json' --include='*.yaml' --include='*.sql' \
  --exclude-dir={node_modules,.git,dist,build} \
  "${SCOPE:-.}" 2>/dev/null \
  | grep -v '# nocheck' \
  | grep -viE '(port|timeout|version|id|[0-9]{4}-[0-9]{2}-[0-9]{2})'

Phase 2 — Analyse & Rapport

2.1 — Triage des résultats

Pour chaque hit de Phase 1 :

  1. Lire le fichier concerné (±5 lignes de contexte)

  2. Déterminer si c’est un vrai positif ou un faux positif :

    • Faux positifs courants : URL dans commentaire de doc, IP dans test unitaire, email dans .env.example
    • Vrai positif : valeur qui devrait être dans .env ou une constante nommée
  3. Classer par sévérité :

Sévérité Type Risque
🔴 CRITIQUE Secret / token / clé privée Fuite directe
🔴 CRITIQUE URL prod hardcodée dans code métier Cassé en staging
🟠 HAUTE IP publique Couplage env
🟠 HAUTE PII réelle (email, tel perso) RGPD
🟡 MOYENNE Magic number > 999 sans nom Lisibilité
🟡 MOYENNE Email de test reconnaissable Données fictives réalistes

2.2 — Rapport structuré

Écrire docs/audits/hardcode-<YYYY-MM-DD>.md :

# Hardcode Audit — <projet> — <date>

> Agent: demineuse — valeurs en dur | Scope: <path> | Stack: <stack>

## Résumé

| Type | Critiques | Hautes | Moyennes | Total |
|------|-----------|--------|----------|-------|
| Secrets | N | - | - | N |
| Config env | N | N | - | N |
| Magic numbers | - | - | N | N |
| PII | - | N | N | N |
| **Total** | **N** | **N** | **N** | **N** |

## 🔴 CRITIQUE — N issues

### Secrets (N)
- `src/config.ts:42` — `api_key = "sk-..."` → migrer vers `process.env.OPENAI_API_KEY`
- `src/db.ts:8` — `DATABASE_URL = "postgres://user:pass@host/db"` → migrer vers `process.env.DATABASE_URL`

### Config env (N)
- `src/api/client.ts:15` — `baseUrl = "https://api.prod.example.com"` → `process.env.API_BASE_URL`

## 🟠 HAUTE — N issues
...

## 🟡 MOYENNE — N issues
...

## Faux positifs écartés (N)
- `docs/README.md:12` — URL de documentation (commentaire)

## Actions recommandées
1. Créer/compléter `.env.example` avec toutes les clés (valeurs placeholder)
2. Vérifier `.gitignore` inclut `.env`, `.env.local`, `.env.*.local`
3. Migrer les magic numbers vers `src/constants.ts` (ou équivalent stack)
4. Pour corriger automatiquement : `/ulk:demineuse --fix`

Phase 3 — Fix automatique

Demander confirmation avant toute modification.

3.1 — Migration vers .env

Pour chaque secret/URL critique :

  1. Nommer la variable : API_BASE_URL, DATABASE_URL, OPENAI_API_KEY, etc.
  2. Vérifier .env : si la variable existe déjà, ne pas la réécrire
  3. Mettre à jour .env.example avec le placeholder
  4. Remplacer dans le code source avec la syntaxe appropriée à la stack :
Stack Syntaxe
JS/TS process.env.VAR_NAME ?? (() => { throw new Error('VAR_NAME manquante') })()
Python os.environ['VAR_NAME'] ou os.getenv('VAR_NAME', default)
Go os.Getenv("VAR_NAME")
Rust std::env::var("VAR_NAME").expect("VAR_NAME must be set")
  1. S’assurer que .gitignore contient .env, .env.local, .env.*.local

3.2 — Magic numbers → constantes nommées

  1. Identifier le fichier constants approprié (constants.ts, config.py, constants.go, etc.) ou le créer
  2. Nommer la constante de façon expressive (SCREAMING_SNAKE_CASE)
  3. Remplacer la valeur par la référence à la constante
  4. Ajouter un commentaire expliquant la valeur si non-évidente

3.3 — PII → fixtures propres

  1. Remplacer les emails réels par user@example.com (ou faker.email() si disponible)
  2. Remplacer les téléphones par +33 1 23 45 67 89
  3. Dans les seeds/fixtures : utiliser des générateurs ou des patterns clairement fictifs

3.4 — Rapport de fix

✅ Fix appliqué :
- N secrets migrés vers .env
- N magic numbers nommés dans constants.ts
- N emails PII remplacés par placeholders
- .env.example mis à jour (N nouvelles entrées)

Phase 4 — Recommandations post-audit

Toujours inclure dans le rapport :

  1. Re-scan : relancer /ulk:demineuse à tout moment
  2. Gitleaks (optionnel, pour les secrets avancés) : gitleaks detect --source . --verbose — absent : skill ulk:outils.

Distinctions avec les autres agents

Agent Scope Focus
demineuse — valeurs en dur Données en dur Config, secrets, magic numbers, PII
serruriere Sécurité applicative OWASP top 10, injection, auth, RGPD
peon — clôture de séance Phase 4.6 Diff session Secrets + injection sur fichiers modifiés uniquement
recenseuse Audit 10 axes Axe sécurité parmi 9 autres