Skill : cve-veille (D1)
Objectif
Avaler les CVE des dernières 24h (ou depuis last_run) répondant aux critères de criticité, et créer une page draft par CVE retenue dans _Inbox/raw/cve/. Idempotent. Plafond strict de 5 CVE par run pour éviter la pollution de l'inbox.
⚠️ Conventions techniques (à respecter strictement)
- Encoding UTF-8 sans BOM pour tous les writes :
Set-Content -Encoding utf8 -NoNewlineouOut-File -Encoding utf8 -NoNewline. JAMAIS d'écriture en encoding par défaut PowerShell (UTF-16 LE BOM corromprait les fichiers). - Lecture :
Get-Content -Raw -Encoding utf8pour les fichiers JSON,-Rawpour le markdown. - Append jsonl :
Add-Content -Encoding utf8 -Path <file> -Value <line>. - Atomicité : pour modifier un fichier existant, écrire
<file>.tmppuisMove-Item -Force. - WebFetch pour les URLs HTTPS,
Invoke-RestMethodpour les APIs JSON (NVD).
Procédure
1. Lire l'état
Lire 99-Meta/lazy-obsidian-state.json :
$statePath = "D:\tommyDossier\Documents\Obsidian Vault\99-Meta\lazy-obsidian-state.json"
$state = Get-Content $statePath -Raw -Encoding utf8 | ConvertFrom-Json
$lastRun = $state.last_run.'cve-veille'
$seenCves = $state.seen_cves
Si $lastRun correspond à aujourd'hui (UTC, format YYYY-MM-DD) → afficher "Déjà fait aujourd'hui ($lastRun). Skip." et terminer (exit gracieux). Append une ligne SKIP_IDEMPOTENT au runs.jsonl.
2. Définir la fenêtre temporelle
start=$lastRunou(Get-Date).AddDays(-1).ToUniversalTime()si nullend=(Get-Date).ToUniversalTime()- Format ISO 8601 :
yyyy-MM-ddTHH:mm:ss.fffZ
3. Sources à interroger
a. NVD JSON API 2.0
https://services.nvd.nist.gov/rest/json/cves/2.0?lastModStartDate=<start>&lastModEndDate=<end>
Pagination avec startIndex si > 2000 résultats (rare en 24h).
b. MSRC RSS
https://api.msrc.microsoft.com/update-guide/rss
Filtrer entrées pubDate dans la fenêtre.
c. CISA KEV catalog
https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
Filtrer vulnerabilities[].dateAdded dans la fenêtre.
Per-source try/catch. Une source en échec n'invalide pas les autres. Logger les sources échouées dans sources_failed[] du runs.jsonl.
4. Whitelist produits
Liste configurable (modifier ici si besoin) :
Windows
Linux kernel
Apache HTTPD
nginx
OpenSSH
OpenSSL
Kubernetes
Docker
Microsoft Exchange
Active Directory
Match case-insensitive, substring sur les champs produit (CPE criteria pour NVD, titre/description pour MSRC/RSS).
5. Critères de retenue
Une CVE est retenue si TOUTES ces conditions :
- ID CVE non présent dans
$seenCves - (CVSS v3 baseScore ≥ 7.0) OU (présente dans CISA KEV —
vulnerabilities[].cveID) - Au moins un produit affecté match (case-insensitive) un item de la whitelist
6. Tri & plafond
$retained | Sort-Object @{e='cvss';desc=$true}, @{e='date';desc=$true} | Select-Object -First 5
Plafond strict : 5 CVE par run. Si la fenêtre contient plus de 5 CVE éligibles, on garde les plus critiques (CVSS desc puis date desc).
7. Pour chaque CVE retenue : écrire le fichier
Chemin : _Inbox\raw\cve\CVE-YYYY-NNNNN.md
Contenu (template — substituer les <…>) :
---
titre: "CVE-YYYY-NNNNN — <produit principal> <classe vuln en 3-5 mots>"
type: vulnerabilité
cluster: <cluster_inferé>
cve: [CVE-YYYY-NNNNN]
cvss_score: <float>
statut: draft
criticite: <low|medium|high|critical>
importance: standard
source_knowledge: vendor-doc
source_url: <URL NVD primaire>
tags: ["#cyber/cve", "#meta/veille-auto"]
date_maj: <YYYY-MM-DD>
---
# CVE-YYYY-NNNNN — <produit> <classe>
> [!warning] Vulnérabilité critique
> Score CVSS : <score>. CISA KEV : <oui/non>.
## Description
<Description NVD originale, traduite en français si possible, max 6 lignes.>
## Produits affectés
- <Liste CPE / versions, max 5 items>
## Source
- NVD : <URL>
- Vendor advisory : <URL si présent>
- CISA KEV : <URL si applicable>
## À investiguer
- [ ] Cluster cible définitif (parmi les 16 du Schema.md)
- [ ] Lien vers pages existantes du vault (wikilinks)
- [ ] Promotion vers `pages/{01|02|03}-…` si pertinent
Écriture impérative :
$path = "...\_Inbox\raw\cve\CVE-2026-XXXXX.md"
Set-Content -Path $path -Value $content -Encoding utf8 -NoNewline
8. Inférence du cluster (heuristique)
| Mots-clés produit | Cluster inféré |
|---|---|
Active Directory, Kerberos, NTLM, LDAP |
07-IAM-ActiveDirectory |
Windows, Microsoft Exchange, Office |
02-Blue-Team |
Linux kernel |
02-Blue-Team |
Apache, nginx, OpenSSL |
05-AppSec-DevSecOps |
Kubernetes, Docker |
06-Cloud-Security |
OpenSSH |
04-Reseau-Protocoles |
| (autre / ambigu) | 99-Meta |
Mapping criticité ↔ CVSS :
- 9.0–10.0 →
critical - 7.0–8.9 →
high - (<7.0 ne passe pas le filtre)
9. Append dans Fact-Check-Log.md
Pour chaque CVE retenue :
$line = "- [veille-auto] [$cveId](_Inbox/raw/cve/$cveId.md) — CVSS $score, source NVD, ingéré $now"
Add-Content -Path "...\99-Meta\Fact-Check-Log.md" -Value $line -Encoding utf8
Si la section "## Veille automatique" n'existe pas dans Fact-Check-Log.md, la créer en append avant de logger.
10. Update state.json
$state.last_run.'cve-veille' = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
foreach ($cve in $retainedIds) { $state.seen_cves += $cve }
$state | ConvertTo-Json -Depth 5 | Set-Content -Path $statePath -Encoding utf8 -NoNewline
Si erreurs → append dans state.errors[] (FIFO max 50 entrées).
11. Append runs.jsonl
$runEntry = @{
ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
skill = "cve-veille"
status = "OK" # ou "PARTIAL", "FAIL", "SKIP_IDEMPOTENT"
new = $retainedCount
skipped = $skippedCount
errors = $errorCount
sources_failed = @()
} | ConvertTo-Json -Compress
Add-Content -Path "...\99-Meta\lazy-obsidian-runs.jsonl" -Value $runEntry -Encoding utf8
Statuts :
OK: ≥1 retenue, 0 erreur sourcePARTIAL: ≥1 retenue mais ≥1 source en échecFAIL: aucune retenue ET ≥1 source en échecSKIP_IDEMPOTENT: déjà fait aujourd'hui
12. Output utilisateur
Affichage console final :
✅ cve-veille — 3 nouvelles CVE retenues, 12 filtrées hors whitelist, 0 erreurs.
- CVE-2026-12345 (CVSS 9.8) → _Inbox/raw/cve/
- CVE-2026-12346 (CVSS 8.1) → _Inbox/raw/cve/
- CVE-2026-12347 (CVSS 7.5) → _Inbox/raw/cve/
Si SKIP : ⏭ cve-veille — déjà fait aujourd'hui (2026-04-25T09:12:33Z). Skip.
Si FAIL : ❌ cve-veille — sources en échec : <liste>. Aucune CVE ingérée.
Gestion d'erreurs
| Cause | Action |
|---|---|
| NVD 5xx / timeout | Retry x2 backoff (1s, 5s). Échec → log state.errors[], continuer avec MSRC/CISA |
| NVD parser fail | Écrire _Inbox\raw\cve\_PARSE-ERROR-<date>.md avec payload brut, log, skip |
| RSS feed down | Per-source try/catch, log sources_failed, autres sources continuent |
| Quota / 429 / 403 | Backoff 30s → 5min → abandon |
Set-Content échoue (fichier verrouillé Obsidian) |
Retry après 2s. Si échec → log + skip ce CVE spécifique |
Limites volontaires (YAGNI)
- Pas de retry infini (max 2)
- Pas de notification externe (push, email)
- Pas de check sémantique (date plausible) — confiance NVD
- Pas de suppression de CVE déjà ingérée même si retirée du KEV
- Pas de classification fine du cluster (heuristique simple ; humain réajuste)