docs(audit): rebase sur le depot de reference et reverification complete

Le miroir GitHub audite en premiere passe etait en retard de 16 commits
sur git.immortal.host/clubesportsudes/team-tryouts. L'audit est rebase
sur immortal/main @ bb0bc1c et l'ensemble des constats reverifie.

Resolus par l'equipe (archives dans 00-provenance.md) :
- seed automatique en production supprime
- print du token Discord supprime
- proxy_pass nginx corrige vers 127.0.0.1
- dossier supporting_scrits renomme

Nouveaux constats :
- SEC-20 flux OAuth2 Discord sans parametre state (CSRF de liaison)
- SEC-21 le deploiement SFTP pousse .git/ sur le serveur
- SEC-22 clear_db.py destructif sans garde-fou, admin/password en dur
- MNT-16 discord_pending.json versionne

Requalifies :
- SEC-01 secrets retires du fichier mais toujours dans l'historique des
  deux depots, et dans le HEAD du miroir GitHub -> revocation requise
- SEC-05 trusted_proxy='*' + bind 0.0.0.0 rend X-Forwarded-For usurpable,
  ce qui ouvre le rate limiting au lieu de le corriger
- SEC-07 l'OAuth2 ajoute ne contraint pas l'identite : le discord_user_id
  transite par un champ cache du formulaire
- MNT-05 psycopg[binary] non epingle, incompatible avec les URI
  postgresql:// que SQLAlchemy resout vers psycopg2

48 constats. Aucune modification du code applicatif.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
GGThed
2026-08-07 13:17:08 -04:00
co-authored by Claude Opus 5
parent fa0a378827
commit ed586233f6
6 changed files with 704 additions and 333 deletions
+66 -20
View File
@@ -1,6 +1,8 @@
# 2 — Maintenabilité
15 constats sur la structure du code, l'outillage et la chaîne de livraison.
16 constats sur la structure du code, l'outillage et la chaîne de livraison.
Références de lignes sur `immortal/main` @ `bb0bc1c`. Le renommage `supporting_scrits``supporting_scripts` relevé lors de la première passe a été effectué par l'équipe — voir [`00-provenance.md`](00-provenance.md).
| ID | Constat | Sévérité |
|---|---|---|
@@ -18,7 +20,8 @@
| [MNT-12](#mnt-12--) | Duplication du parsing date/heure dans quatre modules | 🔵 Faible |
| [MNT-13](#mnt-13--) | `datetime.utcnow()` déprécié — 12 occurrences | 🔵 Faible |
| [MNT-14](#mnt-14--) | Aucune pagination sur les listes | 🔵 Faible |
| [MNT-15](#mnt-15--) | README en décalage avec le code, et dossier `supporting_scrits` mal orthographié | 🔵 Faible |
| [MNT-15](#mnt-15--) | README en décalage avec le code | 🔵 Faible |
| [MNT-16](#mnt-16--) | Fichier d'état d'exécution `discord_pending.json` versionné | 🔵 Faible |
---
@@ -157,7 +160,9 @@ Ajouter `pytest`, `pytest-cov` et `factory-boy` à un `requirements-dev.txt`, pu
run: python security_scan.py --skip-http
```
Le script se trouve en réalité à `app/supporting_scrits/security_scan.py`. Il n'y a pas de `security_scan.py` à la racine du dépôt.
Le script se trouve en réalité à `app/supporting_scripts/security_scan.py`. Il n'y a pas de `security_scan.py` à la racine du dépôt.
> Le renommage du dossier (commit `0dd4ecd`, `supporting_scrits` → `supporting_scripts`) **n'a pas corrigé la CI** : le chemin invoqué était déjà erroné avant, il l'est toujours après.
Le job échoue donc à chaque exécution avec `can't open file 'security_scan.py': [Errno 2] No such file or directory`. Contrairement au job `test`, celui-ci n'a pas de `continue-on-error` : il apparaît **en rouge en permanence**.
@@ -175,7 +180,7 @@ Deux problèmes secondaires dans le même job :
SECRET_KEY: ${{ secrets.CI_SECRET_KEY }}
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci
FLASK_DEBUG: 'false'
run: python -m app.supporting_scrits.security_scan --skip-http
run: python -m app.supporting_scripts.security_scan --skip-http
```
Et faire échouer le job si le script renvoie un code non nul, ce qu'il fait déjà via son `exit(main())`.
@@ -198,7 +203,19 @@ Vérifier aussi que le job `lint` passe (voir MNT-06) : dans l'état actuel, `te
**Impact.** Surface d'approvisionnement élargie sans contrepartie : trois paquets supplémentaires exécutent leur `setup.py` à l'installation et sont dans le chemin d'import. Les paquets aux noms proches de bibliothèques populaires (`dotenv`, `login`) sont précisément la cible privilégiée des attaques par confusion de dépendances. Ils sont ici bénins, mais leur présence indique que la liste n'est pas relue.
Par ailleurs, `psycopg2==2.9.12` (`:35`) exige une chaîne de compilation C et les en-têtes PostgreSQL ; `psycopg2-binary` est le choix usuel pour un déploiement sans compilation, ou `psycopg[binary]` (v3) pour un projet neuf.
**Un quatrième point mérite une vérification urgente.** Le pilote PostgreSQL est passé de `psycopg2==2.9.12` à **`psycopg[binary]`, sans version épinglée** — seule entrée non épinglée d'un fichier qui l'est partout ailleurs. Deux conséquences :
- La reproductibilité est rompue : deux installations à quelques semaines d'intervalle n'obtiendront pas la même version du pilote.
- **`psycopg[binary]` est psycopg 3, pas psycopg2.** Or SQLAlchemy 2.0 résout le préfixe `postgresql://` vers le dialecte **psycopg2** par défaut. Si `DATABASE_URL` commence par `postgresql://` — ce qu'indiquait le fichier d'exemple avant son nettoyage — la création du moteur lèvera `ModuleNotFoundError: No module named 'psycopg2'` au démarrage.
Le fonctionnement actuel suppose donc que la variable de production utilise la forme explicite `postgresql+psycopg://…`. **À vérifier** : si l'application tourne, c'est le cas ; sinon, c'est la cause du dysfonctionnement. Dans les deux situations, la dépendance implicite entre le format de l'URL et le pilote installé doit être documentée, ou levée en normalisant l'URI au démarrage :
```python
url = os.getenv('DATABASE_URL', '')
if url.startswith('postgresql://'):
url = url.replace('postgresql://', 'postgresql+psycopg://', 1)
app.config['SQLALCHEMY_DATABASE_URI'] = url
```
**Correction.**
1. Retirer `dotenv`, `login` et `discord` — vérifier au préalable qu'aucun `import` ne les référence (aucun n'apparaît dans le code).
@@ -208,11 +225,12 @@ Par ailleurs, `psycopg2==2.9.12` (`:35`) exige une chaîne de compilation C et l
dependencies = [
"Flask~=3.1", "Flask-SQLAlchemy~=3.1", "Flask-Login~=0.6",
"Flask-WTF~=1.3", "Flask-Limiter~=4.1", "flask-cors~=6.0",
"SQLAlchemy~=2.0", "psycopg2-binary~=2.9", "marshmallow~=4.3",
"SQLAlchemy~=2.0", "psycopg[binary]~=3.2", "marshmallow~=4.3",
"python-dotenv~=1.2", "discord.py~=2.7", "APScheduler~=3.11",
"waitress~=3.0", "requests~=2.34",
]
```
En épinglant `psycopg`, retenir une contrainte de version — l'absence d'épinglage est le point le plus risqué de la liste actuelle.
3. Fournir un `requirements-dev.txt` (`pytest`, `pytest-cov`, `ruff`, `pip-audit`).
4. Une fois MNT-02 corrigé, générer des hachages (`pip-compile --generate-hashes`) pour que `pip-audit --require-hashes` de `ci.yml:31` ait un sens.
@@ -302,23 +320,25 @@ Volumétrie des modules de routes :
| Module | Lignes |
|---|---|
| `users.py` | **1 245** |
| `users.py` | **1 265** |
| `matches.py` | 583 |
| `auth.py` | 473 |
| `teams.py` | 455 |
| `tryouts.py` | 432 |
| `tryouts.py` | ~430 |
| `team_matches.py` | 309 |
| `auth.py` | 296 |
| `evaluations.py` | 233 |
| `main.py` | 139 |
`users.py` regroupe six domaines fonctionnels sans lien entre eux, matérialisés par les commentaires de section du fichier lui-même :
1. gestion des comptes (`:76-228`) — CRUD administrateur ;
2. profil personnel (`:231-299`) ;
3. disponibilités (`:302-441`) — API JSON ;
4. contrats (`:444-629`) — téléversement et téléchargement de fichiers ;
5. sessions 1:1 (`:632-888`) — dont l'intégration Discord ;
6. notes (`:891-1245`) — personnelles et d'équipe, avec quatre routes de création presque identiques.
1. gestion des comptes (`:76-241`) — CRUD administrateur ;
2. profil personnel (`:244-320`) ;
3. disponibilités (`:323-462`) — API JSON ;
4. contrats (`:465-650`) — téléversement et téléchargement de fichiers ;
5. sessions 1:1 (`:653-909`) — dont l'intégration Discord ;
6. notes (`:912-1265`) — personnelles et d'équipe, avec quatre routes de création presque identiques.
`auth.py` a par ailleurs gagné 177 lignes avec le flux OAuth2 Discord (`:326-456`) et suit la même trajectoire : authentification par mot de passe, CAPTCHA et fédération d'identité désormais dans un seul fichier.
**Impact.** Un fichier de cette taille concentre les conflits de fusion, ralentit la navigation, et rend la revue de code superficielle. Le symptôme visible ici : les trois schémas de validation importés en tête ont cessé d'être utilisés (SEC-06) sans que personne ne le remarque, parce que la déclaration et l'usage sont séparés par plusieurs centaines de lignes.
@@ -564,9 +584,9 @@ Pour les listes déroulantes, préférer un point d'API filtré par saisie (auto
## MNT-15 · 🔵
**README en décalage avec le code, et dossier `supporting_scrits` mal orthographié**
**README en décalage avec le code**
**README.** La section « Security Features Implemented » (`README.md:11-21`) énonce des garanties qui ne correspondent pas au code :
La section « Security Features Implemented » (`README.md:11-21`) énonce des garanties qui ne correspondent pas au code :
| Affirmation | Réalité |
|---|---|
@@ -578,8 +598,34 @@ Le README ne mentionne par ailleurs ni la commande de démarrage exacte, ni la v
**Impact.** Une documentation de sécurité fausse est plus nuisible qu'absente : elle sert de base aux décisions de déploiement et coupe court aux vérifications. C'est vraisemblablement ce qui explique que les constats SEC-06 et SEC-15 aient survécu.
**Nommage.** Le dossier `app/supporting_scrits/` contient une faute (`scrits` → `scripts`). Elle se propage dans tous les imports (`app.py:355`, `ci.yml`, docstrings) et dans les chemins que les développeurs tapent quotidiennement.
Le README ne documente par ailleurs **rien de ce qui a été ajouté depuis** : ni le flux OAuth2 Discord et ses trois variables d'environnement (`DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`, `DISCORD_REDIRECT_URI`), ni `clear_db.py`, ni la chaîne de déploiement `.gitea/workflows/git-to-ptero.yaml`, ni le fait que le dépôt de référence n'est pas GitHub.
**Correction.**
- Réécrire la section sécurité pour décrire l'état réel, en distinguant ce qui est implémenté de ce qui est prévu. Y ajouter les prérequis, l'installation et le démarrage.
- Renommer le dossier en `app/supporting_scripts/` (`git mv`), en mettant à jour `app/app.py:355` et `.github/workflows/ci.yml:72` — ce dernier étant de toute façon à corriger (MNT-04).
- Réécrire la section sécurité pour décrire l'état réel, en distinguant ce qui est implémenté de ce qui est prévu.
- Ajouter les prérequis (version de Python, PostgreSQL obligatoire au démarrage), l'installation, le démarrage, et le tableau des variables d'environnement.
- **Indiquer explicitement quel dépôt fait autorité.** C'est ce qui a manqué ici : l'absence de cette mention a conduit à auditer un miroir en retard de 16 commits. Un `README.md` sur le miroir GitHub renvoyant vers `git.immortal.host` — ou la suppression du miroir — éviterait la récidive.
---
## MNT-16 · 🔵
**Fichier d'état d'exécution `discord_pending.json` versionné**
Le fichier `discord_pending.json` est suivi par git à la racine du dépôt. Son contenu actuel :
```json
{}
```
Le nom et l'usage indiquent un état d'exécution du bot Discord — vraisemblablement le suivi des demandes en attente de réaction, équivalent persistant du dictionnaire `self.pending_requests` de `discord_bot.py:52`.
**Impact.** Un fichier d'état écrit par l'application et versionné pose trois problèmes :
1. **Conflits de fusion permanents.** Dès que le bot écrit dedans, le fichier apparaît modifié dans `git status`. Chaque développeur qui lance l'application localement produit une modification non intentionnelle, qu'il committera par inadvertance ou devra écarter à chaque fois.
2. **Écrasement au déploiement.** Le workflow SFTP ([SEC-21](01-securite.md#sec-21--)) pousse le contenu du dépôt sur le serveur : chaque déploiement **remet l'état du bot à `{}`**, perdant les demandes en attente. Les réactions ✅/❌ sur les messages Discord antérieurs cesseront d'être reconnues.
3. **Fuite potentielle.** Selon ce qui y est stocké (identifiants Discord, identifiants de demandes 1:1), le fichier peut contenir des données personnelles qui n'ont rien à faire dans un dépôt.
**Correction.**
- Retirer le fichier du suivi (`git rm --cached discord_pending.json`) et l'ajouter au `.gitignore`.
- Mieux : persister cet état **en base**, où vivent déjà les `OneOnOneRequest`. Un fichier JSON local ne survit ni au déploiement, ni à la mise à l'échelle ([STD-03](03-standards-stack.md#std-03--)), et n'est pas partageable entre plusieurs instances du bot.
- Vérifier au passage qu'aucune donnée personnelle n'a été committée dans ses versions antérieures (`git log -p -- discord_pending.json`).