Lots 1-3 modernisation JS (S8786/S2703/S2486) + retrait Sonar CI/prod

- Lot 1 (S8786, ReDoS) : 5 sites documentes NOSONAR apres preuve empirique
  (script reproductible docs/redos_probe_s8786.js), aucune reecriture
  defensive necessaire.
- Lot 2 (S2703, variable globale implicite) : bug reel trouve et corrige
  (SCENE_OBJECT_NAMES en const au lieu de let, cassait la reassignation
  cross-script depuis scene-editor.js) + test de non-regression ; 4 autres
  sites confirmes surs et documentes.
- Lot 3 (S2486, exceptions avalees) : 6 sites confirmes surs et
  documentes ; 2 sites (config sprite JSON invalide) corriges avec un
  console.warn devtools, comportement joueur inchange, couverts par un
  nouveau test.
- Retrait du job CI sonarqube (.gitea/workflows/deploy.yml) et du service
  prod sonarqube/sonar-postgres (docker-compose.prod.yml) : acces dashboard
  bloque par des soucis d'infrastructure reseau (WSL2/pare-feu Hyper-V en
  local, reseau Docker partage avec Caddy pas en place en prod), sans lien
  avec le code du moteur - mis de cote plutot que de continuer a bloquer
  sur de l'infra. Les lots 4+ de modernisation JS dependent de scores
  Sonar exacts et sont donc egalement en pause (voir CODE_QUALITY.md).

SKIP=djlint : H021 (styles inline, 49 occurrences) est un backlog deja
documente et assume (CODE_QUALITY.md section 6), sur des templates non
touches par ce commit - deja exclu de la CI pour la meme raison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-18 08:46:24 +02:00
co-authored by Claude Sonnet 5
parent 55e81c0fbb
commit 66d8eaeae8
16 changed files with 666 additions and 112 deletions
+134
View File
@@ -0,0 +1,134 @@
# Plan — lot JS "modernisation" (357 issues SonarQube)
Trace de la stratégie validée avant exécution — même valeur que
`docs/SESSION_RECAP.md` pour la Phase 3 : ce document décrit le plan tel
que discuté et confirmé, pas un compte-rendu de ce qui a déjà été fait.
Pour l'état d'avancement réel lot par lot, voir les commits de ce
chantier (référencés ici au fur et à mesure).
Origine : rapport SonarQube local (`http://localhost:9000`), catégorie
"code smells" JavaScript, 357 occurrences réparties sur 22 règles.
## 1. Répartition exacte par règle Sonar
| Règle | Nom | Occurrences |
|---|---|---|
| `S7773` | Number static methods preferred over globals (`parseFloat`→`Number.parseFloat`, etc.) | 119 |
| `S6582` | Optional chaining should be preferred | 98 |
| `S7781` | `replaceAll()` instead of `replace()` with global regex | 29 |
| `S7761` | Data attributes via `.dataset` | 22 |
| `S7765` | `.includes()` instead of `.indexOf()`/`.lastIndexOf()` | 19 |
| `S3972` | Conditionals should start on new lines | 15 |
| `S3776` | Cognitive Complexity too high | 9 |
| `S4138` | `for-of` should be used with Iterables | 6 |
| `S4624` | Template literals should not be nested | 6 |
| `S2703` | Variables should be declared explicitly (var globale implicite) | 6 |
| `S8786` | Regex non-linear backtracking (ReDoS) | 5 |
| `S3358` | Ternary operators should not be nested | 4 |
| `S7744` | Unnecessary fallback objects in object spread | 3 |
| `S2486` | Exceptions should not be ignored | 3 |
| `S6557` | `startsWith()`/`endsWith()` instead of manual check | 3 |
| `S7721` | Function should be moved to highest possible scope | 2 |
| `S4144` | Functions should not have identical implementations | 2 |
| `S6653` | `Object.hasOwn()` instead of `hasOwnProperty` | 2 |
| `S6671` | Literal used for promise rejection (pas une `Error`) | 1 |
| `S7755` | `.at()` instead of `[…length - index]` | 1 |
| `S7769` | Modern Math APIs (`Math.hypot` etc.) | 1 |
| `S3735` | `void` should not be used | 1 |
| **Total** | | **357** |
## 2. Classification
### Mécanique, sans risque sémantique (équivalence prouvée)
| Règle | Pourquoi c'est sûr |
|---|---|
| `S7773` | `parseFloat`/`parseInt`/`isNaN`/`isFinite` sont des alias exacts de `Number.*` depuis ES2015 |
| `S7781` | `replace()`→`replaceAll()` seulement quand la regex a déjà le flag `g` (précondition de la règle) |
| `S7765` | `.indexOf(x) !== -1` ⇔ `.includes(x)` — équivalence stricte pour ce pattern |
| `S7761` | `getAttribute('data-x')`→`.dataset.x` — mécanique si nom d'attribut statique |
| `S7744` | Le spread de `null`/`undefined` ne lève jamais — le fallback `\|\| {}` est toujours redondant |
| `S6557`, `S6653`, `S7755`, `S7769` | Équivalences directes, très faible volume (7 sites), pas besoin d'outil |
| `S3972` | Pur formatage, zéro impact comportemental |
### ⚠️ Correction par rapport à l'hypothèse de départ : `S6582` n'est PAS mécanique
`a && a.b` et `a?.b` divergent quand `a` est une valeur **fausse mais
définie** (`0`, `""`, `false`, `NaN`) : `a && a.b` s'arrête et renvoie
`a`, `a?.b` continue et accède à `.b`. Une transformation aveugle peut
changer le comportement. Malgré son volume (98, 2ᵉ plus grosse règle),
`S6582` est traité en examen au cas par cas, pas en lot mécanique.
### Nécessite un vrai examen au cas par cas
| Règle | Nature | Note |
|---|---|---|
| `S6582` (98) | Sémantique | Voir ci-dessus — sous-lots avec vérification du type de la valeur testée |
| `S8786` (5) | **Sécurité-adjacent** | Sites JS, distincts de l'exception Python déjà validée en Phase 3 (`auth/email_validation.py`) |
| `S2703` (6) | **Sécurité-adjacent** | 1 seul des 6 sites déjà examiné et validé (`bindings.js:179`, `gameData`) — 5 restent à vérifier, ne pas supposer qu'ils sont identiques |
| `S2486` (3) | **Sécurité-adjacent** | Exception avalée, peut masquer un vrai bug |
| `S3776` (9) | Refactor réel | Pas de fix mécanique — restructuration fonction par fonction |
| `S4624` (6), `S3358` (4) | Lisibilité | Extraction manuelle |
| `S4138` (6) | Risque de perte de l'index | Fix proposé par un outil mais vérifié site par site avant application |
| `S4144` (2), `S7721` (2) | Décision de conception | Dédupliquer/déplacer une fonction change la portée |
| `S6671` (1) | Change le type de la valeur de rejet | Impact possible sur du code aval qui inspecterait `.message`/`instanceof Error` |
| `S3735` (1) | Dépend de l'intention | À lire avant de trancher |
## 3. Outillage — vérifié empiriquement
- **`eslint-plugin-sonarjs`** (officiel SonarSource, 4.2.1, dernière version) : ne couvre qu'**une seule** des 22 règles avec un fixer (`S3972`) — les règles `S77xx` sont trop récentes pour cette version.
- **`eslint-plugin-unicorn`** : dernière version (75.0.0) exige ESLint ≥10 (incompatible avec notre 8.57.1). Version retenue : **55.0.0** (exige ESLint ≥8.56.0, compatible), ajoutée en devDependency permanente. 9 règles activées dans `.eslintrc.json` avec un fixer confirmé (`fixable: 'code'` vérifié dans le code source du plugin) :
- `unicorn/prefer-number-properties` (S7773)
- `unicorn/prefer-string-replace-all` (S7781)
- `unicorn/prefer-dom-node-dataset` (S7761)
- `unicorn/prefer-includes` (S7765)
- `unicorn/prefer-string-starts-ends-with` (S6557)
- `unicorn/prefer-modern-math-apis` (S7769)
- `unicorn/prefer-at` (S7755)
- `unicorn/no-useless-fallback-in-spread` (S7744)
- `unicorn/no-for-loop` (S4138)
- Uniquement ces 9 règles activées, jamais la config `recommended` complète du plugin (qui en contient des dizaines d'autres, jamais évaluées pour ce projet) — voir `CODE_QUALITY.md` section 2.
- **Dry-run réel** (`--fix-dry-run`) sur tout `static/js/**/*.js` avec ces 9 règles : **222 problèmes détectés, 222 avec un fix proposé.** Les comptes par règle ne correspondent pas exactement à ceux de Sonar (moteurs différents, ex. `prefer-includes` détecte 41 cas contre 19 pour `S7765` côté Sonar) — normal, la vérification finale se fait via un rescan Sonar après application, pas via le compte ESLint.
- **`@typescript-eslint`** (candidat pour combler `S6582`) : nécessite de remplacer tout le parser JS par le parser TypeScript — écarté, trop lourd pour un projet 100% JS.
- **`Object.hasOwn` (`S6653`, 2 sites) et `startsWith`/`endsWith` (`S6557`, 3 sites)** : aucun outil ne les a détectés dans le dry-run — 5 sites au total, correction manuelle directe.
## 4. Ordre de priorité
1. Sécurité-adjacent : `S8786` (5), `S2703` (6, dont 5 non-vérifiés), `S2486` (3) — 14 sites.
2. Structure/lisibilité à risque de bug : `S3776` (9), `S4624` (6), `S3358` (4), `S4144` (2), `S7721` (2), `S6671` (1), `S3735` (1) — 25 sites.
3. `S6582` optional chaining (98) — lot dédié à part.
4. Modernisation mécanique via outil (`S7773`, `S7781`, `S7765`, `S7761`, `S7744`, `S3972`, `S4138` vérifié) — le plus gros volume, le moins risqué, en dernier.
5. Reliquat manuel très faible volume (`S6557`, `S6653`, `S7755`, `S7769`) — 7 sites.
## 5. Découpage en 9 lots
| Lot | Contenu | Méthode |
|---|---|---|
| 1 | `S8786` (5) | Lecture individuelle, diff par site |
| 2 | `S2703` (5 restants, hors `gameData` déjà validé) | Lecture individuelle — même pattern ou différent ? |
| 3 | `S2486` (3) | Lecture individuelle |
| 4 | `S3776` (9) | Refactor fonction par fonction |
| 5 | `S4624` + `S3358` + `S4144` + `S7721` + `S6671` + `S3735` (16) | Groupés, petits volumes, revue individuelle |
| 6 | `S6582` (98) | Sous-lots de ~20-25, vérification du type à chaque site |
| 7 | `S7773`+`S7781`+`S7765`+`S7761`+`S7744`+`S3972` via `eslint-plugin-unicorn`@55.0.0 `--fix` | `--fix` en un coup, diff complet relu avant commit |
| 8 | `S4138` (fix proposé par `no-for-loop`, vérifié un par un) | Semi-automatique |
| 9 | `S6557`+`S6653`+`S7755`+`S7769` (7) | Manuel direct |
**À chaque lot** : diff complet montré avant application, suite de tests
JS (`node --test static/js/play/__tests__/*.test.js
static/js/play/offline/__tests__/*.test.js
static/js/scenes/__tests__/*.test.js`, 241 tests) après application,
signalement immédiat de tout changement de comportement réel (pas
seulement du style). Rescan SonarQube local après le lot 7 (le plus gros
volume mécanique) pour confirmer la réduction réelle plutôt que de se
fier au seul compte ESLint, puis un rescan final après le lot 9.
## Note de séquencement Git
Les 9 règles `unicorn/*` ont été activées dans `.eslintrc.json` (et
`eslint-plugin-unicorn` ajouté en devDependency) **avant** que les 222
occurrences correspondantes soient corrigées — ce qui rend `pre-commit`/
la CI rouges sur ESLint dès maintenant si ces changements de config
étaient commités seuls. Ces changements restent donc **non commités**
tant que le lot 7 (la correction mécanique) n'est pas fait, pour ne
jamais introduire un commit où le lint échouerait sur `dev`.
+242
View File
@@ -0,0 +1,242 @@
# Compte-rendu — mise en place qualité de code (2026-09-15)
Compte-rendu chronologique de la session ayant mis en place le dispositif
qualité de code du moteur (Phases 1 à 4 du plan). Documente ce qui a été
fait, trouvé et décidé — pour savoir comment utiliser les outils au
quotidien, voir `CODE_QUALITY.md` (à venir, Phase 5).
Commits de référence : `c57420c8` (Phase 3), `2ff127f6` et `20f9398b`
(Phase 4).
## 1. Contexte et objectif
Le moteur disposait déjà d'une suite de tests (591 tests Python, 241
tests JS) mais d'aucun outillage de qualité de code, d'architecture ou de
détection de code mort — rien n'empêchait un import circulaire, une
fonction non typée, une variable inutilisée ou un champ de formulaire
inaccessible d'atteindre la production. L'objectif de cette session :
mettre en place un dispositif **strictement strict** (aucune règle
désactivée "pour ne pas casser le build" — l'existant est corrigé pour
satisfaire la config, jamais l'inverse), puis nettoyer l'existant pour le
faire passer au vert, en 4 phases :
1. Configuration des outils.
2. Audit de l'existant (rapport initial).
3. Corrections par lots (typage, sécurité, architecture, a11y, ESLint/
Stylelint).
4. Intégration continue (CI) — pour que ces vérifications protègent
aussi un push direct ou une PR, pas seulement la machine de qui
committe.
## 2. Outils mis en place (Phase 1)
| Outil | Rôle |
|---|---|
| **Ruff** | Lint + formatage Python (remplace flake8/isort/black en un seul outil) |
| **Mypy** (`--strict`) | Typage statique Python — détecte les incohérences de type avant l'exécution |
| **Vulture** | Détection de code mort Python (fonctions/imports jamais utilisés) |
| **Bandit** | Analyse de sécurité Python (injections, primitives cryptographiques faibles, etc.) |
| **import-linter** | Fait respecter les couches applicatives (ex. `db/` ne doit dépendre d'aucun autre paquet du moteur) |
| **ESLint** (`airbnb-base`) | Lint JavaScript |
| **Stylelint** (`stylelint-config-standard`) | Lint CSS |
| **djLint** | Lint des templates Jinja |
| **SonarQube Community Build** | Analyse qualité/sécurité agrégée, auto-hébergée (Docker + PostgreSQL) sur `sonar.forgebase.fr` |
| **pre-commit** | Orchestre tous les hooks ci-dessus localement, bloquant, avant chaque commit |
## 3. Bilan chiffré avant/après (Phase 2-3)
État final, vérifié directement à plusieurs reprises au cours de la
session (dernière vérification avant le commit `c57420c8`) :
| Outil | Résultat final |
|---|---|
| Ruff (lint + format) | 0 erreur |
| Mypy `--strict` | 0 erreur sur 388 fichiers source |
| Vulture | 0 signalement |
| Bandit | 0 issue (23 suppressions `# nosec` documentées individuellement) |
| import-linter | 1 contrat ("Couches applicatives Forge Engine"), respecté |
| ESLint | 0 erreur (12 avertissements pré-existants `no-alert`, jugés acceptables) |
| Stylelint | 0 erreur |
| djLint | 49 erreurs H021 (styles inline) — **backlog assumé**, voir §8 |
**Note de transparence** : cette session n'a pas produit de rapport
d'audit initial formalisé et conservé (`CODE_QUALITY.md`/Phase 5 n'existe
pas encore) — un tableau détaillé "avant/après par paquet" avec des
comptes précis par outil n'est donc pas reconstituable après coup avec
certitude. Ce qui a été retenu au fil de la session :
- Le typage Mypy `--strict` a été déployé **paquet par paquet**, dans
l'ordre : `db` → `screens` → `auth` → `core` → `ai` → `routes` → puis
`publish`/`scripts`/`tests`/`app.py`/`build_css.py` — chaque paquet
validé à 0 erreur avant de passer au suivant. `routes/` était le plus
gros lot (366 signalements Mypy à lui seul avant correction).
`screens/` était le paquet le plus étendu en nombre de fichiers touchés
(95 fichiers dans le commit final).
- Ruff signalait de l'ordre de 800+ erreurs cumulées (F401/F811,
formatage, imports) avant nettoyage, toutes résolues.
- Un régression Vulture a été détectée et corrigée en cours de route (un
changement de forme d'import — groupé vs individuel — modifiait la
détection d'usage), traitée via `vulture_whitelist.py`.
Le rapport SonarQube (voir §8), lui, a été entièrement conservé et trié
point par point : **72 bugs**, **57 vulnérabilités**, **466 code
smells**, **0 security hotspot**, Quality Gate **OK**.
## 4. Vrais bugs trouvés et corrigés
Bugs de comportement réel (pas du style), chacun vérifié dans le diff du
commit `c57420c8` :
- **`db/definitions/create_definition.py` / `delete_definition.py`** —
un `relation_definition_id`/`definition_id` invalide faisait planter
la fonction avec un `TypeError` cru (indexation d'un `None` renvoyé par
`get_definition`). Remplacé par un `ValueError` explicite et lisible.
- **`db/connection.py` → `core/flask_app.py`** — inversion de dépendance
architecturale : `db/` (couche la plus basse) importait
`core.flask_app` en interne (`_install_teardown_safety_net`), en
violation du contrat import-linter. Extrait en
`install_teardown_safety_net(app)`, une fonction pure prenant l'app en
paramètre ; le câblage réel (l'import de `core.flask_app`) déplacé dans
un nouveau fichier `core/db_teardown_guard.py` (couche de câblage, qui
a le droit de dépendre des deux côtés).
- **`ai/chat.py::_describe_scene_state`** — si l'écran associé à une
conversation IA avait été supprimé entre-temps, la fonction plantait
avec un `TypeError` (indexation d'un `screen` à `None`). Ajout d'une
exception dédiée `ScreenDeletedError`, interceptée par
`run_chat_turn` pour renvoyer un message clair ("Cet écran a été
supprimé...") plutôt qu'un crash.
- **`routes/scenes/scene_object_geometry.py`** — condition de course :
l'objet de scène était relu (`get_scene_object`) **après** avoir
appliqué la mise à jour de géométrie, uniquement pour lire
`obj["scene_id"]` — si l'objet avait été supprimé entre-temps (onglet
concurrent), `obj` valait `None` et l'accès plantait. La vérification
d'existence est maintenant faite **avant** toute mise à jour, avec un
retour 404 propre si l'objet n'existe plus.
- **Fuite `</script>` dans le JSON embarqué (XSS potentiel)** — 25 sites
Python (`routes/scenes/scene_edit_view.py` ×22,
`routes/play/game_play.py` ×1, `publish/build_scorm_package.py` ×2)
utilisaient `json.dumps(...)` brut pour embarquer des données dans un
bloc `<script>` — un champ texte utilisateur (nom d'écran/objet/
variable, texte de dialogue...) contenant littéralement `</script>`
aurait refermé la balise prématurément et injecté du HTML/JS non
échappé sur la page. Nouveau helper partagé `db.json_for_script()`
(`json.dumps(data).replace("</", "<\\/")`), appliqué aux 25 sites ;
vérifié par un test dédié (rendu strictement identique quand aucun
`</` n'est présent, neutralisation confirmée sinon).
- **Fuite de handle fichier Windows (export SCORM)** —
`routes/publish/export_scorm.py` servait le zip exporté via
`send_file()` en mode passthrough, puis tentait de le supprimer
(`after_this_request`) pendant que Werkzeug tenait encore le fichier
ouvert — l'erreur (`PermissionError`) était silencieusement avalée par
un `contextlib.suppress(OSError)`, laissant le fichier temporaire sur
le disque à chaque export. Corrigé en bufferisant le zip en mémoire
(`io.BytesIO`) et en supprimant le fichier temporaire avant de
construire la réponse. Vérifié par un script de détection de fuite
(avant : un fichier de 7 Mo restait sur le disque après chaque export ;
après : aucune fuite).
- **Bug d'isolation de tests (compte admin partagé)** — la suite de
tests partage un seul compte admin sur toute une session pytest ; 22
sites (répartis dans `test_ai_tools.py`, `test_user_assets.py`,
`test_scene_object_add_asset.py`) créaient un "asset" sur ce compte
sans le nettoyer, polluant les tests suivants. Corrigé ponctuellement
aux 22 sites, **puis** doublé d'une fixture `autouse=True` dans
`tests/conftest.py` (`_cleanup_admin_assets`) qui nettoie
automatiquement tout asset créé pendant un test — filet de sécurité
pour un futur test qui oublierait le même nettoyage manuel.
## 5. Code mort supprimé
Chaque suppression vérifiée par une recherche exhaustive de toute
référence restante dans le code (pas seulement le signalement Vulture) :
- `db/definitions/add_field_to_definition.py`, `delete_field.py`,
`rename_definition.py`, `update_field.py`
- `db/rows/relation_options.py`, `rows_referencing.py`
- `screens/clause_list_codec.py`
- `screens/rendering/trigger_for.py`
Pour chacun : recherche du nom de la fonction/fichier dans tout le
dépôt (`grep` récursif) après suppression — zéro référence restante dans
le code exécutable (au pire une mention dans un commentaire de
documentation, sans impact fonctionnel).
## 6. Décisions de configuration vs changements de code
Cas où la configuration a été adaptée aux conventions réelles et déjà
établies du projet plutôt que de réécrire massivement le code existant :
| Règle désactivée/ajustée | Fichier | Raison |
|---|---|---|
| `selector-class-pattern`/`selector-id-pattern: null` | `.stylelintrc.json` | Le projet utilise du camelCase pour ses classes/id CSS (`.canvasElement`, `#scormProgress`...) depuis le début — `stylelint-config-standard` impose du kebab-case par défaut ; renommer aurait touché des milliers de sélecteurs et leurs usages JS/HTML pour un gain de lisibilité nul |
| `no-descending-specificity: null` | `.stylelintrc.json` | Le CSS existant est organisé par composant/fonctionnalité, pas par ordre strict de spécificité — se conformer aurait demandé de réordonner une grande partie du fichier |
| `no-param-reassign: "off"` | `.eslintrc.json` | Convention déjà répandue dans le code (réassignation de paramètres pour la commodité, ex. petites moulinettes sur `event`/élément) — vérifiée site par site avant de désactiver la règle plutôt que de tout réécrire |
| `no-use-before-define` limité aux variables/classes (`functions: false`) | `.eslintrc.json` | Pattern de callback "classique" très répandu : des gestionnaires `onclick="foo()"` posés dans le HTML référencent des fonctions déclarées plus bas dans le fichier JS — le hoisting des déclarations de fonction rend ça sûr, contrairement aux variables |
| `func-names`, `no-underscore-dangle`, `no-console`, `no-plusplus`, `no-continue`, `no-bitwise`, `guard-for-in`, `no-restricted-syntax`, `prefer-destructuring`, `consistent-return`, `no-return-assign`, `no-nested-ternary`, `no-void`, `implicit-arrow-linebreak`, `no-unused-expressions`, `no-useless-concat` : `"off"` | `.eslintrc.json` | Chacune vérifiée individuellement comme sûre pour ce code avant désactivation (style déjà cohérent dans le projet, ex. underscore-prefixed pour le "privé" par convention) |
| `max-len` porté à 120 | `.eslintrc.json` | Convention déjà en usage dans le code Python (Ruff `line-length = 120`) — alignée côté JS plutôt que forcer un retour à 80 sur tout le dépôt |
| `unused-imports/no-unused-vars` avec liste blanche de ~95 noms | `.eslintrc.json` | Fonctions invoquées uniquement depuis des attributs `onclick`/`onchange` inline dans les templates Jinja — invisibles pour l'analyse statique d'ESLint (qui ne lit pas le HTML), mais réellement utilisées |
## 7. Intégration CI (Phase 4)
`.gitea/workflows/deploy.yml` — déclenché sur chaque push (`main` et
`dev`) :
- **`lint-python`** (bloquant) — rejoue `ruff check`/`ruff format
--check`/`mypy .`/`vulture`/`bandit`/`lint-imports` dans une image
Docker jetable (mêmes commandes que `.pre-commit-config.yaml`, jamais
dupliquées avec des paramètres différents). `djlint` volontairement
exclu (backlog H021, voir §8).
- **`lint-js`** (bloquant) — `npm run lint:js` / `npm run lint:css` dans
une image `node:20-slim`.
- **`sonarqube`** (`continue-on-error: true`, non-bloquant) — scan
contre l'instance self-hébergée `sonar.forgebase.fr`. N'est **pas**
dans le `needs` de `build-and-push` : un échec ici n'affecte jamais le
reste du pipeline.
- **`build-and-push`** dépend désormais de `test-python`, `test-js`,
`lint-python` **et** `lint-js`.
- **`deploy`** sécurisé : le token du registre passe par
`--password-stdin` (jamais en argument `-p`, visible via `ps` sur le
serveur) ; la clé SSH privée est supprimée en fin de job
(`if: always()`, `rm -f` — ne peut jamais échouer même si le fichier
n'a jamais été créé).
Premier run réel après le push de `2ff127f6` (run CI Gitea #96, commit
`2ff127f6`) : `test-python` ✅, `test-js` ✅, `lint-python` ✅, `lint-js`
✅, `sonarqube` ❌ (attendu — infra pas encore prête, voir §8),
conclusion globale du run : **succès** (confirmant que le mode
non-bloquant de `sonarqube` fonctionne bien tel que configuré).
## 8. Points en suspens / backlog
- **djLint H021 (styles inline, 49 occurrences sur 6 templates)** —
backlogué explicitement, jamais désactivé silencieusement (documenté
dans chaque commit qui l'a sauté via `SKIP=djlint`). **Priorité
immédiate après cette session**, voir §9.
- **SonarQube en CI** — échoue actuellement (`Failed to query server
version`) : l'endpoint `sonar.forgebase.fr` (Caddy + Let's Encrypt)
n'est pas encore pleinement opérationnel côté serveur perso. Aucune
date communiquée pour la mise en service — à vérifier avec l'infra
avant de s'attendre à un scan CI qui aboutit.
- **Rapport SonarQube — 466 code smells** — analysés en volume mais non
triés individuellement (contrairement aux 72 bugs et 57 vulnérabilités,
entièrement passés en revue point par point cette session). Lot séparé
à traiter après la Phase 4.
- **Trajectoire vers un SonarQube bloquant en CI** — une fois (a) l'infra
serveur opérationnelle et (b) les code smells triés (faux positifs
documentés en `NOSONAR`, comme déjà fait pour les 72 bugs/57
vulnérabilités), retirer `continue-on-error: true` du job `sonarqube`
et l'ajouter au `needs` de `build-and-push`.
- **Phase 5 (CODE_QUALITY.md)** — non commencée : documentation de
référence sur les outils et comment les utiliser au quotidien (ce
fichier-ci n'en tient pas lieu, voir l'introduction).
## 9. Prochaines étapes
**Priorité immédiate** (juste après cette session, avant ou après la
Phase 4 selon décision à prendre le moment venu) : supprimer le style
inline (djLint H021) au profit d'un système de modèles/classes CSS
réutilisables, plutôt que de continuer à accumuler des exceptions sur
cette règle.
Ensuite, dans l'ordre discuté : trier les 466 code smells SonarQube,
activer SonarQube en CI dès que l'infra serveur est prête, puis rédiger
`CODE_QUALITY.md` (Phase 5).
+57
View File
@@ -0,0 +1,57 @@
// Script de verification empirique pour les 5 sites NOSONAR javascript:S8786
// (voir CODE_QUALITY.md, section 5) - teste les 3 regex distinctes flaguees
// (2 sont dupliquees dans 2 fichiers chacune) contre des entrees construites
// pour maximiser l'ambiguite que l'analyseur Sonar soupconne, a plusieurs
// tailles croissantes. Un vrai ReDoS montrerait une croissance exponentielle
// du temps avec la taille (x10 la taille -> beaucoup plus que x10 le temps).
//
// A relancer si l'une de ces 3 regex change de forme, pour confirmer que le
// NOSONAR reste justifie plutot que de le recopier aveuglement.
//
// Usage : node docs/redos_probe_s8786.js
function timeIt(label, fn) {
const start = process.hrtime.bigint();
const result = fn();
const end = process.hrtime.bigint();
const ms = Number(end - start) / 1e6;
console.log(`${label}: ${ms.toFixed(2)}ms -> match=${result !== null}`);
return ms;
}
console.log('=== Pattern A (filter-repeater-rows.js:10, panel-init.js:254) ===');
const patternA = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/;
for (const n of [1000, 10000, 50000, 100000]) {
const evil = `{{${'a'.repeat(n)}`; // pas de point, pas de fermeture
timeIt(` n=${n} (pas de point, pas de fermeture)`, () => patternA.exec(evil));
}
for (const n of [1000, 10000, 50000, 100000]) {
const evil = `{{${'a'.repeat(n)}.${'b'.repeat(n)}`; // point present, pas de fermeture
timeIt(` n=${n} (point present, pas de fermeture)`, () => patternA.exec(evil));
}
console.log('');
console.log('=== Pattern B (filter-repeater-rows.js:11, panel-init.js:261) ===');
const patternB = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/;
for (const n of [1000, 5000, 10000, 20000]) {
const evil = `{{$a${'.b'.repeat(n)}`; // repetition simple, pas de fermeture
timeIt(` n=${n} (${n} segments ".b", pas de fermeture)`, () => patternB.exec(evil));
}
for (const n of [1000, 5000, 10000, 20000]) {
const evil = `{{$a${'.b[0]'.repeat(n)}`; // alternance des deux branches
timeIt(` n=${n} (${n} segments ".b[0]" alternes, pas de fermeture)`, () => patternB.exec(evil));
}
console.log('');
console.log('=== Pattern C (xapi-client.js:132) ===');
const patternC = /\/+$/;
for (const n of [10000, 100000, 1000000]) {
const evil = '/'.repeat(n);
timeIt(` n=${n} (que des slashes)`, () => patternC.exec(evil));
}
console.log('');
console.log('=== Verification d\'equivalence (entrees valides normales, comportement inchange attendu) ===');
console.log('A sur "{{Objet.champ}}":', patternA.exec('{{Objet.champ}}'));
console.log('B sur "{{$var.champ[0].sous}}":', patternB.exec('{{$var.champ[0].sous}}'));
console.log('C sur "https://host///":', 'https://host///'.replace(patternC, ''));