Retrait des fichiers internes du suivi git

CLAUDE.md, AGENTS.md et SECURITY*.md sont des documents de travail
qui n'ont pas vocation à être publiés sur le dépôt distant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 09:26:16 +02:00
parent 88cf6458d0
commit 423413a282
11 changed files with 0 additions and 2248 deletions
-207
View File
@@ -1,207 +0,0 @@
# Stupid Simple Network Inventory — Agent Notes
## Project Overview
Web application for manual server/network inventory and logical network topology display.
## Stack
- Backend: FastAPI, Python 3.11, SQLAlchemy, SQLite
- Frontend: Vue 3 + Vite
- Topology view: CSS cards, no Cytoscape
- Icons: `lucide-vue-next` for generic UI, `simple-icons` for brand logos
- Reverse proxy: Nginx serves the frontend and proxies `/api/` to the backend
- Runtime: Docker Compose
## Run
```bash
docker compose up --build -d
```
Application URL: http://localhost:8080
## Architecture
- `backend/main.py`: FastAPI entry point + startup SQLite migrations (run before `create_all`)
- `backend/database.py`: SQLAlchemy engine and sessions
- `backend/models.py`: ORM models (User, Vlan, Device, DeviceInterface)
- `backend/routers/auth.py`: JWT login, account update, token refresh — exports `get_current_user` for main.py
- `backend/routers/vlans.py`: network CRUD
- `backend/routers/devices.py`: device CRUD — assign fields explicitly, do not use `model_dump()` on Device
- `backend/routers/discovery.py`: ICMP ping sweep + DNS PTR (`/scan`) + parallel ping (`/ping`)
- `frontend/src/App.vue`: global layout and state + auth guard (shows LoginPage if not authenticated)
- `frontend/src/api.js`: axios API calls (`vlansApi`, `devicesApi`, `discoveryApi`, `authApi`)
- `frontend/src/auth.js`: reactive auth state (`isAuthenticated`, `currentUsername`, `setAuth`, `clearAuth`, `getToken`) — persisted in localStorage
- `frontend/src/i18n.js`: i18n — `locale` (ref), `t(key)`, `tFmt(key, ...args)`, `setLocale(lang)`
- `frontend/src/theme.js`: theme — `theme` (ref), `toggleTheme()`, applies/removes `html.dark` class
- `frontend/src/brandIcons.js`: BRANDS array + `detectBrands(name, description)` — shared utility
- `frontend/src/components/TopologyGraph.vue`: topology card layout, ping feature
- `frontend/src/components/DeviceIcon.vue`: Lucide type icon (prop `typeOnly`) or brand logos
- `frontend/src/components/DeviceManager.vue`: device CRUD + filter bar + search
- `frontend/src/components/VlanManager.vue`: network CRUD (LANs + VLANs)
- `frontend/src/components/DiscoveryModal.vue`: automatic discovery UI
- `frontend/src/components/LoginPage.vue`: full-screen login form, emits `@login` with `{ token, username }`
- `frontend/src/components/AccountModal.vue`: change username/password, emits `@updated` with new token
## Data Model
- `User`: `id`, `username` (unique), `hashed_password` (bcrypt), `must_change_password` (bool, default 0), `token_version` (int, default 1)
- `Vlan`: `id`, `vlan_id` (nullable int — null = plain LAN), `name`, `cidr`, `color`
- `Device`: `id`, `name`, `type`, `description`, `is_gateway`, `is_livebox`, `virt_type` (nullable), `url` (nullable)
- `DeviceInterface`: `id`, `device_id`, `vlan_id` (nullable FK), `ip_address`, `name`, `is_upstream`
## Authentication
JWT HS256, **24-hour expiry**. Payload: `{ sub: username, ver: token_version, exp: ... }`. Secret key persisted in `data/secret_key.txt` (0600 permissions) or `SECRET_KEY` env var.
Default credentials: `admin` / `admin` — seeded with `must_change_password=1` by `_migrate_users()` if the `users` table is empty. Set `INITIAL_ADMIN_PASSWORD` env var to skip this bootstrap.
**Token invalidation**: password change increments `User.token_version`. `get_current_user` rejects tokens whose `ver` doesn't match → old sessions expire immediately.
**must_change_password**: when `True`, business routers return 403 `"Password change required"`. The frontend shows a forced `<AccountModal :forced="true">` that blocks the app. `_migrate_force_admin_password_change()` sets this flag at startup if admin's password is still the default.
**bcrypt compatibility**: `passlib 1.7.4` is incompatible with `bcrypt >= 4.0`. `requirements.txt` pins `bcrypt==3.2.2`. Do not upgrade `bcrypt` without also upgrading `passlib`.
Business route groups protected via `dependencies=[Depends(require_password_changed)]` (wraps `get_current_user`). Auth router has no protection.
Frontend auth guard in `App.vue`:
- `!isAuthenticated``<LoginPage>`
- `mustChangePassword``<AccountModal :forced="true">`
- else → full app
Template root is `<div class="app-root">` with `display: contents` — prevents Firefox autofill overlay `NotFoundError` on fragment comment nodes.
## Device Types (18)
`server`, `switch`, `router`, `nas`, `gateway`, `livebox`, `access_point`, `camera`, `temperature`, `sensor`, `hub`, `smart_plug`, `alarm`, `light`, `doorbell`, `desktop`, `laptop`, `other`
## Device Fields
### virt_type
Form label: "Type d'environnement d'exécution". Values: `null`, `baremetal`, `lxc`, `qemu`.
### url
Optional web UI URL. Hidden in the form for `desktop` and `laptop` types. Displayed as a clickable link in the device card, and as a green "Link" tag on topology chips.
When adding a new Device field: update `models.py`, add a startup migration in `main.py`, update `DeviceCreate` and `DeviceOut` in `routers/devices.py`, assign explicitly in `create_device` and `update_device`.
## Networks (VlanManager)
`vlan_id` is optional. Omitting it creates a plain LAN ("LAN" badge). Networks ordered: LANs first (NULL vlan_id), then VLANs by ascending ID.
## Topology View
`TopologyGraph.vue` uses CSS cards only.
- Toolbar: Ping button with animated dot indicator + up/down count
- WAN card: red, `is_livebox` devices
- Gateway card: yellow, `is_gateway` devices
- Network list: one full-width card per network, stacked vertically (flex-col); a device appears in every card matching its interfaces
- Unassigned card: devices with no interface, not livebox, not gateway
- Devices sorted by IP ascending; no-IP devices go last
### Card layout
All network cards are full width, stacked vertically. Inside each card, chips are arranged in a horizontal grid (`grid auto-fill, minmax(210px, 1fr)`), wrapping as needed. Card height grows with the number of chips. Single column below 600px.
### Device chip structure
```
[chip-icon (type, Lucide)] [chip-body] [chip-tags]
chip-name
chip-sub: IP + iface + brand SVGs (11px)
chip-tags: Link | GW | LXC | VM + ping dot
```
- `chip-icon`: always the Lucide type icon (`type-only` prop)
- Brand logos: small colored SVGs in `chip-sub`, no text label, `title` tooltip on hover
- `tag-link`: green, clickable `<a>`, only if `device.url` is set
- Ping dot: green (`ping-up`) or red (`ping-down`), shown after a ping run
## DeviceIcon.vue
Prop `typeOnly` (bool, default false):
- `false`: show brand logos if detected, else Lucide — used in TopologyGraph chips
- `true`: always show Lucide type icon — used in DeviceManager cards
## Brand Icons (brandIcons.js)
Central `detectBrands(name, description)` returns all matching brand icon objects from `simple-icons`.
To check if a brand exists: `node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('name')))"`
To add a brand: import `si<Name>` in `brandIcons.js`, add entry to `BRANDS` array.
Supported brands: Proxmox, Docker, Synology, TrueNAS, Ubiquiti/UniFi, MikroTik, Cisco, TP-Link, ASUS, Netgear, pfSense, OPNsense, OpenWrt, Apache/Apache2, Traefik, MariaDB, Kubernetes/k3s, Debian, Ubuntu, Ansible, Dell, HP, Raspberry Pi, Arduino, KDE/Plasma, Excalidraw, Nextcloud, Paperless-NGX, Uptime Kuma, MkDocs, Jellyfin, Home Assistant, Philips Hue, Xiaomi.
## DeviceManager Filter Bar
Compact 28px bar combining:
- Search input (name, description, IP — real-time, Escape to clear)
- Type dropdown (multi-select, only present types)
- Network dropdown (with color swatch)
- Brand dropdown (with SVG icon, sorted alphabetically)
- Virt dropdown (only if at least one device has a virt_type)
All filters combine (AND between categories, OR within). "Clear" button + "X / total" counter when active.
## Ping (discovery.py)
`POST /api/discovery/ping` — parallel ICMP ping, 50 workers.
- Body: `{ ips: ["1.2.3.4", ...] }`
- Returns: `[{ ip, alive }, ...]`
Device status: "up" if any of its IPs responds, "down" if all fail, no dot if no IPs.
## SQLite Migrations (main.py)
Idempotent startup migrations run before `create_all`:
- `_migrate_vlan_nullable()`: recreates `vlans` to allow NULL `vlan_id`
- `_migrate_device_virt_type()`: adds `virt_type VARCHAR` to `devices`
- `_migrate_device_url()`: adds `url VARCHAR` to `devices`
- `_migrate_users()`: creates the `users` table and seeds `admin`/`admin` if count is 0
Pattern for new nullable column: check if column exists via `PRAGMA table_info`, run `ALTER TABLE … ADD COLUMN` if missing.
## Discovery
`POST /api/discovery/scan`: ICMP ping sweep + DNS PTR lookup. Max 1024 hosts/network, 100 workers. Needs `cap_add: NET_RAW` (already configured).
## Theme (theme.js)
`theme.js` exports `theme` (ref, `'light'`/`'dark'`) and `toggleTheme()`. Persisted in `localStorage('theme')`. Applies/removes `html.dark` class on `document.documentElement`.
CSS variables are defined in `App.vue` global `<style>`:
- `:root` — light theme
- `html.dark` — dark theme
Available variables: `--bg-page`, `--bg-card`, `--bg-card-hover`, `--bg-input`, `--bg-thead`, `--bg-chip`, `--bg-chip-hover`, `--border`, `--border-strong`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--chip-ip-bg`, `--chip-ip-color`, `--modal-overlay`, `--shadow-card`, `--shadow-modal`, `--shadow-drop`.
**Rule**: all background/text/border colors in components must use these variables, not hardcoded hex. Exception: semantic fixed colors (type-specific chip icon colors, VLAN badge colors) stay as hex.
Dark-mode overrides for scoped components must use `:global(html.dark .selector)` — the full selector must be inside the parentheses. `:global(html.dark) .selector` does NOT work (Vue's scoped CSS transformer does not follow descendant selectors after `:global()`).
Toggle button (☾/☀) in sidebar footer.
## i18n (i18n.js)
`i18n.js` exports `locale` (ref), `t(key)`, `tFmt(key, ...args)`, `setLocale(lang)`. Persisted in `localStorage('locale')`, default `'fr'`.
Supported languages: `fr`, `en`, `es`.
**Rule**: all visible UI strings use `t('key')` — never hardcoded text in templates. `deviceTypes` in `DeviceManager` is a `computed` (not a const) so type labels react to locale changes.
Language switcher pills (fr / en / es) in sidebar footer.
## Favicon
`frontend/public/favicon.svg` — SVG network graph icon (indigo #6366f1), same shape as sidebar logo. Linked in `index.html` as `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`.
## Action Buttons (btn-icon)
`.btn-icon` uses `background: var(--bg-chip)` and `color: var(--text-secondary)` for adequate contrast in both themes. Hover: `var(--border-strong)` / `var(--text-primary)`. Danger hover: `rgba(239,68,68,0.15)` (transparent red, theme-neutral).
## Persistence
Docker volume `db_data``/app/data/topology.db` and `/app/data/secret_key.txt`.
-247
View File
@@ -1,247 +0,0 @@
# Stupid Simple Network Inventory
Application web d'inventaire manuel de serveurs et de génération de topologie réseau logique.
## Stack technique
- **Backend** : FastAPI (Python 3.11) + SQLAlchemy + SQLite
- **Frontend** : Vue 3 + Vite (pas de Cytoscape — topologie en cards CSS)
- **Icônes** : `lucide-vue-next` (UI générique) + `simple-icons` (logos de marques)
- **Reverse proxy** : Nginx (sert le frontend et proxifie `/api/` vers le backend)
- **Conteneurisation** : Docker Compose
## Lancement
```bash
docker compose up --build -d
```
L'application est accessible sur **http://localhost:8080**
## Architecture
```
topologie/
├── docker-compose.yml
├── backend/
│ ├── Dockerfile # Python 3.11-slim + iputils-ping (pour ICMP)
│ ├── requirements.txt # fastapi, sqlalchemy, dnspython, ...
│ ├── main.py # Point d'entrée FastAPI + migrations SQLite au démarrage
│ ├── database.py # SQLAlchemy engine + session
│ ├── models.py # Modèles ORM (Vlan, Device, DeviceInterface)
│ └── routers/
│ ├── vlans.py
│ ├── devices.py
│ └── discovery.py # POST /api/discovery/scan + POST /api/discovery/ping
└── frontend/
├── Dockerfile # Multi-stage: build Vite → Nginx
├── nginx.conf # Proxy /api/ → backend:8000
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.js
├── App.vue # Layout + état global (vlans, devices) + garde auth
├── api.js # Appels axios (vlansApi, devicesApi, discoveryApi, authApi)
├── auth.js # État auth : isAuthenticated, currentUsername, setAuth(), clearAuth()
├── i18n.js # Internationalisation : locale (ref), t(key), tFmt(key, ...args), setLocale()
├── theme.js # Thème : theme (ref), toggleTheme() — classe html.dark
├── brandIcons.js # Tableau BRANDS + fonction detectBrands() — partagé entre composants
└── components/
├── TopologyGraph.vue # Vue cards par réseau + ping statut
├── DeviceIcon.vue # Icône Lucide par type (prop typeOnly) ou logos marques
├── DeviceManager.vue # CRUD équipements + barre filtres/recherche
├── VlanManager.vue # CRUD réseaux (VLANs + LANs classiques)
├── DiscoveryModal.vue # Découverte auto : ping sweep + DNS PTR
├── LoginPage.vue # Page de connexion plein écran
└── AccountModal.vue # Modale changement nom d'utilisateur / mot de passe
```
## Modèle de données
- **User** : id, username (unique), hashed_password (bcrypt)
- **Vlan** : id, vlan_id (int, nullable — null = LAN classique sans tag 802.1Q), name, cidr, color
- **Device** : id, name, type, description, is_gateway, is_livebox, virt_type (nullable), url (nullable)
- **DeviceInterface** : id, device_id, vlan_id (nullable), ip_address, name, is_upstream
## Types d'équipements (18)
`server`, `switch`, `router`, `nas`, `gateway`, `livebox`, `access_point`, `camera`, `temperature`, `sensor`, `hub`, `smart_plug`, `alarm`, `light`, `doorbell`, `desktop`, `laptop`, `other`
## Champ virt_type (Device)
Libellé dans le formulaire : "Type d'environnement d'exécution". Valeurs :
- `null` — non précisé (défaut)
- `baremetal` — serveur physique
- `lxc` — conteneur LXC (tag bleu "LXC" dans la topologie)
- `qemu` — VM QEMU/KVM (tag violet "VM" dans la topologie)
## Champ url (Device)
URL optionnelle d'accès à l'interface web de l'équipement. Non applicable aux types `desktop` et `laptop` (masqué dans le formulaire). Affiché comme lien cliquable dans la card équipement et comme tag "Link" vert sur les chips de topologie.
## Réseaux (VlanManager.vue)
`vlan_id` est optionnel. Si absent → réseau LAN classique (badge "LAN"). Si présent → VLAN 802.1Q (badge "VLAN X").
Les réseaux sont triés : LANs en premier (vlan_id NULL), puis VLANs par ID croissant.
## Topologie (TopologyGraph.vue)
Pas de graphe Cytoscape. Layout en cards CSS :
- **Barre d'outils** en haut : bouton Ping avec indicateur animé + compteur up/down
- **Carte WAN** (rouge) — devices `is_livebox`
- **Carte Passerelle** (jaune) — devices `is_gateway`
- **Grille réseaux** — une card par réseau (LAN ou VLAN), chaque device apparaît dans toutes les cards de ses réseaux (multi-réseau natif)
- **Zone non assigné** — devices sans interface réseau, ni livebox, ni gateway
- **Tri** : devices triés par IP croissante dans chaque card
### Layout des cards
- **Cards pleine largeur**, empilées verticalement (flex-col) — toutes de la même largeur, hauteur variable selon le nombre de devices
- Barre colorée en haut de chaque card (3px, couleur du réseau), bordures latérales neutres
- Header : badge VLAN/LAN (pill colorée) + nom + CIDR inline + compteur de devices (pill)
- **Chips en grille horizontale** (`grid auto-fill, minmax(210px, 1fr)`) à l'intérieur de chaque card — s'adaptent automatiquement au nombre de colonnes disponibles
- Responsive : sous 600px, une seule colonne de chips
### Device chip
- Icône de type (Lucide) toujours dans le carré arrondi (28px) avec fond coloré
- Nom (bold 12px) + IP (pill monospace) + nom d'interface (grisé) + logos de marques (SVG colorés 11px)
- Tags à droite en colonne : **Link** (vert, cliquable — tag URL de l'équipement) / **GW** / **LXC** / **VM**
- Dot de ping : vert (joignable) ou rouge (injoignable), affiché après un ping
## Icônes (DeviceIcon.vue + brandIcons.js)
La détection de marque est centralisée dans `brandIcons.js` (`detectBrands(name, description)`).
`DeviceIcon.vue` accepte une prop `typeOnly` (booléen) :
- `false` (défaut) : affiche les logos de marques si détectés, sinon icône Lucide — utilisé dans TopologyGraph
- `true` : toujours l'icône Lucide par type — utilisé dans DeviceManager
Dans **DeviceManager**, les logos de marques apparaissent comme badges colorés séparés (couleur officielle de la marque) dans les meta de la card.
Dans **TopologyGraph**, les logos de marques apparaissent comme petits SVG 11px colorés dans `chip-sub`.
Marques supportées via Simple Icons :
Proxmox, Docker, Synology, TrueNAS, Ubiquiti/UniFi, MikroTik, Cisco, TP-Link, ASUS, Netgear, pfSense, OPNsense, OpenWrt, Apache/Apache2, Traefik, MariaDB, Kubernetes/k3s, Debian, Ubuntu, Ansible, Dell, HP, Raspberry Pi, Arduino, KDE/Plasma, Excalidraw, Nextcloud, Paperless-NGX, Uptime Kuma, MkDocs, Jellyfin, Home Assistant, Philips Hue, Xiaomi
Pour ajouter une marque : vérifier avec `node -e "const si = require('./node_modules/simple-icons'); console.log(Object.keys(si).filter(k => k.toLowerCase().includes('nom')))"`, importer `si<Name>` dans `brandIcons.js`, ajouter une entrée dans BRANDS.
Fallback : Lucide (icône par type d'équipement).
## Vue Équipements (DeviceManager.vue)
### Barre de filtres + recherche
Barre compacte (28px) combinant :
- **Champ de recherche** : filtre par nom, description ou IP en temps réel (Échap pour vider)
- **Filtre Type** : dropdown multi-sélection, seuls les types présents dans les données
- **Filtre Réseau** : dropdown avec pastille colorée par VLAN/LAN
- **Filtre Marque** : dropdown avec icône SVG, trié alphabétiquement
- **Filtre Virt** : dropdown, seulement si au moins un équipement a un virt_type
Tous les filtres sont combinables (ET entre catégories, OU à l'intérieur). Bouton "Effacer" + compteur `X / total` si filtre actif.
## Ping (discovery.py + TopologyGraph.vue)
`POST /api/discovery/ping` — ping ICMP parallèle (50 workers).
- Body : `{ ips: ["1.2.3.4", ...] }`
- Retourne : `[{ ip, alive }, ...]`
Dans la topologie : bouton "Ping" déclenche le ping de toutes les IPs connues. Un équipement est "up" si au moins une de ses IPs répond. Dot vert/rouge sur chaque chip.
## Authentification (auth.py + auth.js)
JWT HS256, **expiration 24h**. Payload : `{ sub: username, ver: token_version, exp: ... }`. Clé secrète dans `data/secret_key.txt` (permissions 0600, auto-générée) ou `SECRET_KEY` env var.
**Compte par défaut** : `admin` / `admin` avec `must_change_password=1` — créé si la table `users` est vide. Définir `INITIAL_ADMIN_PASSWORD` pour un mot de passe personnalisé (sans changement forcé). La migration `_migrate_force_admin_password_change()` remet `must_change_password=1` si l'admin utilise encore le mot de passe bootstrap sur une base existante.
**Invalidation de token** : le changement de mot de passe incrémente `User.token_version`. `get_current_user` rejette tout token dont `ver` ne correspond pas → invalidation immédiate des sessions précédentes.
**Compatibilité bcrypt** : `passlib 1.7.4` est incompatible avec `bcrypt >= 4.0`. Le fichier `requirements.txt` épingle `bcrypt==3.2.2`. Ne pas mettre à jour `bcrypt` sans mettre à jour `passlib`.
### Endpoints auth
| Méthode | Chemin | Auth requise | Description |
|---------|--------|:------------:|-------------|
| POST | `/api/auth/login` | Non | Form → `{ access_token, token_type, username, must_change_password }` — 429 si rate-limited |
| PUT | `/api/auth/account` | Oui (même si must_change) | `{ current_password, new_username?, new_password? }` → nouveau token + `must_change_password` |
| GET | `/api/auth/me` | Oui | `{ username, must_change_password }` |
Routeurs métier protégés via `dependencies=[Depends(require_password_changed)]` (= `get_current_user` + rejet 403 si `must_change_password=True`).
### Côté frontend
`auth.js` expose `isAuthenticated`, `currentUsername`, `mustChangePassword` (refs) et `setAuth(token, username, mustChange)`, `clearAuth()`, `getToken()`. Persisté dans `localStorage`.
`App.vue` guard :
- `!isAuthenticated``<LoginPage>`
- `mustChangePassword``<AccountModal :forced="true">` (bloque l'application)
- sinon → application complète
Template enveloppé dans `<div class="app-root">` (un seul nœud racine) pour éviter le bug Firefox avec l'overlay d'autofill sur les fragments Vue (plusieurs nœuds racines). `.app-root` utilise `display: flex; flex-direction: column; min-height: 100vh` — ne pas revenir à `display: contents` (bug Firefox : les clics ne sont plus dispatché aux enfants après un remplacement du DOM à l'intérieur d'un élément `display: contents`).
## Migrations SQLite (main.py)
`main.py` exécute des migrations idempotentes au démarrage avant `create_all` :
- `_migrate_vlan_nullable()` — recrée `vlans` sans contrainte NOT NULL sur `vlan_id`
- `_migrate_device_virt_type()` — ajoute `virt_type` si absente
- `_migrate_device_url()` — ajoute `url` si absente
- `_migrate_users_must_change_password()` — ajoute `must_change_password` si absente
- `_migrate_users_token_version()` — ajoute `token_version` si absente
- `_migrate_force_admin_password_change()` — rattrapage : force must_change si admin utilise le mot de passe bootstrap
- `_migrate_drop_links_table()` — supprime la table `links` (fonctionnalité retirée en phase 3)
- `_migrate_users()` — crée `users` + compte admin si vide
Pour toute nouvelle colonne nullable sur une table existante, ajouter une fonction de migration du même type.
Quand on ajoute un champ à Device : mettre à jour `models.py`, ajouter la migration dans `main.py`, ajouter le champ dans `DeviceCreate` et `DeviceOut` dans `routers/devices.py`, l'assigner explicitement dans `create_device` et `update_device`.
## Découverte automatique (DiscoveryModal.vue)
`POST /api/discovery/scan` — ping sweep ICMP + lookup PTR DNS contre un serveur DNS configurable.
- Max 1024 hôtes par VLAN, 100 workers concurrents
- Retourne : ip, hostname, vlan_id, durée
- Nécessite `cap_add: NET_RAW` sur le service backend (déjà configuré)
## Thème (theme.js + App.vue)
`frontend/src/theme.js` exporte `theme` (ref) et `toggleTheme()`.
- Valeurs : `'light'` (défaut) / `'dark'`
- Persisté dans `localStorage('theme')`
- Applique / retire la classe `html.dark` via `watch`
Les variables CSS sont définies dans le `<style>` global de `App.vue` :
- `:root` — thème clair (`--bg-page: #F1F5F9`, `--bg-card: #ffffff`, `--text-primary: #0F172A`, ...)
- `html.dark` — thème sombre (`--bg-page: #0F172A`, `--bg-card: #1E293B`, `--text-primary: #F1F5F9`, ...)
Variables disponibles : `--bg-page`, `--bg-card`, `--bg-card-hover`, `--bg-input`, `--bg-thead`, `--bg-chip`, `--bg-chip-hover`, `--border`, `--border-strong`, `--text-primary`, `--text-secondary`, `--text-muted`, `--text-faint`, `--chip-ip-bg`, `--chip-ip-color`, `--modal-overlay`, `--shadow-card`, `--shadow-modal`, `--shadow-drop`.
Bouton toggle (☾/☀) dans le footer de la sidebar.
**Règle** : toutes les couleurs de fond/texte/bordure dans les composants doivent utiliser ces variables, pas des valeurs hex hardcodées.
Exception : les couleurs sémantiques fixes (couleur des icônes par type d'équipement dans les chips, couleurs des badges VLAN) restent en hex car elles ne dépendent pas du thème.
## Internationalisation (i18n.js)
`frontend/src/i18n.js` exporte :
- `locale` (ref) — langue active, persistée dans `localStorage('locale')`, défaut `'fr'`
- `setLocale(lang)` — change la langue
- `t(key)` — retourne la traduction, fallback `'fr'` puis la clé brute
- `tFmt(key, ...args)` — idem avec interpolation `{0}`, `{1}`, ...
Langues supportées : **français** (`fr`), **anglais** (`en`), **espagnol** (`es`).
Sélecteur (pills `fr / en / es`) dans le footer de la sidebar.
**Règle** : toutes les chaînes visibles dans l'UI passent par `t('key')` — jamais de texte hardcodé dans les templates. `deviceTypes` dans `DeviceManager` est un `computed` (pas une constante) pour réagir aux changements de locale.
## Favicon
`frontend/public/favicon.svg` — SVG indigo (#6366f1), même forme que le logo sidebar (graphe de nœuds réseau).
Référencé dans `frontend/index.html` : `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />`.
## Données persistées
Volume Docker `db_data` monté sur `/app/data/``topology.db` (SQLite)
-336
View File
@@ -1,336 +0,0 @@
# Audit de securite de base
Projet audite: Stupid Simple Network Inventory
Date: 2026-05-05
Referentiels: OWASP ASVS niveau 1, OWASP Top 10 2021
Mode: revue statique locale, sans attaque reelle, sans exploitation, sans correction applicative.
## Synthese
L'application est une SPA Vue exposee par Nginx, avec API FastAPI protegee par JWT pour les routeurs metier. Le modele d'exploitation precise est local, derriere un reverse-proxy. Dans ce modele, certains controles comme TLS, filtrage d'exposition et en-tetes HTTP peuvent legitimement etre portes par le reverse-proxy, mais doivent etre documentes et verifiables.
Risque global estime: moyen en usage local derriere reverse-proxy correctement configure; eleve si le service est expose directement a un LAN non fiable ou a Internet. Le risque principal vient du bootstrap avec identifiants publics `admin/admin` si le changement n'est pas force avant exposition, de l'absence de limitation de tentatives, du stockage du JWT en `localStorage`, de la surface de scan reseau authentifiee et d'un manque de documentation du contrat de securite attendu cote reverse-proxy.
## Perimetre lu
- Documents: `CLAUDE.md`, `AGENTS.md`, `docs/*.md`
- README: `README.md` absent
- Config: `docker-compose.yml`, `backend/Dockerfile`, `frontend/Dockerfile`, `frontend/nginx.conf`, `frontend/vite.config.js`, `backend/requirements.txt`, `frontend/package.json`, `frontend/package-lock.json`
- Backend: `backend/main.py`, `backend/database.py`, `backend/models.py`, `backend/routers/*.py`
- Frontend: `frontend/src/**/*.js`, `frontend/src/**/*.vue`, `frontend/index.html`
- `.env.example`: aucun fichier trouve
## Points positifs
- Les routeurs metier `vlans`, `devices`, `links`, `discovery` sont proteges par `Depends(get_current_user)` dans `backend/main.py:105-108`.
- Les requetes SQL passent par SQLAlchemy ou par `text()` parametre pour les migrations; pas de concatenation SQL dangereuse observee dans les routes CRUD.
- Le ping utilise `subprocess.run([...])` sans `shell=True` dans `backend/routers/discovery.py:42-46`, ce qui limite fortement l'injection de commande shell.
- Les mots de passe sont haches avec bcrypt via passlib dans `backend/routers/auth.py:37`.
- La cle JWT est generee aleatoirement au premier demarrage si `SECRET_KEY` est absent dans `backend/routers/auth.py:20-30`.
- Les liens externes visibles utilisent `rel="noopener"` dans les composants principaux, ce qui reduit le risque de controle de la fenetre ouvrante.
## Constats principaux
### SEC-01 - Bootstrap administrateur `admin/admin` sans changement force
Priorite: P0
OWASP: Top 10 A07 Identification and Authentication Failures, A04 Insecure Design
ASVS L1: authentification, gestion des secrets initiaux
Fichiers:
- `backend/main.py:63-86`
- `CLAUDE.md` / `AGENTS.md` section Authentication
- `docs/backend.md`, `docs/extending.md`
La migration `_migrate_users()` cree automatiquement un compte `admin` avec mot de passe `admin` quand la table `users` est vide. Les documents indiquent explicitement ces identifiants comme mot de passe de depart a changer. Ce comportement est donc intentionnel, mais reste un ecart ASVS si l'application peut etre atteinte avant changement manuel: toute instance nouvellement demarree est accessible avec des identifiants publics jusqu'a cette action.
Impact: compromission complete des donnees d'inventaire, utilisation de la fonctionnalite de scan reseau, modification/suppression d'equipements.
Recommandation: conserver ce bootstrap seulement si l'application force le changement au premier login ou si la documentation exige explicitement une premiere connexion depuis localhost/reseau de confiance avant toute exposition. Variante plus robuste: exiger un mot de passe initial via variable d'environnement, fichier secret Docker, ou assistant de premier demarrage.
### SEC-02 - Pas de limitation de tentatives de connexion
Priorite: P1
OWASP: A07
ASVS L1: protections anti-automatisation de l'authentification
Fichiers:
- `backend/routers/auth.py:72-77`
- `frontend/nginx.conf:1-16`
`POST /api/auth/login` ne limite pas les tentatives par IP, par nom utilisateur ou par fenetre temporelle. Nginx ne definit pas non plus de `limit_req`. Une attaque par devinette de mot de passe devient praticable, surtout pendant la phase de bootstrap avant changement du mot de passe initial.
Recommandation: ajouter une limitation cote API ou Nginx, journaliser les echecs, retourner des erreurs generiques et prevoir un delai progressif ou verrouillage temporaire.
### SEC-03 - Politique de mot de passe insuffisante
Priorite: P1
OWASP: A07
ASVS L1: qualite des secrets d'authentification
Fichiers:
- `backend/routers/auth.py:66-69`
- `backend/routers/auth.py:80-95`
- `frontend/src/components/AccountModal.vue`
`new_password` est accepte s'il est non vide. Aucun minimum de longueur, aucune verification de mot de passe courant compromis/faible, et aucune validation de `new_username` ne sont appliquees cote serveur.
Recommandation: definir une politique minimale cote serveur, par exemple longueur minimale robuste, refus des mots de passe evidents, normalisation/validation du nom utilisateur, messages d'erreur non enumerants.
### SEC-04 - JWT persiste dans `localStorage`
Priorite: P1
OWASP: A02 Cryptographic Failures, A07, A05 Security Misconfiguration
ASVS L1: protection des tokens de session
Fichiers:
- `frontend/src/auth.js:3-13`
- `frontend/src/api.js:6-9`
- `backend/routers/auth.py:41-43`
Le token JWT est stocke dans `localStorage`. En cas de XSS ou d'extension navigateur compromise, le token est directement lisible. Le token dure 7 jours et ne contient pas de version de session permettant d'invalider les anciens tokens apres changement de mot de passe.
Recommandation: preferer un cookie `HttpOnly`, `Secure`, `SameSite`, et ajouter une strategie d'expiration courte + renouvellement ou une version de session cote base. Si le mode bearer est conserve, reduire la duree de vie et ajouter une invalidation apres changement de mot de passe.
### SEC-05 - CORS ouvert a toutes origines dans l'API interne
Priorite: P2
OWASP: A05
ASVS L1: configuration HTTP securisee
Fichiers:
- `backend/main.py:97-102`
- `docs/backend.md` indique que ce choix est intentionnel pour un outil LAN
`allow_origins=["*"]`, `allow_methods=["*"]`, `allow_headers=["*"]` ouvrent l'API a toute origine. Dans le deploiement Compose actuel, le backend n'est pas publie directement et le navigateur passe par le Nginx frontend/reverse-proxy en meme origine, ce qui reduit fortement le risque pratique. Cela reste une configuration permissive a encadrer si un reverse-proxy externe expose `/api/` ou si l'auth evolue vers des cookies.
Recommandation: soit supprimer CORS si l'API reste strictement interne et servie en meme origine par le reverse-proxy, soit configurer une liste d'origines explicites via environnement. Documenter le contrat attendu: backend non expose directement, `/api/` publie uniquement via le reverse-proxy.
### SEC-06 - Surface de scan reseau et SSRF authentifiee
Priorite: P1
OWASP: A10 SSRF, A04 Insecure Design
ASVS L1: validation des cibles reseau et limitation d'abus
Fichiers:
- `backend/routers/discovery.py:22-24`
- `backend/routers/discovery.py:52-60`
- `backend/routers/discovery.py:79-86`
- `backend/routers/discovery.py:89-129`
- `docker-compose.yml:4-5`
Les endpoints `/api/discovery/scan` et `/api/discovery/ping` permettent a tout utilisateur authentifie de provoquer des connexions ICMP et DNS depuis le conteneur backend vers des cibles fournies par le client. `scan` limite chaque reseau a 1024 hotes, mais ne limite pas le nombre total de targets. `ping` n'a pas de limite de taille de liste ni de validation IP. Le serveur DNS est librement fourni.
Impact: cartographie de reseaux accessibles depuis le conteneur, charge CPU/process, requetes DNS vers des serveurs arbitraires, usage comme outil de reconnaissance interne apres compromission d'un compte.
Recommandation: valider toutes les IP/CIDR, limiter le nombre total d'adresses par requete, limiter la frequence, autoriser uniquement des plages configurees dans l'inventaire, restreindre les DNS autorises ou utiliser le resolv.conf du conteneur.
### SEC-07 - Validation d'entree insuffisante sur les modeles metier
Priorite: P1
OWASP: A03 Injection, A04, A05
ASVS L1: validation cote serveur
Fichiers:
- `backend/routers/devices.py:11-33`
- `backend/routers/vlans.py:11-15`
- `backend/routers/links.py:11-15`
- `frontend/src/components/DeviceManager.vue:137`
- `frontend/src/components/TopologyGraph.vue:35,56,104,142`
Les schemas Pydantic acceptent des chaines libres sans bornes de longueur ni enums serveur. Exemples: `Device.type`, `virt_type`, `url`, `Vlan.cidr`, `Vlan.color`, `Link.link_type`, noms/descriptions/interfaces. Vue echappe correctement les interpolations texte, mais les URL sont reinjectees en `href` et l'API peut recevoir des valeurs qui ne passent pas par le formulaire HTML `type="url"`.
Recommandation: ajouter des contraintes Pydantic strictes: longueurs maximales, enums pour types, validation IP/CIDR, validation couleur hex, URL seulement `http`/`https`, normalisation des chaines. Ajouter `rel="noreferrer noopener"` sur les liens externes.
### SEC-08 - En-tetes de securite HTTP absents de la configuration fournie
Priorite: P2
OWASP: A05
ASVS L1: durcissement navigateur
Fichiers:
- `frontend/nginx.conf:1-16`
- `frontend/index.html`
Le Nginx embarque ne configure pas d'en-tetes de securite: CSP, `X-Frame-Options` ou `frame-ancestors`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`. Si un reverse-proxy frontal ajoute deja ces en-tetes, le risque est traite au niveau infrastructure; sinon, la configuration fournie reste incomplete.
Recommandation: fournir une configuration de reference pour le reverse-proxy frontal, ou ajouter ces en-tetes dans `frontend/nginx.conf`. La decision doit eviter les doublons contradictoires et etre documentee.
### SEC-09 - TLS delegue au reverse-proxy, contrat non documente
Priorite: P2
OWASP: A02, A05
ASVS L1: protection des identifiants en transit
Fichiers:
- `docker-compose.yml:14-15`
- `frontend/nginx.conf:1-2`
- `docs/architecture.md`
La configuration applicative expose HTTP sur `localhost:8080`. C'est coherent avec un service local derriere reverse-proxy, a condition que le reverse-proxy frontal termine TLS pour les acces non strictement locaux et que le backend/frontend Compose ne soient pas exposes directement sur un reseau non fiable.
Recommandation: documenter clairement que TLS est une responsabilite du reverse-proxy frontal, fournir un exemple de configuration attendue et recommander un bind local (`127.0.0.1:8080:80`) lorsque l'application est consommee uniquement par le proxy local.
### SEC-10 - Secret JWT persiste dans un bind mount projet, permissions faibles
Priorite: P1
OWASP: A02, A05
ASVS L1: gestion des secrets
Fichiers:
- `backend/routers/auth.py:17-30`
- `docker-compose.yml:6-7`
- `docs/architecture.md`
- `db_data/secret_key.txt` present localement, permissions observees `0644`
La cle JWT est stockee dans `./db_data/secret_key.txt`, dans l'arborescence projet, avec permissions lisibles par d'autres utilisateurs locaux. Elle n'a pas ete lue pendant l'audit. Si cette cle fuit, des JWT valides peuvent etre forges jusqu'a rotation.
Recommandation: utiliser `SECRET_KEY` depuis un secret Docker ou un fichier hors depot, fixer des permissions strictes, ajouter `db_data/` et fichiers secrets a l'exclusion VCS si necessaire, documenter la rotation.
### SEC-11 - Conteneurs peu durcis
Priorite: P2
OWASP: A05
ASVS L1: configuration de plateforme
Fichiers:
- `backend/Dockerfile:1-8`
- `frontend/Dockerfile:8-11`
- `docker-compose.yml:1-26`
Les images ne definissent pas d'utilisateur non-root explicite. Compose n'ajoute pas `read_only`, `security_opt: no-new-privileges:true`, `cap_drop`, limites de ressources, healthchecks ou contraintes de filesystem. Le backend ajoute `NET_RAW`, necessaire au ping, mais sans reduction des autres capacites.
Recommandation: executer les services avec utilisateur non-root, ajouter `cap_drop: [ALL]` puis `cap_add: [NET_RAW]` seulement pour backend, activer `no-new-privileges`, limiter ressources et rendre les FS readonly avec volumes temporaires explicites.
### SEC-12 - Pas de journalisation securite/audit
Priorite: P2
OWASP: A09 Security Logging and Monitoring Failures
ASVS L1: evenements de securite
Fichiers:
- `backend/routers/auth.py`
- `backend/routers/devices.py`
- `backend/routers/vlans.py`
- `backend/routers/discovery.py`
Les evenements sensibles ne sont pas journalises de maniere structuree: login reussi/echec, changement de compte, scans reseau, imports, suppressions. En cas d'incident, l'attribution et l'analyse seront limitees.
Recommandation: ajouter des logs structures sans secrets, incluant utilisateur, endpoint, action, resultat, compteurs et IP client issue du proxy de confiance.
### SEC-13 - Integrite relationnelle SQLite non enforcee
Priorite: P2
OWASP: A04, A08
ASVS L1: integrite des donnees
Fichiers:
- `backend/database.py:6-9`
- `backend/models.py`
- `docs/data-model.md` signale explicitement que les FK SQLite ne sont pas enforcees
Les `ForeignKey` SQLAlchemy ne suffisent pas sous SQLite sans `PRAGMA foreign_keys=ON` par connexion. L'application compense partiellement dans les routeurs, mais des incoherences restent possibles, par exemple via evolutions futures ou migrations.
Recommandation: activer le pragma via event SQLAlchemy, verifier les comportements de suppression, ajouter des tests d'integrite.
### SEC-14 - Gestion des dependances et supply chain incomplete
Priorite: P2
OWASP: A06 Vulnerable and Outdated Components, A08 Software and Data Integrity Failures
ASVS L1: composants connus et maintenus
Fichiers:
- `backend/requirements.txt`
- `frontend/package.json:9-19`
- `frontend/package-lock.json`
- `frontend/Dockerfile:3-6`
Les dependances sont epinglees cote Python, mais il n'y a pas de workflow SCA documente. Cote frontend, `package.json` utilise des ranges `^`, et le Dockerfile lance `npm install` au lieu de `npm ci`, ce qui reduit la reproductibilite. `cytoscape` reste declare alors que les notes disent que la topologie n'utilise plus Cytoscape.
Recommandation: utiliser `npm ci`, supprimer les dependances inutilisees, ajouter `pip-audit`/`npm audit` ou equivalent CI, documenter la procedure d'upgrade passlib/bcrypt.
### SEC-15 - Import JSON sans schema ni limite de taille
Priorite: P2
OWASP: A04, A05
ASVS L1: validation des donnees importees
Fichiers:
- `frontend/src/App.vue:191-208`
- routes CRUD backend appelees ensuite
L'import JSON parse tout le fichier cote navigateur, puis cree des VLANs/devices en boucle sans validation de schema dedie ni limite de taille/quantite. Les erreurs de creation sont ignorees avec `.catch(() => {})`, ce qui peut masquer des imports partiels ou incoherents.
Recommandation: definir un schema d'import, limiter taille et nombre d'elements, afficher un bilan d'erreurs, et s'appuyer sur les validations serveur renforcees.
### SEC-16 - `v-html` inutile dans la navigation
Priorite: P3
OWASP: A03 XSS, defense in depth
ASVS L1: sortie encodee
Fichiers:
- `frontend/src/App.vue:24`
- `frontend/src/App.vue` tableau `tabs`
`v-html` est utilise pour afficher des icones de navigation depuis des constantes locales. Le risque actuel est faible car les valeurs ne viennent pas de l'utilisateur, mais l'usage de HTML injecte est inutile et peut devenir dangereux lors d'une future evolution.
Recommandation: remplacer par du texte, des composants d'icones ou une interpolation normale.
### SEC-17 - Endpoint health public minimal
Priorite: P3
OWASP: A05
ASVS L1: exposition minimale
Fichiers:
- `backend/main.py:111-113`
- `docs/backend.md`
`GET /api/health` est public. Dans un modele local derriere reverse-proxy, c'est acceptable pour un healthcheck minimal. Il ne divulgue que `{"status":"ok"}`; risque faible. Il doit simplement rester minimal et ne pas exposer version, configuration ou details d'erreur.
Recommandation: conserver public si necessaire au healthcheck, documenter cette decision, et s'assurer que le reverse-proxy ne l'expose pas avec plus d'information que necessaire.
### SEC-18 - Documentation de securite et configuration d'environnement incompletes
Priorite: P2
OWASP: A05, A04
ASVS L1: configuration securisee documentee
Fichiers:
- `README.md` absent
- aucun `.env.example`
- `CLAUDE.md`, `AGENTS.md`, `docs/*.md`
Les fichiers de notes de developpement sont riches, mais il manque un README utilisateur et un `.env.example` pour documenter `SECRET_KEY`, origines CORS si conservees, URL publique du reverse-proxy, modele TLS delegue, creation du compte initial et durcissement production/local.
Recommandation: ajouter une documentation securite minimale et un exemple d'environnement sans secrets reels.
## Ecarts avec CLAUDE.md / AGENTS.md
- `README.md` est absent alors que le perimetre demande sa revue. Les informations equivalentes sont dans `CLAUDE.md`, `AGENTS.md` et `docs/`.
- Aucun `.env.example` n'est present, alors que `SECRET_KEY` est supporte par le code.
- `CLAUDE.md` et `AGENTS.md` documentent le compte de bootstrap `admin/admin`; ce n'est pas un ecart fonctionnel, mais cela doit etre accompagne d'un changement force ou d'une consigne de non-exposition avant changement pour satisfaire une posture ASVS L1.
- `docs/backend.md` indique que CORS ouvert est intentionnel pour un outil LAN. Avec un backend interne derriere reverse-proxy, le risque est reduit; il faut surtout documenter que le backend ne doit pas etre publie directement.
- `AGENTS.md` indique que les routeurs sont proteges; c'est vrai pour les routeurs metier. `GET /api/health` reste public et doit rester explicitement assume.
- Les notes disent "no Cytoscape", mais `frontend/package.json` garde `cytoscape` dans les dependances.
## Couverture OWASP Top 10
| Categorie | Evaluation |
|---|---|
| A01 Broken Access Control | Pas de multi-utilisateur/role. Routeurs metier proteges. Risque residuel faible a moyen via compte compromis. |
| A02 Cryptographic Failures | JWT en localStorage, TLS delegue au reverse-proxy a documenter, secret en bind mount lisible localement. |
| A03 Injection | SQL/commande correctement limites; validation URL/CSS/strings a renforcer; `v-html` inutile. |
| A04 Insecure Design | Bootstrap `admin/admin` a encadrer, scan reseau authentifie, absence de politique session/password. |
| A05 Security Misconfiguration | CORS permissif pour API interne, headers HTTP a porter par le proxy ou Nginx embarque, conteneurs peu durcis, health public minimal. |
| A06 Vulnerable and Outdated Components | Pas de SCA ni politique d'upgrade; `npm install`; dependance inutilisee. |
| A07 Identification and Authentication Failures | Bootstrap `admin/admin` sans changement force, pas de rate limit, politique password faible. |
| A08 Software and Data Integrity Failures | Build frontend non reproductible strictement; FK SQLite non enforcees; imports JSON peu controles. |
| A09 Logging and Monitoring Failures | Logs securite absents. |
| A10 SSRF | Discovery permet ping/DNS vers cibles fournies par utilisateur authentifie. |
## References
- OWASP ASVS: https://owasp.org/www-project-application-security-verification-standard/
- OWASP Top 10 2021: https://owasp.org/Top10/2021/
-124
View File
@@ -1,124 +0,0 @@
# Security Decisions
Decisions, trade-offs, and documented limitations for Stupid Simple Network Inventory.
---
## JWT in localStorage vs HttpOnly cookie
**Decision**: keep JWT in `localStorage` + Bearer token.
**Rationale**: migrating to HttpOnly cookies requires changes to the Nginx config (cookie proxying, SameSite handling), the FastAPI auth flow, and all frontend API calls. The added complexity is disproportionate for a self-hosted LAN tool. The risk is mitigated by:
- 24-hour token expiry (reduced from 7 days)
- `token_version` invalidation: password change immediately revokes all prior tokens
- The application is designed for trusted LAN use, not public internet exposure
**Assumption**: the deployment is behind a trusted proxy or on a private network where XSS is the primary concern and is partially mitigated by the LAN context.
**Future**: if the app ever needs public internet exposure, migrating to `HttpOnly; Secure; SameSite=Strict` cookies should be the first priority.
---
## Token versioning instead of a session store
**Decision**: `token_version` integer column on `User`, incremented on password change.
**Rationale**: no Redis, no external session store. A single integer per user provides immediate token invalidation with zero dependencies. The version is included in the JWT payload (`ver` field) and validated on every request.
**Trade-off**: revocation is per-user, not per-token. Logging out one device still lets other devices use their tokens until the common password is changed. This is acceptable for a single-user or small-team tool.
**Limitation**: `ver` absent from old tokens is treated as `ver=1` for backward compatibility. Existing valid tokens (before `token_version` was added) will therefore continue to work until the user changes their password.
---
## no-new-privileges absent from backend container
**Decision**: omit `security_opt: no-new-privileges:true` for the backend service.
**Rationale**: the discovery feature uses `subprocess.run(["ping", ...])`. In `iputils-ping` on Debian, the ping binary has the file capability `cap_net_raw=ep`. For a non-root process to execute ping successfully, the `execve` call must be allowed to promote capabilities from the file's permitted set. `no-new-privileges` blocks this promotion, breaking the ping feature.
**Alternatives considered**:
- Ambient capabilities (Linux 4.3+): would require modifying the entrypoint to `prctl(PR_CAP_AMBIENT_RAISE, ...)` before exec. Adds complexity with no meaningful security gain for this threat model.
- SUID ping: `no-new-privileges` also blocks SUID.
- Replace subprocess ping with a Python ICMP implementation: significant refactor out of scope.
**Mitigations in place**: `cap_drop: ALL` + `cap_add: NET_RAW DAC_OVERRIDE` limits the container to only the capabilities needed. `DAC_OVERRIDE` is required because the `db_data/` bind-mount is owned by the host user; root without it cannot create SQLite journal files in that directory. Both capabilities are in Docker's default set, so this is a net reduction from a standard container.
---
## CORS default to `*`
**Decision**: `ALLOWED_ORIGINS` defaults to `"*"` for backward compatibility.
**Rationale**: the application is designed for same-origin access via Nginx (browser → Nginx → FastAPI), so CORS headers are not required for normal operation. However, changing the default to empty (no CORS) would silently break integrations where users access the API from a different origin. The default `"*"` maintains existing behavior while making it configurable.
**Production recommendation**: set `ALLOWED_ORIGINS=` (empty) if the backend is only accessed through the Nginx proxy, or set it to the specific domain if cross-origin access is needed.
---
## In-memory rate limiting
**Decision**: Python `dict[str, list[float]]` protected by `threading.Lock`, no Redis.
**Rationale**: Redis adds operational complexity (another service, volume, potential failure mode). For a single-process Uvicorn deployment behind Nginx, in-memory rate limiting is sufficient. The Nginx `limit_req` module provides the primary protection at the network edge; the Python layer adds defense-in-depth.
**Limitations**:
- State is lost on container restart (acceptable: a restart already breaks all active sessions).
- Does not work across multiple workers. Uvicorn defaults to a single worker; if scaled, the Nginx layer remains effective.
- IP tracking sees the Nginx container's IP (Docker internal), not the real client IP, unless `X-Real-IP` is forwarded to the backend. The Nginx `limit_req` uses the real client IP correctly.
---
## APP_UID / APP_GID build args
**Decision**: configurable container UID/GID via build args, defaulting to 1000.
**Rationale**: the backend bind-mounts `./db_data:/app/data`. For the non-root container user to write to this directory, the container UID must match the host directory owner. On most Linux systems, the first non-root user is UID 1000. The build args allow customisation without Dockerfile changes.
**Alternative**: use an entrypoint that `chown`s the directory at runtime (current implementation via `entrypoint.sh`). This ensures compatibility regardless of UID mismatch, at the cost of a brief root execution before dropping to `appuser`.
---
## nginx-unprivileged for frontend container
**Decision**: use `nginxinc/nginx-unprivileged:alpine` as the frontend base image.
**Rationale**: the standard `nginx:alpine` image requires `CAP_CHOWN` and `CAP_SETUID` at startup — the master process (root) chowns temp directories and forks workers as `nginx` user (UID 101). With `cap_drop: ALL`, both syscalls fail. Two alternative approaches were tried and abandoned:
- `USER nginx` in the Dockerfile: BusyBox `sed` does not support `\s` in basic regex mode, so the PID path substitution silently failed; the entrypoint scripts also need root.
- `gosu appuser` in the backend: `CAP_SETUID` is dropped, so `gosu` cannot switch UIDs.
`nginxinc/nginx-unprivileged:alpine` is maintained by nginx Inc. and pre-configures nginx to run entirely as UID 101 without needing `CAP_CHOWN` or `CAP_SETUID`. The entire process tree (master + workers) runs as nginx, making `cap_drop: ALL` compatible with zero extra capabilities on the frontend.
**Trade-off**: adds a dependency on a third-party image (`nginxinc/nginx-unprivileged`) instead of the official `nginx` image. The image is maintained by nginx Inc. itself, so the trust model is equivalent.
---
## ping and NET_RAW capability
**Decision**: retain `cap_add: NET_RAW` on the backend container.
**Rationale**: the discovery feature (`/api/discovery/ping`, `/api/discovery/scan`) uses ICMP ping, which requires `CAP_NET_RAW`. This is the minimum capability needed and is explicitly documented. The capability is dropped from the frontend container which has no need for it.
---
## SQLite without foreign key enforcement
**Status**: known limitation, not addressed in this phase.
SQLite does not enforce foreign keys by default. Enabling `PRAGMA foreign_keys=ON` per connection via a SQLAlchemy event was out of scope for this phase. The application code manually handles cascades (explicit DELETE before device removal). This is documented in SEC-FIX-013 as a future fix.
---
## Future security improvements (not scheduled)
| ID | Topic |
|----|-------|
| SEC-FIX-006 | Scope discovery scan to inventory CIDRs, validate IPs |
| SEC-FIX-007 | Pydantic field constraints on business models |
| SEC-FIX-008 | HTTP security headers (CSP, X-Content-Type-Options, etc.) |
| SEC-FIX-009 | Document TLS / reverse-proxy contract more explicitly |
| SEC-FIX-012 | Structured audit logging |
| SEC-FIX-013 | SQLite foreign key enforcement |
| SEC-FIX-014 | `npm audit` / `pip-audit` in CI |
| SEC-FIX-015 | Import JSON schema validation and size limits |
| SEC-FIX-016 | Remove `v-html` from App.vue |
-108
View File
@@ -1,108 +0,0 @@
# Security Decisions — Phase 3
Date: 2026-05-06
Ce document enregistre les arbitrages pris lors de la phase 3, les alternatives écartées et les limitations acceptées.
---
## SEC-FIX-006 — Cap global discovery
**Décision** : `MAX_HOSTS_TOTAL = 4096` (4 × /22).
**Alternatives écartées** :
- Cap par nombre de targets : ne bloque pas 1 seul /11 découpé en CIDRs /22.
- Cap plus bas (1024 global) : trop restrictif pour les déploiements multi-VLAN légitimes.
**Limitation** : le cap s'applique à la requête, pas à l'utilisateur ou à la session. Un utilisateur authentifié peut lancer N requêtes consécutives. Mitigation acceptable : l'endpoint est protégé par authentification et nécessite `CAP_NET_RAW`.
---
## SEC-FIX-007 — Validation Pydantic : périmètre choisi
**Décision** : validateurs sur les champs à domaine fini (type, virt_type, couleur, CIDR, IP, URL). Pas de validation sur `description` au-delà d'une longueur max.
**Alternatives écartées** :
- Liste noire de caractères dans `name`/`description` : SQLAlchemy paramétrise toutes les requêtes → pas de risque SQL injection. La liste noire ajouterait une friction sans gain de sécurité.
- Validation de `vlan_id` contre les VLANs existants dans l'interface (unicité déjà enforced en base).
**Limite acceptée** : `name` peut contenir des caractères Unicode arbitraires — ce n'est pas un vecteur d'injection dans ce contexte (pas de rendu HTML côté serveur, pas de shell).
---
## SEC-FIX-008 — Content-Security-Policy
**Décision** : `style-src 'self' 'unsafe-inline'` conservé.
**Pourquoi** : Vue 3 utilise des attributs `style=""` inline (ex: `style="display:none"` pour `v-show`, styles dynamiques sur les chips de couleur VLAN). Ces attributs sont contrôlés par `style-src`. Supprimer `'unsafe-inline'` casserait l'app sans migration vers CSS classes ou nonces.
**Alternative écartée** : nonces CSP pour les styles inline — Vite ne génère pas de nonces en production, nécessiterait un middleware serveur dynamique incompatible avec Nginx statique.
**Décision** : `frame-ancestors 'none'` + `X-Frame-Options: DENY` — redondant mais nécessaire pour les navigateurs qui n'implémentent pas CSP `frame-ancestors`.
---
## SEC-FIX-012 — Logs d'audit : format et activation
**Décision** : JSON one-line via `logging.getLogger("audit")`, stdout.
**Pourquoi** : stdlib uniquement, pas de dépendance (structlog, loguru écartes). Docker collecte stdout. Le format JSON permet le parsing par Loki/ELK sans transformation.
**Activation** : uvicorn hérite de la config logging Python. Pour filtrer uniquement les événements d'audit : `--log-config` ou configuration Python logging dans main.py (non ajouté : hors périmètre de cette phase).
**Limitation** : les logs ne sont pas persistants entre redémarrages sans volume dédié ou système de log externe. Acceptable pour un déploiement self-hosted.
---
## SEC-FIX-013 — PRAGMA foreign_keys=ON
**Décision** : listener sur l'événement `connect` de SQLAlchemy — s'applique à toutes les connexions, y compris les connexions de test.
**Attention** : la migration `_migrate_vlan_nullable()` dans `main.py` désactive temporairement `PRAGMA foreign_keys=OFF` pour recréer la table vlans. Elle le réactive ensuite. Ce pattern est correct car le listener ne s'applique qu'à la connexion DBAPI sous-jacente, pas aux connexions SQLAlchemy qui wrappent.
**Conséquence sur SEC-FIX-017** : avec `foreign_keys=ON`, la table `links` (qui référençait `devices`) aurait bloqué les `DELETE` sur les équipements. C'est la raison pour laquelle `_migrate_drop_links_table()` est exécutée au démarrage.
---
## SEC-FIX-015 — Limite import JSON côté frontend uniquement
**Décision** : limite de taille appliquée uniquement côté frontend (JavaScript, avant `file.text()`).
**Limitation connue** : cette protection est bypassable par un appel direct à l'API. Cependant :
- L'endpoint d'import JSON (`/api/devices/`, `/api/vlans/`) est protégé par authentification.
- FastAPI/uvicorn a une limite de corps par défaut (1 Mo via Starlette). Pour les requêtes individuelles, la validation Pydantic rejette les données invalides.
- La protection frontend couvre 99% des cas d'usage (import via UI).
**Alternative écartée** : limite de taille côté Nginx (`client_max_body_size`) — le scan de découverte et l'import sont deux flux différents ; limiter globalement pourrait bloquer des scans légitimes. Une limite fine par endpoint nécessiterait une configuration Nginx complexe.
---
## SEC-FIX-016 — v-html
**Décision** : les icônes de navigation (■ ◆ ▣) sont des caractères Unicode hardcodés dans le `computed`. Pas de source externe, pas de traduction, pas d'interpolation.
**Risque résiduel** : nul — les caractères Unicode passent par `{{ }}` qui échappe automatiquement.
---
## SEC-FIX-017 — Table `links` : DROP vs conservation
**Décision** : DROP de la table `links` via `_migrate_drop_links_table()` au démarrage.
**Pourquoi** : avec `PRAGMA foreign_keys=ON` (SEC-FIX-013), la table `links` (FK → `devices.id` sans ON DELETE CASCADE) aurait bloqué toute suppression d'équipement ayant des liens. Conserver la table sans le code applicatif pour la maintenir est une dette technique et un risque opérationnel.
**Données perdues** : les liens existants en base sont supprimés de façon irréversible. Acceptable car la fonctionnalité est retirée de l'interface — les données ne sont plus accessibles ni maintenables.
**Alternative écartée** : ajouter ON DELETE CASCADE sur la table existante. Aurait préservé les données mais complexifié la migration SQLite (pas d'ALTER CONSTRAINT) et laissé une table orpheline indéfiniment.
---
## Points restants non traités en Phase 3
| ID | Raison du report |
|----|-----------------|
| SEC-FIX-009 | Couvert : documentation TLS/HTTPS dans README (phases 2/3) |
| SEC-FIX-014 | Couvert : cytoscape supprimé en phase 2/3 |
| Audit log configuration fine | Hors périmètre — nécessite une décision d'infrastructure (Loki, ELK, etc.) |
| `no-new-privileges` backend | Contrainte technique inhérente à `cap_net_raw=ep` pour ping (documenté) |
| Rate limiting multi-worker | Mitigation en place via Nginx `limit_req` |
-128
View File
@@ -1,128 +0,0 @@
# Security Fixes Applied
Date: 2026-05-06
## P0-001 — SEC-FIX-001 : Changement de mot de passe obligatoire au premier login
### Problème
Le compte admin par défaut utilisait `admin`/`admin`. N'importe qui ayant accès au port 8080 pouvait se connecter.
### Changements
**`backend/models.py`**
- Colonne `must_change_password BOOLEAN NOT NULL DEFAULT 0` ajoutée sur `User`.
**`backend/main.py`**
- Nouvelle migration idempotente `_migrate_users_must_change_password()``ALTER TABLE users ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT 0` si absente.
- `_migrate_users()` : support de la variable d'environnement `INITIAL_ADMIN_PASSWORD`.
- Si définie → mot de passe fourni, `must_change_password = 0`.
- Sinon → mot de passe `admin`, `must_change_password = 1` (changement forcé au premier login).
- Routeurs `vlans`, `devices`, `links`, `discovery` protégés par `require_password_changed` au lieu de `get_current_user`.
**`backend/routers/auth.py`**
- Dépendance `require_password_changed` : retourne 403 `"Password change required"` si `must_change_password = 1`.
- `TokenOut` enrichi du champ `must_change_password: bool`.
- `login()` retourne `must_change_password`.
- `update_account()` passe `must_change_password` à `False` après changement de mot de passe réussi.
- `GET /me` retourne `must_change_password`.
**`frontend/src/auth.js`**
- Ref `_mustChange` persistée dans `localStorage('auth_mustchange')`.
- `mustChangePassword` computed exporté.
- `setAuth(token, username, mustChange)` / `clearAuth()` mis à jour.
**`frontend/src/App.vue`**
- Branche `v-else-if="mustChangePassword"` : affiche `<AccountModal :forced="true">` avant l'application.
- `onLogin` et `onAccountUpdated` propagent le flag ; `loadAll` n'est appelé que si `mustChangePassword` est faux.
- `onMounted` conditionnel sur `!mustChangePassword.value`.
**`frontend/src/components/AccountModal.vue`**
- Prop `forced` (Boolean, défaut `false`) :
- Masque le bouton ✕ et le bouton Annuler.
- Empêche la fermeture par clic sur l'overlay.
- Affiche une bannière d'avertissement.
- Rend le champ "Nouveau mot de passe" obligatoire.
**`frontend/src/i18n.js`**
- Clés ajoutées (fr/en/es) : `mustChangePasswordWarning`, `newPasswordRequired`.
### Utilisation
```yaml
# docker-compose.yml — optionnel
environment:
- INITIAL_ADMIN_PASSWORD=MonMotDePasseSecurisé
```
Sans cette variable, le premier login avec `admin`/`admin` force immédiatement le changement de mot de passe.
---
## P1-001 — SEC-FIX-002 : Rate limiting sur POST /api/auth/login
### Problème
Aucune limitation du nombre de tentatives de connexion — brute-force possible.
### Changements
**`frontend/rate_limit.conf`** (nouveau)
- `limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;`
**`frontend/Dockerfile`**
- `COPY rate_limit.conf /etc/nginx/conf.d/00_rate_limit.conf` — chargé en premier (ordre alphabétique).
**`frontend/nginx.conf`**
- Location spécifique `= /api/auth/login` avec `limit_req zone=login burst=5 nodelay` et `limit_req_status 429`.
**`backend/routers/auth.py`**
- Rate limiting in-memory (sans dépendance externe) :
- **Par IP** : 20 tentatives / 60 s
- **Par username** : 10 tentatives / 900 s (15 min)
- Compteurs réinitialisés après login réussi.
- `login()` extrait l'IP via `Request.client.host`.
- Retourne HTTP 429 `"Too many attempts, try again later"`.
**`frontend/src/components/LoginPage.vue`**
- Gère le status 429 → affiche `t('tooManyAttempts')`.
**`frontend/src/i18n.js`**
- Clé ajoutée : `tooManyAttempts`.
---
## P1-002 — SEC-FIX-003 : Validation serveur des entrées sur PUT /api/auth/account
### Problème
Aucune validation sur `new_username` et `new_password` — mots de passe vides ou noms d'utilisateurs invalides acceptés.
### Changements
**`backend/routers/auth.py`**
- `_validate_new_password(password)` : min 8 caractères, au moins une lettre et un chiffre. Lève 400 avec code `password_too_short` ou `password_too_weak`.
- `_validate_new_username(username)` : `[a-zA-Z0-9._-]{1,64}`. Lève 400 avec code `username_invalid`.
- `update_account()` appelle ces fonctions avant modification.
**`frontend/src/components/AccountModal.vue`**
- Validation client miroir (évite un aller-retour réseau pour les cas évidents).
- Mapping des codes d'erreur backend → clés i18n : `password_too_short`, `password_too_weak`, `username_invalid`.
**`frontend/src/i18n.js`**
- Clés ajoutées (fr/en/es) : `passwordTooShort`, `passwordTooWeak`, `usernameInvalid`.
---
## Fichiers modifiés
| Fichier | Fixes |
|---------|-------|
| `backend/models.py` | P0-001 |
| `backend/main.py` | P0-001 |
| `backend/routers/auth.py` | P0-001, P1-001, P1-002 |
| `frontend/rate_limit.conf` | P1-001 (nouveau) |
| `frontend/Dockerfile` | P1-001 |
| `frontend/nginx.conf` | P1-001 |
| `frontend/src/auth.js` | P0-001 |
| `frontend/src/i18n.js` | P0-001, P1-001, P1-002 |
| `frontend/src/App.vue` | P0-001 |
| `frontend/src/components/LoginPage.vue` | P0-001, P1-001 |
| `frontend/src/components/AccountModal.vue` | P0-001, P1-002 |
-210
View File
@@ -1,210 +0,0 @@
# Security Fixes Applied — Phase 2
Date: 2026-05-06
## SEC-FIX-001 — Bootstrap admin et rattrapage des bases existantes
### Problème résiduel (après phase 1)
La migration `_migrate_users_must_change_password()` ajoutait la colonne avec `DEFAULT 0`. Un compte admin existant avec le mot de passe par défaut "admin" n'était donc pas forcé à changer.
### Corrections appliquées
**`backend/main.py`**
- Nouvelle migration `_migrate_users_token_version()` — ajout de `token_version INTEGER NOT NULL DEFAULT 1` si absente.
- Nouvelle migration `_migrate_force_admin_password_change()` : vérifie si l'admin a `must_change_password=0` et si son hash correspond au mot de passe "admin". Si oui, pose `must_change_password=1`. Idempotent, s'exécute à chaque démarrage.
- `_migrate_users()` : mise à jour pour inclure `token_version` dans le CREATE TABLE initial.
- Ordre de démarrage mis à jour : `_migrate_users_token_version()` puis `_migrate_force_admin_password_change()` avant `_migrate_users()`.
**`backend/tests/conftest.py`** (nouveau)
- Configure `DATABASE_URL` vers une base temporaire avant tout import applicatif.
- `SECRET_KEY` fixé pour les tests.
**`backend/tests/test_auth.py`** (nouveau)
- Tests SEC-FIX-001 : base vide (must_change=1), CRUD bloqué avant changement, CRUD autorisé après, rattrapage admin existant, admin avec mot de passe personnalisé non touché, `INITIAL_ADMIN_PASSWORD`.
- Tests SEC-FIX-004 : ancien token rejeté après changement, nouveau token valide, backward compat token sans `ver`, token avec mauvaise version.
- Tests SEC-FIX-003 : validation mot de passe (trop court, sans chiffre, sans lettre, valide), username (invalide, trop long, valide), mauvais mot de passe courant.
- Tests SEC-FIX-002 : rate limit IP (429), rate limit username (429), reset après login réussi.
**`backend/requirements-test.txt`** (nouveau)
- `pytest>=7.4`, `httpx>=0.25`.
---
## SEC-FIX-004 — Invalidation de session après changement de mot de passe
### Problème
JWT valable 7 jours, pas d'invalidation après changement de mot de passe.
### Corrections appliquées
**`backend/models.py`**
- Colonne `token_version = Column(Integer, nullable=False, default=1, server_default="1")` ajoutée à `User`.
**`backend/routers/auth.py`**
- `TOKEN_EXPIRE_HOURS = 24` (réduit de 7 jours à 24 heures).
- `create_token(username, version)` : payload inclut `{ ver: version }`.
- `get_current_user` : vérifie `payload["ver"] == user.token_version`. Rejette avec 401 si différent. Rétrocompatibilité : `ver` absent dans le payload est traité comme `ver=1`.
- `update_account` : incrémente `current_user.token_version` lors d'un changement de mot de passe.
- `login` : transmet `user.token_version` au `create_token`.
---
## SEC-FIX-005 — CORS configurable
### Problème
`allow_origins=["*"]` hardcodé.
### Corrections appliquées
**`backend/main.py`**
- Lecture de `ALLOWED_ORIGINS` env var (défaut `"*"` pour rétrocompatibilité).
- `""` désactive le middleware CORS (même origine via proxy).
- Valeur CSV : liste d'origines explicites.
**`.env.example`** (nouveau)
- Documentation de `ALLOWED_ORIGINS` avec exemples.
---
## SEC-FIX-010 — Sécurisation du secret JWT
### Problème
`secret_key.txt` créé sans permissions restrictives. Pas de `.gitignore`.
### Corrections appliquées
**`backend/routers/auth.py`**
- Création du fichier secret via `os.open(..., os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)` pour imposer des permissions 0600 dès la création (owner read/write uniquement).
**`.gitignore`** (nouveau)
- Ignore `db_data/`, `*.db`, `.env`, `__pycache__/`, `node_modules/`, etc.
---
## SEC-FIX-011 — Durcissement des conteneurs
### Problème
Conteneurs lancés en root sans contraintes de capabilities ni healthchecks.
### Corrections appliquées (Phase 2 initiale)
**`frontend/Dockerfile`**
- `npm install``npm ci` (build reproductible, respecte le lockfile).
- Image de base finale : `nginxinc/nginx-unprivileged:alpine` — nginx s'exécute entièrement en UID 101 sans nécessiter `CAP_CHOWN` ni `CAP_SETUID`. Compatible avec `cap_drop: ALL`.
- `EXPOSE 8080` (port non-privilégié, correspondant à la config nginx).
**`frontend/nginx.conf`**
- `listen 80``listen 8080` (port non-privilégié, ne nécessite pas CAP_NET_BIND_SERVICE).
**`docker-compose.yml`**
- `frontend` : `cap_drop: ALL`, `security_opt: no-new-privileges:true`, port `8080:8080`, healthcheck wget, `depends_on: backend: condition: service_healthy`.
- Suppression de la section `volumes: db_data:` (bind mount, pas de volume nommé).
**Approches abandonnées (Phase 2 initiale)**
- `gosu` + `appuser` pour le backend : `cap_drop: ALL` supprime `CAP_SETUID``gosu` ne peut pas changer d'utilisateur depuis le processus uvicorn après démarrage.
### Correction complémentaire — backend non-root via `user:` Compose
**Problème résiduel** : le backend tournait en UID 0 (root) faute de mécanisme interne pour changer d'utilisateur. La capability `DAC_OVERRIDE` avait été ajoutée comme contournement (accès en écriture au bind-mount), mais les fichiers `db_data/` étaient créés root:root sur l'hôte.
**Solution** : la directive `user:` de Docker Compose démarre le processus directement sous l'UID/GID cible, sans nécessiter `CAP_SETUID` ni gosu.
**`docker-compose.yml`**
- `backend` : ajout de `user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"`.
- `cap_add` : suppression de `DAC_OVERRIDE` (inutile — le processus est propriétaire du bind-mount).
- `cap_add` conserve `NET_RAW` (ICMP ping).
- `no-new-privileges` reste omis : ping utilise les file capabilities (`cap_net_raw=ep`) ; `no-new-privileges` supprime le bit effective du fichier, empêchant le sous-processus d'acquérir `CAP_NET_RAW` dans son ensemble effectif même si le parent le détient dans son ensemble permis.
**`.env.example`**
- `DOCKER_UID` / `DOCKER_GID` documentés avec procédure `id -u && id -g` et instruction `mkdir -p db_data`.
**`README.md`**
- Quick start : `docker compose --env-file .env up --build -d`.
- Configuration : `APP_UID`/`APP_GID` remplacés par `DOCKER_UID`/`DOCKER_GID`.
- Container hardening : table mise à jour (`DOCKER_UID:DOCKER_GID`), note `no-new-privileges` précisée.
---
## SEC-FIX-018 — Documentation
### Corrections appliquées
**`README.md`** (nouveau)
- Quick start, configuration, sécurité (HTTPS, rotation de clé, durcissement conteneurs), persistance des données, développement.
**`.env.example`** (nouveau)
- `SECRET_KEY`, `INITIAL_ADMIN_PASSWORD`, `ALLOWED_ORIGINS`, `APP_UID`/`APP_GID` documentés avec exemples.
**`docs/backend.md`**
- Routeurs protégés : `get_current_user``require_password_changed`.
- Contrat login : ajout `must_change_password`.
- Contrat account : codes d'erreur normalisés.
- Modèle `User` : ajout `must_change_password`, `token_version`.
- Section Auth : mise à jour expiry, payload, invalidation, rate limiting.
- Section Migrations : ordre complet, description `_migrate_force_admin_password_change`.
**`docs/architecture.md`**
- Section Auth : schéma mis à jour (`mustChangePassword`, `AccountModal :forced`).
- Section Docker Compose : non-root, cap_drop, suppression volume nommé.
**`CLAUDE.md`**
- Section Authentification : 24h, `token_version`, `must_change_password`, guard App.vue, `INITIAL_ADMIN_PASSWORD`.
- Section Migrations : liste complète des 7 migrations.
**`AGENTS.md`**
- Section Authentication : réécriture complète.
- Data Model : `User` mis à jour.
---
## Fichiers modifiés
| Fichier | Fixes |
|---------|-------|
| `backend/database.py` | Support `DATABASE_URL` pour tests |
| `backend/models.py` | SEC-FIX-004 (`token_version`) |
| `backend/main.py` | SEC-FIX-001 (rattrapage), SEC-FIX-004 (migration), SEC-FIX-005 (CORS) |
| `backend/routers/auth.py` | SEC-FIX-004 (versioning), SEC-FIX-010 (0600), expiry 24h |
| `backend/Dockerfile` | SEC-FIX-011 (cap_drop, DAC_OVERRIDE) |
| `backend/requirements-test.txt` | Tests (nouveau) |
| `backend/tests/__init__.py` | Tests (nouveau) |
| `backend/tests/conftest.py` | Tests (nouveau) |
| `backend/tests/test_auth.py` | Tests SEC-FIX-001, 002, 003, 004 (nouveau) |
| `frontend/Dockerfile` | SEC-FIX-011 (nginxinc/nginx-unprivileged, npm ci) |
| `frontend/nginx.conf` | SEC-FIX-011 (port 8080) |
| `docker-compose.yml` | SEC-FIX-011 (cap_drop, healthchecks) |
| `.gitignore` | SEC-FIX-010 (nouveau) |
| `.env.example` | SEC-FIX-005, SEC-FIX-010, SEC-FIX-018 (nouveau) |
| `README.md` | SEC-FIX-018 (nouveau) |
| `docs/backend.md` | SEC-FIX-018 |
| `docs/architecture.md` | SEC-FIX-018 |
| `CLAUDE.md` | SEC-FIX-018 |
| `AGENTS.md` | SEC-FIX-018 |
---
## Risques couverts
| Risque | Avant | Après |
|--------|-------|-------|
| Bootstrap admin/admin sur instance existante | Non corrigé | `_migrate_force_admin_password_change()` force le changement au prochain démarrage |
| JWT valable 7 jours après compromission | Oui | 24h + invalidation immédiate par `token_version` |
| CORS permissif | Hardcodé `*` | Configurable via `ALLOWED_ORIGINS`, défaut `*` rétrocompatible |
| Secret JWT lisible (permissions) | Mode par défaut (0644 ou plus) | 0600 dès la création |
| Secrets dans le dépôt | Pas de `.gitignore` | `db_data/`, `.env`, `*.db` ignorés |
| Process frontend root | Oui (nginx master + workers en root) | `nginxinc/nginx-unprivileged` — tout le process tree en UID 101 |
| Process backend root | Oui | `user: DOCKER_UID:DOCKER_GID` — process = utilisateur hôte, fichiers `db_data/` non root-owned |
| Capabilities superflues | Aucun cap_drop | `cap_drop: ALL` sur les deux conteneurs ; backend ajoute `NET_RAW` uniquement |
| `DAC_OVERRIDE` exposée | Oui (contournement root) | Supprimée — inutile avec non-root propriétaire du bind-mount |
| Pas de healthchecks | Aucun | Ajoutés sur les deux services |
| Build frontend non reproductible | `npm install` | `npm ci` |
---
## Limitations restantes
- JWT stocké en `localStorage` (pas de cookie HttpOnly) — mitigation : expiry 24h + token versioning.
- Rate limiting backend en mémoire (perdu au redémarrage, ne se partage pas entre workers) — mitigation : Nginx `limit_req` couvre le cas multi-worker.
- `no-new-privileges` absent du backend : `no-new-privileges` supprime le bit effective des file capabilities ; le sous-processus ping ne peut pas acquérir `CAP_NET_RAW` dans son ensemble effectif même si le parent le détient en permis. Mitigation : le processus tourne désormais sous un UID non-privilégié.
- Politique de mot de passe minimale (`password1` passe) — pas de liste noire de mots de passe communs.
- Pas de logs structurés d'audit (SEC-FIX-012 non traité dans cette phase).
-199
View File
@@ -1,199 +0,0 @@
# 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 |
-183
View File
@@ -1,183 +0,0 @@
# Plan d'action securite pour Claude Code
Objectif: corriger les ecarts de securite de base identifies dans `SECURITY_AUDIT_BASE.md`, sans refonte fonctionnelle inutile. Chaque tache est atomique et doit etre livree avec tests.
## SEC-FIX-001
Priorite: P0
Fichier(s) a modifier: `backend/main.py`, `backend/routers/auth.py`, `docker-compose.yml`, `.env.example`, `CLAUDE.md`, `AGENTS.md`, `docs/backend.md`, `docs/extending.md`
Probleme: compte de bootstrap `admin/admin` cree automatiquement au premier demarrage, sans changement force.
Correction attendue: conserver le bootstrap seulement si un changement de mot de passe est force au premier login. Variante plus robuste: remplacer le mot de passe fixe par une initialisation sure via variable `INITIAL_ADMIN_PASSWORD` ou secret Docker obligatoire si aucun utilisateur n'existe.
Risque couvert: compromission immediate d'une instance neuve.
Critere d'acceptation: une base vide peut creer `admin/admin` uniquement avec un etat `must_change_password`; tant que le mot de passe n'est pas change, seuls les endpoints necessaires au changement de compte sont accessibles. Si l'option `INITIAL_ADMIN_PASSWORD` est retenue, `admin/admin` n'est plus cree.
Tests recommandes: test unitaire de `_migrate_users()`; test premier login `admin/admin` redirige vers changement obligatoire; test acces CRUD refuse avant changement; test acces CRUD autorise apres changement.
## SEC-FIX-002
Priorite: P1
Fichier(s) a modifier: `backend/routers/auth.py`, `backend/rate_limit.py` (nouveau si necessaire), `frontend/nginx.conf`, `backend/requirements.txt`
Probleme: aucune limitation de tentatives sur `/api/auth/login`.
Correction attendue: ajouter une limitation par IP et par username avec fenetre temporelle, ou configurer `limit_req` Nginx plus une protection API. Les erreurs doivent rester generiques.
Risque couvert: bruteforce et credential stuffing.
Critere d'acceptation: apres N echecs rapproches, les tentatives suivantes recoivent une reponse de limitation temporaire; une connexion legitime fonctionne apres expiration de la fenetre.
Tests recommandes: tests API simulant echecs successifs, reset apres fenetre, non-enumeration des comptes.
## SEC-FIX-003
Priorite: P1
Fichier(s) a modifier: `backend/routers/auth.py`, `frontend/src/components/AccountModal.vue`, `frontend/src/i18n.js`
Probleme: `new_password` et `new_username` ne sont pas valides robustement cote serveur.
Correction attendue: imposer une longueur minimale de mot de passe, refuser les mots de passe evidents, borner/normaliser le username, retourner des messages localisables.
Risque couvert: prise de compte par mot de passe faible et donnees auth incoherentes.
Critere d'acceptation: les mots de passe trop courts/faibles sont refuses cote API; le frontend affiche une erreur claire; les usernames invalides sont refuses.
Tests recommandes: tests API pour password vide, court, faible, valide; username vide, trop long, caracteres invalides, doublon.
## SEC-FIX-004
Priorite: P1
Fichier(s) a modifier: `backend/routers/auth.py`, `backend/models.py`, `backend/main.py`, `frontend/src/auth.js`, `frontend/src/api.js`, `frontend/src/components/LoginPage.vue`, `frontend/src/components/AccountModal.vue`, `frontend/nginx.conf`
Probleme: JWT stocke en `localStorage`, duree 7 jours, pas d'invalidation apres changement de mot de passe.
Correction attendue: migrer vers cookie `HttpOnly`, `Secure`, `SameSite=Lax` ou `Strict`; ajouter version de session/token dans la base; invalider les anciens tokens apres changement de mot de passe. Si cookie impossible immediatement, reduire l'expiration et ajouter une version de session.
Risque couvert: vol/rejeu de token apres XSS ou poste compromis.
Critere d'acceptation: le token n'est plus lisible par JavaScript dans le mode cible; un changement de mot de passe invalide les tokens precedents; logout supprime la session/cookie.
Tests recommandes: tests login/me/logout, changement de mot de passe puis ancien token refuse, verification flags `Set-Cookie`.
## SEC-FIX-005
Priorite: P2
Fichier(s) a modifier: `backend/main.py`, `.env.example`, `docker-compose.yml`, `docs/backend.md`
Probleme: CORS ouvert a toutes origines alors que l'application est prevue en local derriere reverse-proxy.
Correction attendue: documenter que le backend reste interne et que le navigateur accede a `/api/` en meme origine via reverse-proxy. Supprimer CORS si inutile, ou ajouter `ALLOWED_ORIGINS` configurable pour les cas ou une origine externe est necessaire.
Risque couvert: publication accidentelle de l'API interne ou configuration dangereuse si l'auth migre vers cookies.
Critere d'acceptation: le mode recommande n'expose pas directement le backend; la doc explique quand CORS est necessaire; si `ALLOWED_ORIGINS` est ajoute, une origine non autorisee ne recoit pas d'en-tetes CORS permissifs.
Tests recommandes: test API avec headers `Origin` autorisee/non autorisee si CORS conserve; verification de la configuration Compose/reverse-proxy documentee.
## SEC-FIX-006
Priorite: P1
Fichier(s) a modifier: `backend/routers/discovery.py`, `frontend/src/components/DiscoveryModal.vue`, `frontend/src/api.js`, `frontend/src/i18n.js`
Probleme: `/scan` et `/ping` acceptent des cibles arbitraires et insuffisamment limitees.
Correction attendue: valider `ips` avec `ipaddress.ip_address`, valider `dns_server`, limiter le nombre total d'IP par requete, limiter le nombre de targets, autoriser uniquement les CIDR presents en base ou explicitement configures, ajouter rate limit.
Risque couvert: SSRF/reconnaissance interne et deni de service par surcharge de scans.
Critere d'acceptation: IP/CIDR/DNS invalides refuses; targets hors perimetre refusees; total d'adresses plafonne; les scans legitimes de VLAN configures fonctionnent.
Tests recommandes: tests API `ping` liste vide, liste trop grande, IP invalide, IP valide; tests `scan` CIDR trop large, target hors inventaire, DNS invalide, cas nominal.
## SEC-FIX-007
Priorite: P1
Fichier(s) a modifier: `backend/routers/devices.py`, `backend/routers/vlans.py`, `backend/routers/links.py`, `frontend/src/components/DeviceManager.vue`, `frontend/src/components/VlanManager.vue`, `frontend/src/components/TopologyGraph.vue`, `frontend/src/i18n.js`
Probleme: schemas metier trop permissifs.
Correction attendue: ajouter contraintes Pydantic v2: `Field(max_length=...)`, enums/Literal pour device type, virt type et link type, validation IP/CIDR, couleur hex, URL `http/https` uniquement. Ajouter `rel="noreferrer noopener"` sur liens externes.
Risque couvert: XSS via URL dangereuse, donnees incoherentes, injection future, DoS par champs enormes.
Critere d'acceptation: l'API refuse les valeurs hors enum, URL non http/https, CIDR/IP/couleur invalides et champs trop longs; l'UI affiche les erreurs.
Tests recommandes: tests API par champ invalide/valide; tests frontend de rendu lien externe avec `noreferrer`.
## SEC-FIX-008
Priorite: P2
Fichier(s) a modifier: `frontend/nginx.conf`, `frontend/index.html`, `frontend/src/App.vue` si ajustements CSP necessaires
Probleme: absence d'en-tetes de securite navigateur dans la configuration fournie; ils peuvent etre portes par le reverse-proxy frontal.
Correction attendue: fournir une configuration de reference reverse-proxy ou ajouter dans `frontend/nginx.conf` une CSP minimale, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `Permissions-Policy`, et protection anti-framing via CSP `frame-ancestors` ou `X-Frame-Options`.
Risque couvert: impact XSS, clickjacking, sniffing MIME, fuite referrer.
Critere d'acceptation: les en-tetes sont presents sur `/` et assets; l'application build fonctionne sans violation CSP critique.
Tests recommandes: `curl -I` sur `/`; test navigateur console sans erreurs CSP bloquantes; build Vite.
## SEC-FIX-009
Priorite: P2
Fichier(s) a modifier: `docker-compose.yml`, `frontend/nginx.conf`, `docs/architecture.md`, `.env.example`
Probleme: TLS est delegue au reverse-proxy, mais le contrat de deploiement n'est pas documente.
Correction attendue: documenter explicitement que le mode actuel est local derriere reverse-proxy; fournir un exemple de deploiement derriere reverse-proxy TLS; recommander le bind `127.0.0.1:8080:80` si seul le proxy local doit consommer l'application.
Risque couvert: interception d'identifiants et tokens sur reseau non fiable.
Critere d'acceptation: la documentation exige HTTPS au niveau reverse-proxy pour tout acces non local; un exemple utilisable indique les variables et headers proxy necessaires; Compose n'encourage pas une exposition directe involontaire.
Tests recommandes: revue doc; test login via reverse proxy TLS si un profil est ajoute.
## SEC-FIX-010
Priorite: P1
Fichier(s) a modifier: `backend/routers/auth.py`, `docker-compose.yml`, `.gitignore`, `.env.example`, `docs/architecture.md`
Probleme: `data/secret_key.txt` est dans un bind mount projet et peut etre lisible localement.
Correction attendue: charger `SECRET_KEY` depuis secret Docker ou env obligatoire en production; creer les fichiers generes avec permissions `0600`; ignorer `db_data/`; documenter rotation de cle.
Risque couvert: fuite de cle JWT et forge de tokens.
Critere d'acceptation: nouveau fichier secret cree en `0600`; `db_data/` ignore; production documentee avec secret externe; absence de secret reel dans exemples.
Tests recommandes: test unitaire ou integration verifiant permissions; verification `.gitignore`; demarrage avec `SECRET_KEY`.
## SEC-FIX-011
Priorite: P2
Fichier(s) a modifier: `backend/Dockerfile`, `frontend/Dockerfile`, `docker-compose.yml`
Probleme: conteneurs lances sans durcissement explicite.
Correction attendue: executer backend et frontend en non-root si compatible, ajouter `cap_drop: [ALL]`, garder `NET_RAW` uniquement backend, `security_opt: no-new-privileges:true`, healthchecks, limites ressources, filesystem readonly avec volumes temporaires necessaires.
Risque couvert: impact accru en cas de compromission de conteneur.
Critere d'acceptation: les services demarrent et fonctionnent avec utilisateur non-root et privileges reduits; ping reste fonctionnel si active.
Tests recommandes: `docker compose up --build -d`, test login/CRUD/ping, verification `docker compose ps`, inspection utilisateur/capabilities.
## SEC-FIX-012
Priorite: P2
Fichier(s) a modifier: `backend/routers/auth.py`, `backend/routers/devices.py`, `backend/routers/vlans.py`, `backend/routers/links.py`, `backend/routers/discovery.py`, `frontend/nginx.conf`
Probleme: absence de logs securite structures.
Correction attendue: journaliser login reussi/echec, changement username/password, scans, imports/creates/deletes, erreurs de validation et rate limits, sans jamais logger mots de passe/tokens.
Risque couvert: detection et investigation insuffisantes.
Critere d'acceptation: chaque evenement sensible produit un log structure avec action, resultat, username, IP client fiable, compteurs utiles.
Tests recommandes: tests API capturant logs; verification absence de secrets dans logs.
## SEC-FIX-013
Priorite: P2
Fichier(s) a modifier: `backend/database.py`, `backend/models.py`, `backend/routers/devices.py`, `backend/routers/vlans.py`, `backend/routers/links.py`
Probleme: SQLite n'enforce pas les foreign keys.
Correction attendue: activer `PRAGMA foreign_keys=ON` sur chaque connexion via event SQLAlchemy; ajuster cascades/ondelete si necessaire.
Risque couvert: incoherence et corruption logique des donnees.
Critere d'acceptation: creation d'une interface avec `device_id` inexistant echoue; suppression VLAN/device conserve le comportement fonctionnel attendu.
Tests recommandes: tests DB d'integrite FK; tests suppression VLAN/device/link.
## SEC-FIX-014
Priorite: P2
Fichier(s) a modifier: `frontend/Dockerfile`, `frontend/package.json`, `frontend/package-lock.json`, `backend/requirements.txt`, documentation CI
Probleme: supply chain incomplete, `npm install`, dependance `cytoscape` inutilisee.
Correction attendue: remplacer `npm install` par `npm ci`; supprimer `cytoscape` si vraiment inutilise; ajouter scripts/documentation pour `npm audit` et audit Python; conserver compatibilite passlib/bcrypt.
Risque couvert: composants vulnerables/obsoletes et builds non reproductibles.
Critere d'acceptation: build Docker frontend utilise le lockfile; dependances inutilisees supprimees; procedure SCA documentee.
Tests recommandes: `docker compose build frontend`; `npm run build`; audit dependances dans CI ou local.
## SEC-FIX-015
Priorite: P2
Fichier(s) a modifier: `frontend/src/App.vue`, `backend/routers/devices.py`, `backend/routers/vlans.py`, `frontend/src/i18n.js`
Probleme: import JSON sans schema, limites ni bilan d'erreurs.
Correction attendue: limiter taille fichier/nombre d'objets, valider structure avant import, afficher un resume des erreurs, ne pas ignorer silencieusement les echecs.
Risque couvert: deni de service local, import incoherent, donnees dangereuses passant par CRUD.
Critere d'acceptation: fichier trop gros refuse; schema invalide refuse; import partiel affiche les erreurs; import valide fonctionne.
Tests recommandes: tests frontend unitaires pour parsing/validation; tests manuels import valide/invalide/gros.
## SEC-FIX-016
Priorite: P3
Fichier(s) a modifier: `frontend/src/App.vue`
Probleme: `v-html` utilise pour des icones statiques de navigation.
Correction attendue: remplacer par interpolation texte ou composants d'icones sans HTML injecte.
Risque couvert: reduction defense-in-depth du risque XSS futur.
Critere d'acceptation: aucun `v-html` ne reste dans `App.vue`; les icones de navigation s'affichent toujours.
Tests recommandes: `rg "v-html" frontend/src`; test visuel rapide.
## SEC-FIX-017
Priorite: P3
Fichier(s) a modifier: `backend/main.py`, `frontend/nginx.conf`, `docker-compose.yml`, `docs/backend.md`
Probleme: `/api/health` est public; c'est acceptable pour un healthcheck local/reverse-proxy mais doit rester assume.
Correction attendue: documenter que l'endpoint est volontairement public et minimal, ou le restreindre au reverse-proxy si souhaite. Ne pas exposer d'informations de version.
Risque couvert: exposition inutile d'informations de disponibilite si le proxy publie trop largement les endpoints internes.
Critere d'acceptation: decision documentee; endpoint ne retourne toujours aucun detail sensible; si restreint, le healthcheck Compose/reverse-proxy fonctionne.
Tests recommandes: `curl /api/health` selon mode attendu; test healthcheck Docker.
## SEC-FIX-018
Priorite: P2
Fichier(s) a modifier: `README.md`, `.env.example`, `CLAUDE.md`, `AGENTS.md`, `docs/architecture.md`, `docs/backend.md`
Probleme: documentation securite et configuration d'environnement incompletes.
Correction attendue: ajouter README et `.env.example` sans secrets reels; documenter `SECRET_KEY`, `INITIAL_ADMIN_PASSWORD`, `ALLOWED_ORIGINS`, TLS, stockage des donnees, rotation de cle, sauvegarde/restauration et profil dev/prod.
Risque couvert: mauvaises configurations deployees par defaut.
Critere d'acceptation: un nouvel operateur peut deployer sans identifiants par defaut ni secret genere dans le depot; les differences dev/prod sont explicites.
Tests recommandes: revue doc; demarrage Compose avec variables de `.env.example` adaptees.
-310
View File
@@ -1,310 +0,0 @@
# Revue securite apres corrections
Date: 2026-05-06
Base de comparaison: `SECURITY_AUDIT_BASE.md`, `SECURITY_FIX_PLAN_FOR_CLAUDE.md`, `SECURITY_FIXES_APPLIED.md` et code actuel.
Mode: revue statique locale, sans exploitation. Aucun fichier applicatif n'a ete modifie.
## Synthese
Les corrections declarees dans `SECURITY_FIXES_APPLIED.md` sont presentes dans le code pour les trois premiers sujets: changement force au premier login, rate limiting du login, validation minimale du changement de compte. Elles reduisent le risque principal de bootstrap et de bruteforce.
La correction SEC-FIX-001 reste partielle pour les installations deja creees avant la migration: l'ajout de colonne met `must_change_password` a `0` par defaut. Dans la base locale observee, l'utilisateur `admin` a `must_change_password=0`. Si ce compte utilise encore l'ancien mot de passe public, il ne sera pas force a changer. La documentation est egalement en retard sur le code: plusieurs fichiers decrivent encore `get_current_user` comme dependance des routeurs metier et ne documentent pas le nouveau flux `must_change_password`.
Verification effectuee:
- Parsing Python par AST: OK, 9 fichiers parses.
- Build frontend Vite vers `/tmp/topologie-vite-build`: OK.
- Aucun test automatise dedie n'a ete trouve dans `backend` ou `frontend`.
## SEC-FIX-001 - Changement de mot de passe obligatoire au premier login
Statut: partiellement corrige
Preuve dans le code:
- `backend/models.py:6-12` ajoute `User.must_change_password`.
- `backend/main.py:78-110` cree `admin` avec `must_change_password=1` si `INITIAL_ADMIN_PASSWORD` est absent, et `0` si la variable est fournie.
- `backend/main.py:130-133` protege les routeurs metier avec `Depends(require_password_changed)`.
- `backend/routers/auth.py:97-100` refuse les routeurs proteges avec 403 si le changement est requis.
- `backend/routers/auth.py:149-172` remet `must_change_password` a `False` apres changement de mot de passe.
- `frontend/src/App.vue:3-11` affiche `AccountModal` forcee avant l'application si `mustChangePassword` est vrai.
- `frontend/src/components/AccountModal.vue:93-97` exige un nouveau mot de passe cote client en mode force.
Risque restant:
- `backend/main.py:63-75` ajoute la colonne aux bases existantes avec `DEFAULT 0`; les comptes deja presents ne sont donc pas forces a changer.
- La base locale contient `admin|0` pour `must_change_password`.
- `docker-compose.yml` ne montre pas `INITIAL_ADMIN_PASSWORD`; `.env.example` est absent.
- `AGENTS.md`, `CLAUDE.md` et `docs/backend.md` decrivent encore l'ancien modele `Depends(get_current_user)` ou l'ancien contrat de login.
Recommandation finale:
- Ajouter une migration de rattrapage explicite pour les anciennes bases, ou documenter une commande de remediation obligatoire.
- Documenter `INITIAL_ADMIN_PASSWORD`, `must_change_password` et `require_password_changed` dans `AGENTS.md`, `CLAUDE.md`, `docs/*` et `.env.example`.
- Ajouter un test couvrant base vide, base existante et acces CRUD avant/apres changement.
## SEC-FIX-002 - Rate limiting sur POST /api/auth/login
Statut: corrige
Preuve dans le code:
- `frontend/rate_limit.conf:1` definit `limit_req_zone`.
- `frontend/Dockerfile:10-11` copie `nginx.conf` et `rate_limit.conf`.
- `frontend/nginx.conf:6-13` applique `limit_req zone=login burst=5 nodelay` et retourne 429 sur `/api/auth/login`.
- `backend/routers/auth.py:43-76` implemente un rate limit in-memory par username et par IP.
- `backend/routers/auth.py:132-146` applique ces controles avant verification du mot de passe.
- `frontend/src/components/LoginPage.vue:69-74` affiche un message dedie sur 429.
Risque restant:
- Le rate limit backend est en memoire: il est perdu au redemarrage et se fragmente avec plusieurs workers/process.
- `Request.client.host` voit souvent l'adresse du proxy Nginx, pas le client final; le controle IP backend peut donc devenir global derriere proxy. Le controle Nginx compense dans le deploiement Compose fourni.
- Les compteurs IP backend ne sont pas remis a zero apres login reussi.
- Aucun test automatise de fenetre, expiration ou non-enumeration n'a ete trouve.
Recommandation finale:
- Conserver le controle Nginx et soit documenter que le backend ne doit pas etre expose directement, soit gerer proprement `X-Forwarded-For` depuis un proxy de confiance.
- Ajouter des tests API pour echecs successifs, expiration de fenetre et succes apres expiration.
## SEC-FIX-003 - Validation serveur des entrees sur PUT /api/auth/account
Statut: partiellement corrige
Preuve dans le code:
- `backend/routers/auth.py:103-117` valide username et mot de passe.
- `backend/routers/auth.py:157-165` applique ces validations avant modification.
- `frontend/src/components/AccountModal.vue:91-117` applique une validation miroir.
- `frontend/src/i18n.js:160-165`, `310-315`, `460-465` contient les messages localises.
Risque restant:
- La politique mot de passe reste minimale: `password1` ou `admin123` respectent la regle lettre+chiffre et longueur 8.
- `AccountUpdate` accepte les champs optionnels sans contraintes Pydantic; la robustesse depend des validations manuelles.
- Les erreurs `Username already taken` et `Current password is incorrect` ne sont pas sous forme de codes stables et restent moins homogenes que les nouveaux codes.
- Aucun test API dedie n'a ete trouve.
Recommandation finale:
- Renforcer la politique avec une liste de mots de passe evidents/refuses et des contraintes Pydantic (`Field`, `StringConstraints`) en plus des helpers.
- Normaliser/trim le username avant comparaison et stockage.
- Ajouter des tests pour mot de passe faible evident, username invalide, doublon et cas valide.
## SEC-FIX-004 - JWT localStorage, expiration et invalidation
Statut: non corrige
Preuve dans le code:
- `frontend/src/auth.js:3-17` stocke toujours le JWT dans `localStorage`.
- `frontend/src/api.js:6-9` continue d'envoyer un bearer token lu par JavaScript.
- `backend/routers/auth.py:38-80` conserve une expiration de 7 jours et un payload `{sub, exp}` sans version de session.
- `backend/routers/auth.py:162-168` change le mot de passe sans invalider les tokens deja emis.
Risque restant:
- Vol/rejeu de JWT en cas de XSS, extension compromise ou poste compromis.
- Ancien token toujours valide apres changement de mot de passe jusqu'a expiration.
Recommandation finale:
- Migrer vers cookie `HttpOnly`, `Secure`, `SameSite` ou, a minima, ajouter `session_version`/`token_version` en base et reduire l'expiration.
## SEC-FIX-005 - CORS configurable ou supprime
Statut: non corrige
Preuve dans le code:
- `backend/main.py:122-127` conserve `allow_origins=["*"]`, `allow_methods=["*"]`, `allow_headers=["*"]`.
- `.env.example` est absent.
Risque restant:
- Configuration permissive si le backend est publie directement ou si l'auth migre vers cookies.
Recommandation finale:
- Supprimer CORS en mode meme origine ou ajouter `ALLOWED_ORIGINS` explicite.
- Documenter que le backend doit rester interne.
## SEC-FIX-006 - Encadrement discovery scan/ping
Statut: non corrige
Preuve dans le code:
- `backend/routers/discovery.py:72-86` accepte `ips: list[str]` sans validation IP ni limite de taille.
- `backend/routers/discovery.py:89-129` valide seulement le format CIDR et limite chaque reseau a 1024 hotes, mais pas le total de targets ni le perimetre autorise.
- `backend/routers/discovery.py:52-60` accepte un serveur DNS fourni librement.
Risque restant:
- Reconnaissance reseau authentifiee, surcharge CPU/process et DNS arbitraire.
Recommandation finale:
- Valider IP/CIDR/DNS, plafonner le total par requete, autoriser seulement les CIDR d'inventaire ou une allowlist, et ajouter rate limit.
## SEC-FIX-007 - Validation stricte des modeles metier
Statut: non corrige
Preuve dans le code:
- `backend/routers/devices.py:11-33`, `backend/routers/vlans.py:11-15`, `backend/routers/links.py:11-15` utilisent encore des `str` libres sans bornes, enums ni validation URL/IP/CIDR/couleur.
- Les liens externes ont `rel="noopener"` mais pas `noreferrer`: `frontend/src/components/DeviceManager.vue:137`, `frontend/src/components/TopologyGraph.vue:35`, `56`, `104`, `142`.
Risque restant:
- Donnees incoherentes, champs enormes, URL dangereuses et surface XSS future.
Recommandation finale:
- Ajouter contraintes Pydantic v2, enums serveur, validation URL `http/https`, IP/CIDR et couleur hex.
- Remplacer `rel="noopener"` par `rel="noreferrer noopener"`.
## SEC-FIX-008 - En-tetes de securite HTTP
Statut: non corrige
Preuve dans le code:
- `frontend/nginx.conf:1-25` ne definit pas CSP, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, `X-Frame-Options` ou `frame-ancestors`.
- La seule configuration ajoutee concerne `limit_req`: `frontend/nginx.conf:6-8`.
Risque restant:
- Durcissement navigateur absent dans la configuration fournie.
Recommandation finale:
- Ajouter les en-tetes dans Nginx ou fournir une configuration reverse-proxy de reference verifiee.
## SEC-FIX-009 - Contrat TLS/reverse-proxy
Statut: non corrige
Preuve dans le code:
- `docker-compose.yml:14-15` expose toujours `"8080:80"` sur toutes les interfaces.
- Aucune `.env.example` ni documentation de bind local/reverse proxy TLS n'a ete ajoutee.
Risque restant:
- Exposition HTTP involontaire sur un LAN non fiable avec interception d'identifiants/tokens.
Recommandation finale:
- Documenter HTTPS obligatoire hors local et recommander `127.0.0.1:8080:80` pour un proxy local.
- Fournir un exemple reverse-proxy TLS.
## SEC-FIX-010 - Gestion du secret JWT
Statut: non corrige
Preuve dans le code:
- `backend/routers/auth.py:20-33` cree toujours `data/secret_key.txt` via `open(..., "w")` sans permissions explicites.
- `docker-compose.yml:6-7` monte toujours `./db_data:/app/data`.
- `.gitignore` et `.env.example` sont absents.
Risque restant:
- Fuite locale de cle JWT et forge de tokens jusqu'a rotation.
Recommandation finale:
- Creer le fichier avec permissions `0600`, ignorer `db_data/`, documenter rotation et supporter un secret Docker/env obligatoire en production.
## SEC-FIX-011 - Durcissement conteneurs
Statut: non corrige
Preuve dans le code:
- `backend/Dockerfile:1-8` et `frontend/Dockerfile:8-12` ne definissent pas d'utilisateur non-root.
- `docker-compose.yml:1-20` ne definit pas `cap_drop`, `security_opt`, `read_only`, limites ressources ni healthchecks.
Risque restant:
- Impact augmente en cas de compromission d'un conteneur.
Recommandation finale:
- Passer en non-root, ajouter `cap_drop: [ALL]`, garder `NET_RAW` seulement pour le backend, ajouter `no-new-privileges`, healthchecks et volumes temporaires explicites.
## SEC-FIX-012 - Logs securite structures
Statut: non corrige
Preuve dans le code:
- Aucun logging structure n'est present dans `backend/routers/auth.py`, `devices.py`, `vlans.py`, `links.py` ou `discovery.py`.
Risque restant:
- Investigation limitee en cas d'incident: login, scans, suppressions et rate limits ne sont pas tracables.
Recommandation finale:
- Ajouter logs structures sans secrets pour authentification, changements de compte, CRUD, scans et rate limits.
## SEC-FIX-013 - Foreign keys SQLite
Statut: non corrige
Preuve dans le code:
- `backend/database.py:7-10` cree l'engine sans event SQLAlchemy pour `PRAGMA foreign_keys=ON`.
- Les `ForeignKey` restent declares dans `backend/models.py`, mais SQLite ne les enforce pas sans pragma par connexion.
Risque restant:
- Incoherences relationnelles possibles via evolutions futures, migrations ou erreurs applicatives.
Recommandation finale:
- Activer `PRAGMA foreign_keys=ON` via event SQLAlchemy et tester suppressions/creation d'interfaces/liens invalides.
## SEC-FIX-014 - Supply chain et builds reproductibles
Statut: non corrige
Preuve dans le code:
- `frontend/Dockerfile:3-4` utilise toujours `npm install` au lieu de `npm ci`.
- `frontend/package.json` contient encore `cytoscape`.
- Aucune procedure d'audit dependances n'a ete ajoutee.
Risque restant:
- Builds moins reproductibles et dependances inutiles/vulnerables plus difficiles a suivre.
Recommandation finale:
- Utiliser `npm ci`, supprimer `cytoscape` s'il est inutilise, documenter `npm audit` et l'audit Python en conservant le pin `bcrypt==3.2.2`.
## SEC-FIX-015 - Import JSON borne et valide
Statut: non corrige
Preuve dans le code:
- `frontend/src/App.vue:198-220` lit le fichier complet, parse sans schema, importe en boucle et ignore silencieusement les erreurs par `.catch(() => {})`.
Risque restant:
- Import incoherent, erreurs masquees et deni de service local par fichier volumineux.
Recommandation finale:
- Ajouter limite de taille, validation de schema, compteur d'erreurs et retour utilisateur detaille.
## SEC-FIX-016 - Suppression de v-html
Statut: non corrige
Preuve dans le code:
- `frontend/src/App.vue:30` utilise encore `v-html="tab.icon"`.
Risque restant:
- Risque XSS defense-in-depth si une future icone devient dynamique ou non controlee.
Recommandation finale:
- Remplacer par texte, composants d'icones ou SVG statique sans `v-html`.
## SEC-FIX-017 - Endpoint health public documente ou restreint
Statut: partiellement corrige
Preuve dans le code:
- `backend/main.py:136-138` expose toujours `/api/health` publiquement avec seulement `{"status": "ok"}`, sans detail sensible.
- La decision de securite n'est pas documentee dans les fichiers lus.
Risque restant:
- Exposition inutile d'information de disponibilite si le proxy publie trop largement l'API.
Recommandation finale:
- Documenter explicitement que l'endpoint est public et minimal, ou le restreindre au proxy/healthcheck.
## SEC-FIX-018 - Documentation securite et environnement
Statut: non corrige
Preuve dans le code:
- `README.md` et `.env.example` sont absents.
- `AGENTS.md:66`, `CLAUDE.md:171` et `docs/backend.md:5-12` decrivent encore les routeurs metier avec `get_current_user` au lieu de `require_password_changed`.
- `docs/backend.md:28-36` ne reflete pas le nouveau champ `must_change_password` ni les erreurs de validation.
Risque restant:
- Operateur guide par une documentation obsolete, avec configuration dev/prod et secrets insuffisamment explicites.
Recommandation finale:
- Ajouter `README.md` et `.env.example`, puis mettre a jour `AGENTS.md`, `CLAUDE.md` et `docs/*` pour `INITIAL_ADMIN_PASSWORD`, `SECRET_KEY`, CORS, TLS, rotation de cle, sauvegarde/restauration et profils dev/prod.
## Conclusion
Les trois corrections declarees sont globalement integrees, mais seules SEC-FIX-002 peut etre consideree corrigee avec des risques residuels acceptables pour le mode Compose actuel. SEC-FIX-001 et SEC-FIX-003 restent partielles. Les autres taches du plan ne sont pas encore appliquees.
Priorite recommandee:
1. Corriger la migration/strategie de rattrapage pour les comptes existants `admin`.
2. Mettre a jour la documentation et ajouter `.env.example`.
3. Ajouter des tests API pour auth, rate limit et changement force.
4. Continuer les corrections P1 restantes: JWT/session, discovery, validation metier et secret JWT.
-196
View File
@@ -1,196 +0,0 @@
# Revue securite Phase 2
Date: 2026-05-06
Base: `SECURITY_REVIEW_AFTER_FIXES.md`, `SECURITY_FIXES_APPLIED_PHASE2.md`, code actuel.
Mode: revue statique locale + builds + controles cibles en conteneur. Aucun fichier applicatif n'a ete modifie.
## Synthese
Les corrections Phase 2 principales sont bien presentes dans le code actuel: rattrapage admin utilisant encore `admin`, `token_version` JWT, expiration 24h, CORS configurable, creation des nouvelles cles JWT en `0600`, build frontend reproductible via `npm ci`, conteneurs Compose avec privileges reduits, et documentation globalement enrichie.
Deux points importants restent a corriger:
- La suite de tests Phase 2 n'est pas reproductible telle que declaree: `backend/requirements-test.txt` laisse installer `httpx>=0.28`, incompatible avec le `TestClient` de Starlette utilise par FastAPI 0.104.1.
- SEC-FIX-010 ne corrige que les nouvelles creations de `secret_key.txt`; un fichier existant cree avant Phase 2 avec des permissions trop larges est relu tel quel, sans `chmod 0600`.
Verification executee:
- `npm run build` dans `frontend`: OK.
- `docker compose build`: OK.
- Controles conteneur: backend sous UID/GID `1000:1000`, frontend sous UID `101`, ping OK avec `cap_drop: ALL` + `cap_add: NET_RAW`, secret auto-genere en `0600`, bootstrap admin OK, rattrapage admin existant OK.
- `pytest -q tests` avec `requirements-test.txt` actuel: ECHEC, incompatibilite `httpx`.
- `pytest -q tests` avec `pytest>=7.4` et `httpx<0.28`: OK, 21 tests passes.
## Verification des corrections Phase 2
### SEC-FIX-001 - Bootstrap admin et rattrapage des bases existantes
Statut: corrige
Preuves:
- `backend/main.py:65-77` ajoute `must_change_password` aux bases existantes.
- `backend/main.py:80-92` ajoute `token_version`.
- `backend/main.py:95-111` force `must_change_password=1` si `admin` utilise encore le mot de passe bootstrap `admin`.
- `backend/main.py:114-146` cree une base neuve avec `admin/admin` + `must_change_password=1`, ou avec `INITIAL_ADMIN_PASSWORD` + `must_change_password=0`.
- `backend/main.py:179-183` protege les routeurs metier via `require_password_changed`.
- Controle conteneur: base neuve -> `admin 1 1`; ancienne base admin/admin avec `must_change_password=0` -> `must_change_password=1`; admin avec mot de passe personnalise -> non modifie.
Point de vigilance:
- `SECURITY_FIXES_APPLIED_PHASE2.md` dit que `_migrate_force_admin_password_change()` s'execute avant `_migrate_users()`. C'est vrai dans le code (`backend/main.py:149-155`) et correct pour les bases existantes. Pour une base neuve, le rattrapage ne s'applique pas car la table n'existe pas encore; le cas est couvert par `_migrate_users()`.
### SEC-FIX-004 - Invalidation de session apres changement de mot de passe
Statut: corrige
Preuves:
- `backend/models.py:13` ajoute `User.token_version`.
- `backend/routers/auth.py:44` reduit l'expiration JWT a 24 heures.
- `backend/routers/auth.py:84-90` inclut `ver` dans le JWT.
- `backend/routers/auth.py:93-107` rejette un token dont `ver` differe de `user.token_version`.
- `backend/routers/auth.py:175-180` incremente `token_version` lors d'un changement de mot de passe.
- Les tests d'invalidation passent quand la dependance `httpx` est pinnee correctement.
Limite restante:
- Le JWT reste stocke dans `localStorage` (`frontend/src/auth.js:3-17`) et lu par JavaScript (`frontend/src/api.js:6-9`). Phase 2 reduit la duree et invalide les anciennes sessions, mais ne supprime pas le risque de vol de token en cas de XSS.
### SEC-FIX-005 - CORS configurable
Statut: corrige avec risque residuel de configuration
Preuves:
- `backend/main.py:163-177` lit `ALLOWED_ORIGINS`, supporte `*`, chaine vide, ou CSV d'origines.
- `.env.example:25-35` documente la variable.
Risque residuel:
- Le defaut reste permissif: `ALLOWED_ORIGINS=*` dans `.env.example:35` et `docker-compose.yml:18`. C'est retrocompatible, mais une installation exposee directement garde CORS ouvert si l'operateur ne change rien.
### SEC-FIX-010 - Secret JWT
Statut: partiellement corrige
Preuves:
- `backend/routers/auth.py:31-38` cree les nouveaux secrets via `os.open(..., 0o600)`.
- Controle conteneur: secret auto-genere avec mode `0o600`.
- `.gitignore:1-8` ignore `db_data/`, `*.db`, `.env`.
Risque restant:
- `backend/routers/auth.py:27-28` lit un fichier existant sans verifier ni corriger ses permissions. Une installation ayant deja cree `db_data/secret_key.txt` avant Phase 2 avec un mode trop large conserve ce mode apres mise a jour.
Recommandation:
- Sur chargement du fichier, appliquer ou au minimum verifier `chmod 0600`.
- Documenter une remediation explicite: `chmod 600 db_data/secret_key.txt`.
### SEC-FIX-011 - Durcissement conteneurs
Statut: corrige pour le chemin Docker Compose fourni
Preuves:
- `docker-compose.yml:4-8` lance le backend sous `DOCKER_UID:DOCKER_GID`, avec `cap_drop: ALL` et `cap_add: NET_RAW` uniquement.
- `docker-compose.yml:35-41` ajoute un healthcheck backend.
- `docker-compose.yml:45-62` lance le frontend sur `8080`, avec `cap_drop: ALL`, `no-new-privileges` et healthcheck.
- `frontend/Dockerfile:4` utilise `npm ci`.
- `frontend/Dockerfile:8-12` utilise `nginxinc/nginx-unprivileged:alpine`.
- `frontend/nginx.conf:2` ecoute sur `8080`.
- Controle conteneur: backend `uid=1000 gid=1000`; frontend `uid=101(nginx)`; ping `127.0.0.1` OK avec la configuration de capabilities.
Limites:
- `backend/Dockerfile` ne definit pas `USER`; le non-root depend de Compose. Un lancement direct de l'image backend sans `--user` tournera en root.
- Le backend n'a pas `no-new-privileges`, par choix documente pour conserver le fonctionnement de `ping`.
## Regressions et nouveaux risques
### R1 - Tests Phase 2 non reproductibles avec `requirements-test.txt`
Severite: moyenne
`backend/requirements-test.txt:1-2` contient:
```text
pytest>=7.4
httpx>=0.25
```
Dans le conteneur backend actuel, cela installe un `httpx` recent. Resultat: tous les tests utilisant `TestClient(app)` echouent avec:
```text
TypeError: Client.__init__() got an unexpected keyword argument 'app'
```
Cause: incompatibilite entre Starlette/FastAPI du projet et `httpx>=0.28`, qui a retire le raccourci `app=`.
Verification:
- Avec `requirements-test.txt` actuel: 1 failed, 20 errors.
- Avec `httpx<0.28`: 21 passed.
Correction recommandee:
- Pinner `httpx<0.28` dans `backend/requirements-test.txt`, ou mettre a jour FastAPI/Starlette de maniere controlee.
### R2 - Option Docker secret probablement fragile avec backend non-root
Severite: moyenne
`docker-compose.yml:31-34` documente une option de secret montee vers `/app/data/secret_key.txt` avec `mode: 0400`, pendant que le backend tourne sous `DOCKER_UID:DOCKER_GID` (`docker-compose.yml:4`). Selon le comportement exact de Docker Compose et du proprietaire du fichier source, un secret root-owned en `0400` peut devenir illisible par le backend non-root au demarrage.
Correction recommandee:
- Tester explicitement cette variante.
- Si elle est conservee, specifier `uid`/`gid` compatibles quand Compose le permet, ou supporter une variable `SECRET_KEY_FILE` pointant vers un chemin de secret lisible par l'utilisateur applicatif.
### R3 - Nouvelles cles protegees, anciennes cles non remediées
Severite: moyenne
Voir SEC-FIX-010. Le risque est particulierement important pour les installations deja deployees avant Phase 2, c'est-a-dire precisement le cas traite par les migrations de rattrapage admin.
## Incoherences documentation / code
- `docs/extending.md:70-71` recommande encore `dependencies=[Depends(get_current_user)]` pour un nouveau routeur. Le code actuel doit utiliser `require_password_changed` pour les routeurs metier.
- `docs/backend.md:5-12` dit "four router groups" mais l'extrait montre `vlans`, `devices`, `discovery` et omet `links`, pourtant enregistre dans `backend/main.py:182`.
- `docs/frontend.md:48-55` documente `linksApi`, mais `frontend/src/api.js` n'exporte pas `linksApi`.
- `docs/frontend.md:31` documente `setAuth(token, username)` alors que le code actuel attend `setAuth(token, username, mustChange)` (`frontend/src/auth.js:11`).
- `docs/frontend.md:114-121` decrit un guard `v-else` direct et des callbacks sans `mustChangePassword`; le code actuel a la branche forcee `AccountModal` (`frontend/src/App.vue:3-11`) et propage `mustChangePassword` (`frontend/src/App.vue:145-153`).
- `docs/architecture.md:54-57` dit encore que le stage final frontend est `nginx:alpine`; le code utilise `nginxinc/nginx-unprivileged:alpine` (`frontend/Dockerfile:8`).
- `docs/architecture.md:62-67` liste une sequence de demarrage backend obsolete et omet les migrations `_migrate_users_must_change_password`, `_migrate_users_token_version`, `_migrate_force_admin_password_change`.
- `AGENTS.md:207` parle d'un "Docker volume `db_data`"; le Compose actuel utilise un bind mount `./db_data:/app/data` (`docker-compose.yml:10-11`).
- `.env.example:5` contient un caractere corrompu dans le commentaire de section. Impact faible, mais a nettoyer.
- `SECURITY_FIXES_APPLIED_PHASE2.md` garde des traces de l'approche abandonnee (`APP_UID`/`APP_GID`, `DAC_OVERRIDE`, mentions backend Dockerfile) alors que le code final utilise `DOCKER_UID`/`DOCKER_GID` via Compose et a supprime `DAC_OVERRIDE`.
## Points herites confirmes ouverts
Ces points etaient deja signales dans `SECURITY_REVIEW_AFTER_FIXES.md` et restent ouverts dans le code actuel:
| ID | Etat actuel |
|---|---|
| SEC-FIX-006 | Discovery: `PingRequest.ips` non borne (`backend/routers/discovery.py:72-85`), DNS libre (`backend/routers/discovery.py:52-60`), limite par reseau mais pas limite totale multi-target (`backend/routers/discovery.py:93-107`). |
| SEC-FIX-007 | Validation metier faible: champs libres dans `devices.py`, `vlans.py`, `links.py`; URL/IP/CIDR/couleur/type non contraints cote serveur. Les `rel="noreferrer noopener"` sont en revanche corriges dans les liens frontend. |
| SEC-FIX-008 | En-tetes HTTP de securite absents dans `frontend/nginx.conf`: pas de CSP, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, ni politique frame. |
| SEC-FIX-009 | Le Compose expose encore `8080:8080` sur toutes les interfaces par defaut (`docker-compose.yml:45-46`). README documente HTTPS et bind loopback, mais ce n'est pas le defaut. |
| SEC-FIX-012 | Pas de logs d'audit structures dans les routeurs backend. |
| SEC-FIX-013 | SQLite foreign keys non forcees par connexion: pas d'event SQLAlchemy `PRAGMA foreign_keys=ON` dans `backend/database.py`. |
| SEC-FIX-014 | `cytoscape` a ete supprime de `frontend/package.json`, mais reste dans `frontend/package-lock.json:12` et dans le `node_modules` local comme dependance extraneous. Le lockfile doit etre regenere, et aucun audit dependances automatise n'est documente/integre. |
| SEC-FIX-015 | Import JSON frontend sans limite de taille ni schema, erreurs ignorees par `.catch(() => {})` (`frontend/src/App.vue:190-207`). |
| SEC-FIX-016 | `v-html` reste present pour les icones d'onglet (`frontend/src/App.vue:30`). Les valeurs sont hardcodees localement (`frontend/src/App.vue:139-143`), donc risque XSS faible aujourd'hui, mais surface inutile. |
## Conclusion
Phase 2 corrige effectivement les risques prioritaires annonces autour du bootstrap admin, de l'invalidation JWT, du CORS configurable, du secret JWT pour nouvelles installations et du durcissement Compose.
Avant de considerer Phase 2 comme stabilisee, corriger en priorite:
1. `backend/requirements-test.txt`: pinner `httpx<0.28` ou mettre a jour FastAPI/Starlette.
2. Remediation des permissions des secrets existants (`chmod 0600` au chargement ou migration).
3. Clarification/test de l'option Docker secret avec backend non-root.
4. Mise a jour des incoherences docs/code listees ci-dessus.