- Python 62.3%
- TypeScript 32.6%
- Jinja 3.1%
- Makefile 0.8%
- CSS 0.5%
- Other 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Trois chantiers repondant aux priorites retenues. SURVEILLANCE. Cette session a connu trois pannes silencieuses — ingestion morte, échecs sans motif, index GIN corrompu — toutes détectées par l'utilisateur et jamais par le système. C'est le pire mode de défaillance pour un outil de veille : il continue d'afficher un tableau de bord plausible sur des données figées, et donne un faux sentiment de couverture. Trois contrôles y répondent, un par panne vécue : fraîcheur de chaque source, intégrité des index btree par `amcheck`, relisibilité des sauvegardes. Les index GIN, que PostgreSQL 16 ne sait pas auditer, sont reconstruits chaque semaine faute de pouvoir être vérifiés. Un courriel part aux administrateurs, sans répétition avant douze heures — une alerte qu'on apprend à ignorer ne protège plus. Deux défauts trouvés en écrivant ces contrôles. `bt_index_check` attend un `regclass` : le paramètre nu déclenchait une erreur de type sur chaque index, et le contrôle annonçait 82 index corrompus sur 82 sains. Et psql n'interpole pas ses variables dans une commande passée par `-c`, si bien que le conteneur de sauvegarde ne rapportait jamais rien. TESTS. La suite était vide ; elle couvre maintenant la logique où une erreur est silencieuse : comparaison dpkg, plages de versions, score de priorité, analyse OSV. Elle a immédiatement trouvé deux défauts réels. Une version contenant un espace interne était comparée alors que `dpkg` lui-même refuse de conclure. Et `LooseVersion` acceptait n'importe quel texte : une contrainte saisie de travers dans une veille produisait un objet comparable qui n'égalait aucune version, donc filtrait toutes les alertes — sans rien dire à l'utilisateur. DISTRIBUTIONS. Le connecteur OSV.dev en apporte douze, portant le total à seize et 141 versions sélectionnables, contre quatre auparavant. Écrire un connecteur par distribution aurait demandé douze parseurs cassant indépendamment ; OSV les normalise en un format unique. Trois surprises de format, chacune silencieuse. Les publieurs répartissent leurs identifiants CVE sur `aliases`, `related` ou `upstream` selon leur habitude : ne lire que le premier faisait perdre cinq distributions entières sans la moindre erreur, archives téléchargées et analysées pour rien. SUSE nomme ses écosystèmes par produit complet, ce qui produisait 227 entrées tronquées et indiscernables dans le menu ; on en extrait la version du système, seule chose qu'un exploitant sache de son parc. Enfin SUSE émet 1,3 million d'états : les accumuler avant écriture faisait tomber le conteneur, d'où une lecture en flux. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| backend | ||
| frontend | ||
| gateway | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.build.yml | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Makefile | ||
| PLAN.md | ||
| README.md | ||
Skuld — veille en vulnérabilités
Skuld est la Norne du futur dans la mythologie nordique, celle qui tisse le destin aux côtés d'Urðr (ce qui fut) et Verðandi (ce qui devient). Son nom signifie littéralement ce qui est dû — même racine que l'anglais should et l'allemand Schuld, « la dette ».
C'est exactement ce que présente cet outil : une dette de sécurité, classée par échéance. Chaque vulnérabilité non corrigée est quelque chose qui est dû, et qui finira par se payer.
Portail de veille CVE pour une équipe interne. On y déclare les produits qu'on exploite, et l'outil signale les vulnérabilités qui les concernent — en distinguant ce qui est théoriquement grave de ce qui est réellement exploité.
- Sources : NVD (référentiel CVE + CPE), CISA KEV (exploitation constatée), EPSS/FIRST (probabilité d'exploitation à 30 jours), trackers Debian / Ubuntu / Red Hat / Alpine, avis éditeurs CSAF 2.0, exploits publics (Exploit-DB, Metasploit, Nuclei, PoC GitHub)
- Matching : CPE officiels, dictionnaire d'alias produits, comparaison de versions tolérante aux schémas des éditeurs
- Notifications : email immédiat ou résumé, flux RSS par veille
- Navigation : tout le référentiel consultable et filtrable, pas seulement les produits suivis
- Interface : claire, dense en information, utilisable au pouce sur téléphone
Installation
Prérequis : Docker et Docker Compose. Le seul port ouvert est celui de la passerelle, à faire pointer depuis votre Nginx Proxy Manager.
git clone <votre-dépôt> vulnwatch && cd vulnwatch
make secrets # génère SECRET_KEY et POSTGRES_PASSWORD dans .env
$EDITOR .env # renseigner PUBLIC_BASE_URL et NVD_API_KEY
make up # construit et démarre la stack
Puis, dans Nginx Proxy Manager, créez un Proxy Host :
| Champ | Valeur |
|---|---|
| Domain Names | vulnwatch.exemple.fr |
| Scheme | http |
| Forward Hostname / IP | IP du serveur Docker |
| Forward Port | 8080 (variable APP_PORT) |
| Websockets Support | activé |
| SSL | certificat Let's Encrypt + Force SSL |
Ouvrez ensuite l'URL : l'écran d'installation guidée vous fait créer le compte administrateur.
Deux réglages qui comptent
PUBLIC_BASE_URL doit être l'URL publique réelle, celle du Proxy
Host. C'est elle qui construit les liens dans les emails ; une valeur
erronée donne des alertes avec des liens morts.
NVD_API_KEY — gratuite, délivrée en quelques
minutes. Sans elle,
le NVD limite à 5 requêtes / 30 s au lieu de 50 : l'import historique
prend environ 27 minutes de plus (~280 requêtes temporisées à 6,5 s au
lieu de 0,7 s). Utile, pas indispensable.
Une clé invalide est pire que pas de clé : le NVD répond
404 Invalid apiKeyà chaque requête au lieu de basculer en mode anonyme. Vérifiez-la avant de la mettre en service :curl -sD- -o/dev/null -H "apiKey: VOTRE_CLE" \ "https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=1" | head -1Attention au faux négatif : enchaîner les requêtes déclenche la limitation de débit du NVD, qui répond alors le même
404 Invalid apiKeypour une clé pourtant valide. Espacez les essais.
Déploiement par gestionnaire de conteneurs (Arcane, Portainer…)
Ces outils ne disposent que du fichier compose, sans le code source.
Toute directive build: ou tout montage de fichier local y échoue :
failed to prepare project images for deploy:
build context not found: stat /app/data/projects/skuld/backend
docker-compose.yml — le fichier racine — ne contient aucun build:
ni montage depuis l'hôte. C'est délibéré : docker compose up sans
argument, ou un gestionnaire qui synchronise ce dépôt par Git, tombe
forcément dessus. Il ne référence que des images du registre, et la
configuration nginx est embarquée dans la sienne.
La synchronisation Git fonctionne donc, quelle que soit la façon dont votre gestionnaire résout les fichiers : le déploiement ne dépend d'aucun chemin local.
| Fichier | Rôle | Chargé |
|---|---|---|
docker-compose.yml |
production, images du registre | par défaut |
docker-compose.build.yml |
ajoute les contextes de build | make up |
docker-compose.dev.yml |
rechargement à chaud, Mailpit, Adminer | make dev |
La surcouche de développement s'appelle
devet nonoverride: Docker Compose charge automatiquement tout fichier nommédocker-compose.override.yml. Sous ce nom, un déploiement piloté par une interface démarrerait en mode développement, avec Adminer et les ports de base de données publiés.
Publier une version depuis un poste disposant du code :
docker login forge.lequen.fr
make release VERSION=1.0.1
Trois images sont produites — skuld-api, skuld-web, skuld-gateway.
Le worker et le planificateur réutilisent celle de l'API : le code y est
identique, seule la commande change.
Côté gestionnaire, renseignez SECRET_KEY, POSTGRES_PASSWORD,
PUBLIC_BASE_URL et NVD_API_KEY. SKULD_VERSION épingle une version ;
à défaut, latest.
Reprise d'une instance existante dont le nom de projet Docker a changé : ses volumes gardent l'ancien préfixe. Renseignez
PGDATA_VOLUMEetREDIS_VOLUMEavec les noms réels, sinon la pile démarre sur une base vide et paraît avoir tout perdu.
La sauvegarde quotidienne est active par défaut. Sur un déploiement piloté par une interface, personne ne pense à activer un profil optionnel, et l'absence de sauvegarde ne se remarque que le jour où l'on en a besoin.
Import historique
make backfill # toutes les CVE depuis 1999 — 1 à 3 h
Le portail reste utilisable pendant l'opération ; les vulnérabilités apparaissent au fil de l'import. Une fois terminé, KEV et EPSS sont rejoués automatiquement pour enrichir ce qui vient d'être importé.
Configuration de l'email
Le SMTP ne se configure pas dans le .env mais dans l'interface :
Paramètres → Administration → Email. Le mot de passe est chiffré au
repos (Fernet, clé dérivée de SECRET_KEY), n'est jamais réaffiché ni
renvoyé par l'API.
Le bouton Tester envoie un vrai message et renvoie un diagnostic exploitable — « authentification refusée par le serveur », « le mode de chiffrement ne correspond pas au port », plutôt qu'un simple « échec ».
Changer
SECRET_KEYrend illisible le mot de passe SMTP enregistré. L'application continue de fonctionner ; il faut le ressaisir.
Tant que le SMTP n'est pas configuré, les alertes ne sont pas perdues : elles restent en attente et partent dès l'activation (au-delà de 48 h, elles sont abandonnées pour éviter un déversement massif — elles restent consultables dans le portail).
Fonctionnement
Priorisation
Le CVSS seul est un mauvais outil de tri : une 9.8 sur un composant jamais exposé ne sera jamais exploitée, alors que des failles en 6.5 le sont activement. Le score combine quatre signaux, pondérables dans l'interface :
| Signal | Question | Poids par défaut |
|---|---|---|
| CVSS | ce que ça fait si c'est exploité | 30 % |
| EPSS | chance que ce soit exploité sous 30 j | 30 % |
| Maturité d'exploit | existe-t-il du code d'attaque | 25 % |
| Criticité du périmètre | ce que ce produit représente | 15 % |
Une CVE au catalogue CISA KEV ne peut pas descendre sous 90/100 : l'exploitation y est constatée, pas estimée. En pratique, une 8.8 exploitée passe devant une 9.8 théorique — ce qui est le comportement recherché.
Chaque score est affiché avec sa décomposition et une explication en français. Un score opaque n'est pas actionnable.
Matching produits
C'est là que la plupart des outils échouent, dans les deux sens : trop de faux positifs, ou des CVE manquées en silence.
Alias. Le NVD indexe fortinet:fortios, l'équipe dit « FortiGate ».
Un dictionnaire d'une centaine de produits d'infrastructure est livré ; il
s'enrichit à l'usage via la file de revue. La résolution se fait à la
saisie, et l'interface montre sur quel produit NVD votre veille a été
rattachée — une veille mal orthographiée ne remonte rien, et l'absence
d'alerte se confond avec l'absence de vulnérabilité.
Versions. Les éditeurs ne respectent pas le versionnement sémantique :
7.2.4 build1396, 9.1(3)h, 2019 CU12, 19.3.0.0.0, 7.0 U3c. Le
comparateur les traite tous, y compris le piège classique où 10.0 passe
avant 9.0 en comparaison lexicographique.
Doute. Quand la comparaison ne peut pas conclure, l'alerte remonte marquée « à confirmer » et ne déclenche aucune notification. Elle part en file de revue. Un faux positif se ferme en un clic ; une vulnérabilité manquée ne se voit jamais.
Apprentissage. Chaque verdict dans la file de revue est réinjecté : « c'est bien mon produit » crée un alias, « faux positif » crée une exclusion durable. Le bruit décroît avec l'usage.
État réel par distribution
Le NVD raisonne en versions amont, les distributions rétroportent les
correctifs sans toucher à ce numéro. « linux_kernel < 6.1 vulnérable » et
« corrigé dans 5.10.223-1~deb11u1 » sont tous deux exacts ; seul le
second dit quoi faire.
Sur un parc Linux, s'en tenir au NVD produit un bruit qui fait abandonner l'outil. Quatre trackers sont donc interrogés deux fois par jour :
| Distribution | Source | Ce qu'elle apporte |
|---|---|---|
| Debian | security-tracker (JSON complet) |
états open/resolved par suite, versions corrigées, urgence |
| Ubuntu | notices USN | versions corrigées par version d'Ubuntu, référence USN |
| Red Hat | Security Data API | paquets corrigés par version RHEL, référence RHSA |
| Alpine | secdb par branche |
versions corrigées, main et community |
Les vocabulaires sont normalisés — Debian, Ubuntu, Red Hat et Alpine
n'appellent pas « corrigé » de la même façon. ignored et end-of-life
remontent quand même : « ne sera pas corrigé » est une décision de
l'éditeur que l'exploitant doit connaître, pas une absence de risque.
Chaque fiche CVE affiche ce tableau avant la liste des produits affectés du NVD, parce que c'est l'information décisive.
Ubuntu : l'API CVE de Canonical plafonne à 20 entrées par requête, soit près de 4 000 appels pour un parcours complet. Le connecteur s'appuie donc sur les USN (ce qui est corrigé) et complète l'état « affecté, non corrigé » par un enrichissement ciblé sur les seules CVE ayant une alerte ouverte.
Avis éditeurs (CSAF 2.0)
CSAF est le format normalisé OASIS que les éditeurs adoptent pour publier
des avis exploitables par une machine. Un connecteur, N éditeurs — et
les suivants s'ajoutent par configuration, sans code : il suffit de l'URL
de leur provider-metadata.json.
Les deux modes de distribution de la norme sont gérés : répertoire avec
changes.csv (Red Hat) et flux ROLIE. Livré avec Red Hat et Siemens
ProductCERT.
Les avis vont dans advisories, pas dans cves : un avis n'est pas une
vulnérabilité, c'est une communication qui en couvre souvent plusieurs, et
parfois aucune référencée.
Exploits publics
Le CVSS dit ce qu'une faille permet, l'EPSS estime si elle sera exploitée. Ni l'un ni l'autre ne dit si du code d'attaque existe déjà. Quatre sources y répondent, et alimentent la maturité — quatrième composante du score :
| Source | Maturité | Ce que ça signifie |
|---|---|---|
| Metasploit | 1,00 | exploitable en trois commandes par n'importe qui |
| Exploit-DB (vérifié) | 0,80 | exploit rejoué et confirmé fonctionnel |
| Nuclei | 0,70 | détection fiable à distance — donc trouvable en masse |
| PoC GitHub | 0,40 | quelqu'un s'y intéresse publiquement, qualité non garantie |
L'effet est concret : une CVE en 8.8 avec module Metasploit passe devant une 9.8 qui n'a qu'un template de détection.
Une CVE qui acquiert son premier exploit public déclenche une re-notification, au même titre qu'une entrée au catalogue KEV. C'est un fait nouveau qui change la conduite à tenir sur une vulnérabilité parfois ignorée depuis des mois.
Ce qui est stocké : des liens et des métadonnées. Jamais de code d'exploitation. Cela écarte les questions de licence et empêche la plateforme de devenir un dépôt d'armes.
Portail public
Administration → Portail public ouvre une page consultable sans compte,
à l'adresse /veille. Désactivé par défaut ; tant qu'il l'est, l'adresse
répond 404 — rien n'indique que la fonctionnalité existe.
Trois périmètres au choix : vulnérabilités activement exploitées (catalogue KEV), au-dessus d'un seuil de gravité, ou tout le référentiel. Options : recherche, affichage des liens d'exploits, indexation par les moteurs.
Ce que le portail public ne fait jamais, par construction :
- il ne réutilise aucun modèle de réponse de l'API interne. Les schémas
sont redéfinis champ par champ dans
app/api/routes/public.py. RéutiliserCveDetailexposeraitwatchlists— donc votre inventaire logiciel. Une évolution de l'API interne ne peut pas créer de fuite ici par inadvertance ; - il ne propose pas de périmètre « limité à mes produits suivis ». Publier ce qu'on surveille revient à publier son inventaire, ce qui renseigne un attaquant bien plus qu'un visiteur. L'option est volontairement absente, et l'interface explique pourquoi ;
- il applique le périmètre aussi sur les accès directs : une adresse devinée hors périmètre répond 404, pas le contenu.
Débit limité à 60 requêtes/minute par IP au niveau de la passerelle, et pagination bornée à 200 pages de 50 éléments : une route anonyme ne doit pas servir d'aspirateur à base de données.
Navigation et recherche
L'onglet Vulnérabilités donne accès à l'intégralité du référentiel, au-delà de ce que vos veilles couvrent — utile pour instruire une question ponctuelle ou évaluer un produit avant de l'adopter.
Recherche plein texte sur l'identifiant, la description et les produits
affectés (/ place le curseur dans le champ). Filtres cumulables :
gravité, catalogue KEV, présence d'exploit, éditeur, année, CVSS minimum,
période de publication. Tri par date, priorité, CVSS ou probabilité
d'exploitation.
L'état des filtres vit dans l'URL. Une recherche se partage par
copier-coller, se met en favori, et le retour arrière du navigateur
fonctionne — ?vendor=fortinet&severity=critical&kev=true est un lien
valide à envoyer à un collègue.
Depuis une fiche CVE, chaque produit affecté porte un bouton Suivre qui l'ajoute à une veille avec le couple éditeur/produit exact du NVD. Ce raccourci évite la ressaisie — et l'orthographe approximative qui fait qu'une veille ne remonte jamais rien.
Re-notification
Une alerte est un lien durable ; un événement est un fait nouveau sur ce lien. On notifie les événements. Concrètement : une CVE de 2023 en CVSS 6.5, ignorée à l'époque, qui entre au catalogue KEV aujourd'hui ressort immédiatement. C'est le cas que les outils notifiant seulement à la publication laissent passer.
Exploitation
make ps # état des services
make logs S=worker # logs d'un service
make sync # synchronisation immédiate de toutes les sources
make reindex # rejoue le matching sur tout l'historique
make rescore # recalcule le score de priorité de toutes les CVE
make backup # sauvegarde ./backups/vulnwatch-<date>.dump
make restore F=... # restauration
Sauvegarde quotidienne automatique :
docker compose -f docker-compose.yml --profile backup up -d
Cadence de collecte
| Source | Fréquence | Pourquoi |
|---|---|---|
| NVD | 15 min | source la plus mouvante ; le délai compte |
| CISA KEV | 6 h | CISA publie par lots |
| EPSS | 1 j | le dump est quotidien |
| Trackers distributions | 2 ×/j | les distributions publient par vagues |
| Avis CSAF | 2 ×/j | idem |
| Sources d'exploits | 2 ×/j | un module Metasploit change la priorité |
| Enrichissement Ubuntu | 1 j | API plafonnée, ciblée sur les alertes ouvertes |
| Notifications | 5 min | borne le délai détection → alerte reçue |
| Résumé quotidien | 8 h 30 | avant la prise de poste |
| Recalcul des scores | 7 h 50 | EPSS et pondérations évoluent |
Le service scheduler doit rester en exemplaire unique — deux
planificateurs déclencheraient chaque cron deux fois, donc deux emails par
alerte. Le worker, lui, se met à l'échelle :
docker compose up -d --scale worker=3.
Rôles
| Rôle | Portée |
|---|---|
| Lecture seule | consulte alertes et veilles |
| Éditeur | gère les veilles, trie les alertes, tranche la file de revue |
| Administrateur | + utilisateurs, SMTP, connecteurs, pondérations |
Le dernier administrateur actif ne peut être ni rétrogradé ni désactivé.
Développement
make dev # docker-compose.yml + docker-compose.dev.yml
| Service | URL |
|---|---|
| Front (hot reload) | http://localhost:3000 |
| API + Swagger | http://localhost:8000/api/docs |
| Mailpit (boîte de test) | http://localhost:8025 |
| Adminer (base) | http://localhost:8081 |
Pour tester les emails en local : Paramètres → Email, hôte mailpit,
port 1025, chiffrement « aucun ».
Structure
backend/app/
connectors/ NVD, KEV, EPSS — socle commun : reprise sur curseur, backoff
matching/ normalisation, comparaison de versions, moteur, résolveur d'alias
scoring/ score de priorité et sa décomposition
notify/ rendu des emails, moteur de diffusion
services/ alertes, réglages en base, envoi SMTP
api/routes/ endpoints REST
jobs/ tâches ARQ et planification
frontend/
app/ pages (App Router)
components/ design system et composants métier
gateway/ Nginx interne : front + API sur la même origine
Choix techniques
Front et API sur la même origine. La passerelle Nginx interne route
/ vers Next.js et /api/ vers FastAPI. Conséquence : aucune
configuration CORS, cookies de session simples, et un seul Proxy Host à
créer dans NPM.
Critères CPE, pas leur produit cartésien. Un critère « fortios de 7.2.0 à 7.2.6 » couvre des centaines de CPE concrets. Les développer donnerait des dizaines de millions de lignes pour aucune information supplémentaire — 1,5 M de lignes ici contre 200 Go dans l'autre approche.
Enrichissements dénormalisés sur cves. KEV, EPSS et nombre
d'exploits sont recopiés sur la table cves. La vue principale trie par
score de priorité, lequel dépend des trois : les recalculer par jointure à
chaque affichage coûterait trois jointures sur des tables de plusieurs
centaines de milliers de lignes. Les tables détaillées restent la source
de vérité.
Alias résolus à l'écriture, pas au matching. Le matching tourne sur
chaque CVE ingérée. Y faire une recherche approximative le transformerait
en opération coûteuse ; en résolvant à la création de la veille, il se
réduit à une égalité sur index. Contrepartie : un alias ajouté après coup
ne s'applique pas rétroactivement — c'est le rôle de make reindex.
Score de priorité recalculé en SQL, pas en Python. Charger 300 000
CVE en mémoire pour les réécrire une par une prendrait des minutes et
saturerait le worker ; une seule instruction UPDATE … FROM le fait en
quelques secondes, et ne réécrit que les lignes dont la valeur change
réellement — sans ce filtre, le job nocturne réécrirait toute la table
chaque nuit.
Réglages en base plutôt qu'en .env. Tout ce qu'un administrateur
peut vouloir changer sans redémarrer un conteneur (SMTP, pondérations,
seuils) vit en base et s'édite dans l'interface.
Ce qui n'est pas encore là
Le socle est complet et fonctionnel. Restent à brancher :
- CERT-FR, OSV, GitHub GHSA, MITRE CVE 5.x — le socle
advisorieset sa recherche plein texte sont en place, alimentés par CSAF. - Push PWA — l'application est installable, le manifeste est en place ; le Web Push (VAPID, service worker) reste à ajouter.
- Webhooks Slack / Teams / Discord — la table
notification_channelsest prête. - Push PWA — l'application est installable et le manifeste est en place ; le Web Push (VAPID + service worker) reste à ajouter.
- Webhooks Slack / Teams / Discord — la table
notification_channelsest prête.