No description
  • Python 62.3%
  • TypeScript 32.6%
  • Jinja 3.1%
  • Makefile 0.8%
  • CSS 0.5%
  • Other 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Julien LEQUEN 4696ec792e Surveiller l'instance, tester la logique, ouvrir seize distributions
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>
2026-09-09 16:24:16 +02:00
backend Surveiller l'instance, tester la logique, ouvrir seize distributions 2026-09-09 16:24:16 +02:00
frontend Surveiller l'instance, tester la logique, ouvrir seize distributions 2026-09-09 16:24:16 +02:00
gateway Déploiement par images pré-construites 2026-09-09 11:41:59 +02:00
.env.example wip volumes 2026-09-09 11:53:18 +02:00
.gitignore Corriger le nom figé, la progression masquée et les échecs muets 2026-09-09 14:22:14 +02:00
docker-compose.build.yml wip 2026-09-09 11:47:54 +02:00
docker-compose.dev.yml Ne plus charger la surcouche de développement automatiquement 2026-09-09 11:45:11 +02:00
docker-compose.yml Surveiller l'instance, tester la logique, ouvrir seize distributions 2026-09-09 16:24:16 +02:00
Makefile Corriger le nom figé, la progression masquée et les échecs muets 2026-09-09 14:22:14 +02:00
PLAN.md Skuld — plateforme de veille en vulnérabilités 2026-09-09 11:05:23 +02:00
README.md Rendre le déploiement par synchronisation Git fiable 2026-09-09 11:56:15 +02:00

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 -1

Attention 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 apiKey pour 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 dev et non override : 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_VOLUME et REDIS_VOLUME avec 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_KEY rend 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éutiliser CveDetail exposerait watchlists — 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 advisories et 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_channels est 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_channels est prête.