Initial commit — Stupid Simple Network Inventory

Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:19:19 +02:00
commit 88cf6458d0
58 changed files with 10365 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
# Security Fixes Applied — Phase 3
Date: 2026-05-06
## SEC-FIX-006 — Validation des entrées discovery
### Problème
- `dns_server` acceptait n'importe quelle chaîne (risque d'injection dans le résolveur DNS)
- `/api/discovery/ping` acceptait n'importe quelle chaîne comme IP (transmise au sous-processus ping)
- Plusieurs targets pouvaient contourner le cap de 1024 : 5 × /22 = 5110 hôtes
### Corrections appliquées
**`backend/routers/discovery.py`**
- `ScanRequest.dns_server` : `field_validator` qui appelle `ipaddress.ip_address(v)` — rejette toute valeur non-IP (422)
- `PingRequest.ips` : `field_validator` qui valide chaque IP — rejette toute entrée malformée avant d'appeler le sous-processus (422)
- `MAX_HOSTS_TOTAL = 4096` : cap global sur la somme des hôtes de tous les targets — rejette si dépassé (400)
---
## SEC-FIX-007 — Validation Pydantic métier
### Problème
Aucun validator sur les schémas métier : valeurs hors-domaine, CIDR invalides, couleurs arbitraires, URLs arbitraires, IPs invalides, types inconnus acceptés silencieusement.
### Corrections appliquées
**`backend/routers/vlans.py`**
- `vlan_id` : 14094 (norme 802.1Q)
- `name` : non vide, max 100 caractères (strip)
- `cidr` : `ipaddress.ip_network(strict=False)` si non vide
- `color` : regex `^#[0-9a-fA-F]{6}$`
**`backend/routers/devices.py`**
- `name` : non vide, max 100 caractères (strip)
- `description` : max 500 caractères
- `type` : enum des 18 types valides
- `virt_type` : enum `{null, baremetal, lxc, qemu}`
- `url` : `urlparse` — schéma `http`/`https` + netloc non vide
- `InterfaceCreate.name` : non vide, max 50 caractères
- `InterfaceCreate.ip_address` : `ipaddress.ip_address(v)` si non vide
Toutes les erreurs de validation retournent 422 (comportement standard FastAPI/Pydantic).
---
## SEC-FIX-008 — En-têtes HTTP sécurité Nginx
### Problème
Aucun en-tête de sécurité HTTP : pas de CSP, pas de protection contre le MIME sniffing ou le framing.
### Corrections appliquées
**`frontend/nginx.conf`**
```
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self';
```
Tous les en-têtes utilisent le flag `always` pour s'appliquer aussi aux réponses d'erreur.
---
## SEC-FIX-009 — Documentation TLS/HTTPS
### Statut : CORRIGÉ EN PHASE 2/3
Documentation ajoutée au `README.md` lors de la session précédente :
- Section HTTPS avec exemple nginx reverse-proxy complet
- `docker-compose.override.yml` pour limiter l'exposition au loopback
---
## SEC-FIX-012 — Logs d'audit structurés
### Problème
Aucune trace des événements d'authentification : connexions, échecs, changements de mot de passe.
### Corrections appliquées
**`backend/routers/auth.py`**
- Logger stdlib `logging.getLogger("audit")` — aucune dépendance supplémentaire
- Helper `_log_audit(event, **kw)` : émet une ligne JSON `{"event": ..., "ts": ..., ...}`
- Événements loggés :
- `auth.login.success` — username, ip
- `auth.login.failure` — username, ip
- `auth.login.rate_limited` — ip, username (si disponible), reason (`ip` | `username`)
- `auth.token_rejected` — username, reason (`user_not_found` | `version_mismatch`)
- `auth.account.password_changed` — username
- `auth.account.username_changed` — old_username, new_username
- `auth.account.bad_password` — username
Activation dans Docker via la config de logging uvicorn (stdout par défaut).
---
## SEC-FIX-013 — PRAGMA foreign_keys=ON
### Problème
SQLite désactive les contraintes de clés étrangères par défaut. Les suppressions en cascade ne sont pas enforced.
### Corrections appliquées
**`backend/database.py`**
```python
@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_conn, _record):
if isinstance(dbapi_conn, sqlite3.Connection):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
```
Le listener s'exécute sur chaque nouvelle connexion SQLite, y compris les connexions de test.
---
## SEC-FIX-014 — Dépendance `cytoscape` non utilisée
### Statut : CORRIGÉ EN PHASE 2
`cytoscape` supprimé de `frontend/package.json` lors de la session précédente.
---
## SEC-FIX-015 — Import JSON sans limite de taille ni validation de schéma
### Problème
Un fichier JSON de plusieurs gigaoctets pouvait être chargé en mémoire. Aucune validation du schéma avant traitement.
### Corrections appliquées
**`frontend/src/App.vue`**
- Vérification `file.size > 5 * 1024 * 1024` avant `file.text()` — rejette avec message localisé
- Validation de schéma :
- Le JSON doit être un objet (pas un tableau, pas null)
- `vlans` et `devices`, si présents, doivent être des tableaux
**`frontend/src/i18n.js`**
- Clé `importTooLarge` ajoutée (fr, en, es)
---
## SEC-FIX-016 — `v-html` dans App.vue
### Problème
`v-html` utilisé pour injecter des entités HTML (`&#9632;`, `&#9670;`, `&#9635;`) dans les boutons de navigation. Risque XSS si la source venait à être dynamique.
### Corrections appliquées
**`frontend/src/App.vue`**
- Entités HTML remplacées par leurs équivalents Unicode directs (`■`, `◆`, `▣`)
- `v-html="tab.icon"` remplacé par `{{ tab.icon }}` (interpolation texte — échappe automatiquement)
---
## SEC-FIX-017 — Nettoyage du code orphelin Links (backend)
### Problème
La vue Liens avait été retirée du frontend en phase 2/3, mais le backend conservait :
- `backend/routers/links.py` — router toujours enregistré dans main.py
- `class Link` dans `models.py` — ORM orphelin
- Référence explicite à `models.Link` dans `delete_device` (devices.py)
- La table `links` en base — avec FK vers `devices`, ce qui bloquerait les suppressions d'équipements avec `PRAGMA foreign_keys=ON`
### Corrections appliquées
**`backend/routers/links.py`** — Supprimé
**`backend/models.py`** — Classe `Link` supprimée
**`backend/routers/devices.py`** — Suppression de la requête `db.query(models.Link).filter(...).delete()` dans `delete_device`
**`backend/main.py`**
- Import `links` retiré
- `app.include_router(links.router, ...)` retiré
- Nouvelle migration `_migrate_drop_links_table()` : DROP TABLE links si elle existe (avec `PRAGMA foreign_keys=OFF/ON` pour éviter les erreurs FK pendant la migration)
---
## Fichiers modifiés
| Fichier | Fix(es) |
|---------|---------|
| `backend/database.py` | SEC-FIX-013 |
| `backend/models.py` | SEC-FIX-017 |
| `backend/main.py` | SEC-FIX-017 |
| `backend/routers/auth.py` | SEC-FIX-012 |
| `backend/routers/vlans.py` | SEC-FIX-007 |
| `backend/routers/devices.py` | SEC-FIX-007, SEC-FIX-017 |
| `backend/routers/discovery.py` | SEC-FIX-006 |
| `backend/routers/links.py` | SEC-FIX-017 (supprimé) |
| `backend/tests/test_validation.py` | Tests SEC-FIX-006, 007, 013, 017 (nouveau) |
| `frontend/nginx.conf` | SEC-FIX-008 |
| `frontend/src/App.vue` | SEC-FIX-015, SEC-FIX-016 |
| `frontend/src/i18n.js` | SEC-FIX-015 |