Files
stupid-simple-network-inven…/README.es.md
T
olivier be64727d48 docs: update READMEs — IP view, OS detection, auto brand icons
- Add IP addressing view and collapsible groups to features list
- Add OS auto-detection feature bullet
- Replace 90-entry brand logo table with description of AUTO_SI
  approach (simple-icons full import, ~3000 logos, word-boundary regex)
- Update brand logo count from "30+" to "3000+"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:30:13 +02:00

367 lines
14 KiB
Markdown

<div align="center">
<img src="frontend/public/favicon.svg" width="96" height="96" alt="" />
# Stupid Simple Network Inventory
**Aplicación web autoalojada para inventario de red y visualización de topología lógica**
[English](README.md) · [Français](README.fr.md)
![Python](https://img.shields.io/badge/Python-3.11-3776AB?logo=python&logoColor=white)
![FastAPI](https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white)
![Vue.js](https://img.shields.io/badge/Vue.js-3-4FC08D?logo=vue.js&logoColor=white)
![SQLite](https://img.shields.io/badge/SQLite-003B57?logo=sqlite&logoColor=white)
![Nginx](https://img.shields.io/badge/Nginx-009639?logo=nginx&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
</div>
---
![Vista de Topología](img/tpology.png)
---
## ✨ Características
- 🗂️ **Inventario manual** — añade y gestiona dispositivos (21 tipos) con IPs, VLANs, descripciones y enlaces web opcionales
- 🗺️ **Vista de topología** — disposición en tarjetas por red (LAN / VLAN 802.1Q), con secciones WAN y puerta de enlace
- 🌐 **Vista de direccionamiento IP** — lista de IPs agrupada por red, grupos contraíbles individualmente o todos a la vez
- 📡 **Ping ICMP** — comprueba la accesibilidad de todos los hosts conocidos con un clic
- 🔍 **Descubrimiento automático** — ping sweep + consulta PTR DNS en una subred para importar nuevos hosts
- 🏷️ **Logos de fabricantes** — detección automática por nombre y descripción vía [simple-icons](https://simpleicons.org/) (3 000+ logos); alias personalizados para variantes comunes (`pve`, `unifi`, `k3s`…)
- 🖥️ **Detección de OS** — icono del sistema operativo deducido automáticamente de la descripción (51 distribuciones reconocidas)
- 🔐 **Autenticación** — inicio de sesión JWT con cambio de contraseña obligatorio en el primer uso
- 🌙 **Modo oscuro** — alternancia entre tema claro y oscuro
- 🌍 **i18n** — francés, inglés, español
## 🛠️ Stack
| Capa | Tecnología |
|------|-----------|
| Backend | FastAPI + SQLAlchemy + SQLite (Python 3.11) |
| Frontend | Vue 3 + Vite, servido por Nginx |
| Auth | JWT HS256, expiración 24h |
| Runtime | Docker Compose |
---
## 🚀 Inicio rápido
```bash
# 1. Clonar y entrar en el proyecto
git clone https://git.raspot.in/olivier/stupid-simple-network-inventory.git
cd stupid-simple-network-inventory
# 2. Crear el directorio de datos propiedad del usuario actual
mkdir -p db_data
# 3. Configurar el entorno (necesario para la propiedad correcta del bind-mount)
cp .env.example .env
# Editar .env:
# DOCKER_UID / DOCKER_GID → resultado de: id -u && id -g
# INITIAL_ADMIN_PASSWORD → definir para evitar el bootstrap admin/admin por defecto
# 4. Construir e iniciar
docker compose --env-file .env up --build -d
# 5. Abrir http://localhost:8080 en el navegador
```
### Primer inicio de sesión
| Caso | Credenciales | Comportamiento |
|------|-------------|----------------|
| `INITIAL_ADMIN_PASSWORD` definido | `admin` / `<tu contraseña>` | Inicio de sesión normal |
| `INITIAL_ADMIN_PASSWORD` no definido | `admin` / `admin` | Cambio de contraseña obligatorio antes de acceder a la aplicación |
---
## 🔄 Actualización
Los datos se almacenan en `./db_data/` (bind-mount), que nunca es modificado al reconstruir los contenedores. La actualización es segura:
```bash
git pull
docker compose up --build -d
```
Las migraciones de base de datos se ejecutan automáticamente al iniciar — no se requieren pasos manuales.
---
## ⚙️ Configuración
Toda la configuración se realiza mediante variables de entorno. Ver `.env.example` para la lista completa con descripciones.
| Variable | Por defecto | Descripción |
|----------|-------------|-------------|
| `SECRET_KEY` | auto-generada | Clave de firma JWT. Definir explícitamente en producción. |
| `INITIAL_ADMIN_PASSWORD` | _(vacío)_ | Contraseña de bootstrap del admin. Si no se define, se usa `admin/admin` con cambio forzado. |
| `ALLOWED_ORIGINS` | `*` | Orígenes CORS permitidos (separados por comas). Definir en tu dominio en producción. |
| `BIND_ADDRESS` | `0.0.0.0` | Dirección IP de escucha. Definir en la interfaz frente al reverse proxy. |
| `DNS_SERVER` | _(vacío)_ | Servidor DNS para lookups PTR durante el descubrimiento automático. Pre-rellena el campo en la UI (modificable por escaneo). Dejar vacío para desactivar el DNS inverso. |
| `DOCKER_UID` / `DOCKER_GID` | `1000` | UID/GID para el proceso backend. Debe coincidir con el usuario propietario de `./db_data/`. |
```bash
cp .env.example .env
# Editar .env — definir como mínimo DOCKER_UID, DOCKER_GID, INITIAL_ADMIN_PASSWORD
docker compose --env-file .env up --build -d
```
---
## 🔒 Seguridad
### Gestión de secretos
Dos opciones según sus requisitos de seguridad.
#### Opción A — Secreto auto-generado (recomendado para nodo único)
Dejar `SECRET_KEY` sin definir (o vacío) en `.env`. En el primer arranque, el backend genera una clave hexadecimal aleatoria de 64 caracteres, la escribe en `db_data/secret_key.txt` con permisos **0600** y la reutiliza en cada reinicio posterior. El secreto nunca aparece en una variable de entorno, un archivo compose o un log.
```bash
# .env — dejar la línea vacía o eliminarla
SECRET_KEY=
```
El único requisito es que `db_data/` esté respaldado (ya contiene la base de datos).
#### Opción B — Secreto mediante archivo Docker Compose
Almacena el secreto en un archivo en el host, fuera del control de versiones, y lo monta en el contenedor. El valor nunca aparece en una variable de entorno.
```bash
# Generar y almacenar el secreto fuera del directorio del proyecto
mkdir -p ~/.secrets
python3 -c "import secrets; print(secrets.token_hex(32))" > ~/.secrets/topologie_secret_key
chmod 600 ~/.secrets/topologie_secret_key
```
Luego descomentar los bloques `secrets:` en `docker-compose.yml` (ver los comentarios en ese archivo) y eliminar `SECRET_KEY` de `.env`. Docker Compose fusiona la anulación automáticamente:
```bash
docker compose up -d
```
### Rotación de clave
Para rotar el secreto JWT (invalida todas las sesiones activas):
```bash
# Opción A — variable de entorno (recomendado)
# Definir un nuevo SECRET_KEY en la configuración de despliegue y reiniciar
# Opción B — rotación por archivo
docker compose stop backend
rm db_data/secret_key.txt
docker compose start backend
# Todos los usuarios deberán iniciar sesión de nuevo
```
### HTTPS
Esta aplicación no termina TLS. Para uso en producción, colócala detrás de un reverse proxy que gestione HTTPS.
#### Nginx
```nginx
server {
listen 443 ssl;
server_name inventory.example.com;
ssl_certificate /etc/letsencrypt/live/inventory.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/inventory.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
#### Traefik — Labels Docker
Añadir estas etiquetas al servicio `frontend` y conectarlo a la red compartida con Traefik:
```yaml
# docker-compose.override.yml
services:
frontend:
labels:
- "traefik.enable=true"
- "traefik.http.routers.inventory.rule=Host(`inventory.example.com`)"
- "traefik.http.routers.inventory.entrypoints=websecure"
- "traefik.http.routers.inventory.tls.certresolver=letsencrypt"
- "traefik.http.services.inventory.loadbalancer.server.port=8080"
networks:
- internal
- traefik_public # red compartida con tu instancia de Traefik
networks:
traefik_public:
external: true
```
#### Traefik — Configuración dinámica (file provider)
Si Traefik no corre en Docker (o prefieres el file provider), deposita un archivo en tu directorio de configuración dinámica:
```yaml
# /etc/traefik/dynamic/inventory.yml
http:
routers:
inventory:
rule: "Host(`inventory.example.com`)"
entryPoints:
- websecure
tls:
certResolver: letsencrypt
service: inventory-svc
services:
inventory-svc:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
```
Traefik detecta el archivo automáticamente — no se requiere reinicio.
Para uso solo local, enlazar a loopback para evitar exposición accidental en la LAN:
```yaml
# docker-compose.override.yml
services:
frontend:
ports:
- "127.0.0.1:8080:8080"
```
### Endurecimiento de contenedores
| Medida | Backend | Frontend |
|--------|---------|----------|
| Usuario no-root | `DOCKER_UID:DOCKER_GID` (usuario host) | `nginx` (UID 101) |
| `cap_drop: ALL` | ✓ | ✓ |
| `cap_add: NET_RAW` | ✓ (ping) | — |
| `no-new-privileges` | — ¹ | ✓ |
| Healthcheck | ✓ | ✓ |
¹ Omitido en el backend: el ping usa file capabilities (`cap_net_raw=ep`); `no-new-privileges` suprimiría el bit efectivo del archivo e impediría al subproceso adquirir `CAP_NET_RAW` en su conjunto efectivo aunque el padre lo tenga en su conjunto permitido.
---
## 💾 Persistencia de datos
Todos los datos se almacenan en `./db_data/`:
| Archivo | Descripción |
|---------|-------------|
| `topology.db` | Base de datos SQLite |
| `secret_key.txt` | Secreto JWT auto-generado (permisos 0600) |
**Copia de seguridad**: `cp -r db_data/ db_data.bak/`
**Restauración**: detener la stack, reemplazar `db_data/`, reiniciar.
---
## 🏷️ Detección de logos de marcas
La detección se realiza automáticamente sobre el **nombre** y la **descripción** del equipo (sin distinción de mayúsculas, coincidencia de palabras completas). No se requiere configuración manual.
Los logos se muestran en dos lugares:
- **Vista de topología** — pequeños SVGs en línea en cada chip de equipo
- **Lista de equipos** — badge(s) de color en la card del equipo
Pueden aparecer varios logos simultáneamente si coinciden varias palabras clave.
**Fuente**: la biblioteca [simple-icons](https://simpleicons.org/) se importa íntegramente (~3 000 logos). La detección opera en dos pasadas:
1. **Alias curados** — ~50 entradas para casos no cubiertos por el título exacto: alias comunes (`pve` → Proxmox, `unifi` → Ubiquiti, `k3s` → Kubernetes, `routeros` → MikroTik…), iconos ausentes de simple-icons (Eaton, Riello, Vertiv, Zabbix, Centreon, Nagios, PRTG, Free, Bouygues, SFR, Windows…), colores o formas personalizadas.
2. **Auto-detección** — todos los logos de simple-icons restantes (título ≥ 4 caracteres) están disponibles automáticamente: Grafana, Gitea, Portainer, GitLab, Immich, y miles más, sin ningún código adicional.
---
## 🧑‍💻 Desarrollo
### Tests del backend
```bash
cd backend
pip install -r requirements.txt -r requirements-test.txt
pytest tests/ -v
```
### Dev local (sin Docker)
```bash
# Backend
cd backend
pip install -r requirements.txt
uvicorn main:app --reload
# Frontend (terminal separada)
cd frontend
npm install
npm run dev # Servidor dev Vite en :5173, proxifica /api/ hacia :8000
```
---
## 🔧 Solución de problemas
### El escaneo devuelve cientos de falsos positivos (proxy-ARP / UniFi)
**Síntoma** — El descubrimiento automático reporta tantos hosts como direcciones hay en la subred (por ejemplo 254 para un `/24`), cuando en realidad solo hay unas pocas máquinas presentes.
**Causa** — Algunos equipos de red (especialmente UniFi Security Gateway, Dream Machine y equipos similares) activan el proxy-ARP y responden a los pings ICMP para **todas** las IPs de la subred, suplantando la IP de origen de la respuesta. La verificación de IP de origen integrada en el escáner no puede filtrar estos falsos positivos.
**Solución** — Activar la opción **"Escaneo TCP (anti proxy-ARP)"** en la pantalla de configuración del escaneo. Esta opción usa TCP en lugar de ICMP para detectar hosts activos (puertos 22, 80, 443, 8080, 8443):
- Un **host real** responde con RST (puerto cerrado) o acepta la conexión → considerado activo.
- Una **IP fantasma**: el gateway descarta silenciosamente el SYN sin responder → timeout → eliminada.
> **Nota**: un equipo cuyo firewall descarte silenciosamente (*DROP*, sin RST) **todos** los puertos sondeados no será descubierto automáticamente y deberá añadirse manualmente.
### Faltan equipos en el escaneo (rate-limiting ICMP)
**Síntoma** — Algunos hosts (APs Wi-Fi, switches, dispositivos IoT) responden a un ping directo pero no aparecen en un escaneo completo de la subred.
**Causa** — Cuando 100 workers ICMP concurrentes inundan un `/24`, algunos equipos o switches gestionados limitan las respuestas ICMP. El equipo ignora la sonda durante el escaneo aunque un ping simple funcione correctamente.
**Solución** — Dos opciones:
- Activar **"Escaneo suave (ICMP lento)"**: reduce la concurrencia de 100 a 10 workers. El escaneo tarda más pero las sondas ICMP se distribuyen en el tiempo, evitando el rate-limiting. Ideal para redes sin proxy-ARP.
- Activar **"Escaneo TCP (anti proxy-ARP)"**: evita ICMP por completo. Las sondas TCP no están sujetas al mismo rate-limiting. Ideal cuando hay tanto proxy-ARP como rate-limiting.
---
## 🏗️ Arquitectura
Ver [`docs/architecture.md`](docs/architecture.md) para el flujo de solicitudes detallado, la configuración Docker y el modelo de autenticación.
---
## 🤖 Desarrollado con asistencia de IA
Esta aplicación fue desarrollada íntegramente con la ayuda de asistentes de código IA:
- **[Claude Code](https://claude.ai/code)** (Anthropic) — utilizado a lo largo del proyecto para el diseño de la arquitectura, la API backend (FastAPI, SQLAlchemy, autenticación, descubrimiento ICMP), los componentes frontend (Vue 3, layout CSS de topología, i18n, modo oscuro), la configuración Docker & Nginx, y la documentación.
- **[Codex](https://openai.com/codex)** (OpenAI) — también utilizado durante el desarrollo para la generación de código y sugerencias en todo el proyecto.
---
## 📄 Licencia
Este proyecto se distribuye bajo la [Licencia Pública General GNU v3.0](LICENSE).
Eres libre de usar, modificar y distribuir este software bajo los términos de la GPL v3. Cualquier trabajo derivado debe distribuirse bajo la misma licencia.