guides/workflow: cve-berigelse
Vulnerability management · n8n · OpenCVE → dashboard

CVE-berigelse
OpenCVE → dashboard

Et n8n-workflow der modtager en webhook fra OpenCVE når en abonneret CVE dukker op, beriger den med data fra NVD og EPSS, og skriver et samlet record til din CVE-dashboard-database. Reference-mønster med importérbar JSON.

n8n Reference-skabelon OpenCVE · NVD · EPSS
7noder
Webhooktrigger fra OpenCVE
Upserttil Postgres-dashboard

Hvad det gør

OpenCVE fortæller dig at en CVE er relevant, men en CVE-id alene er ikke handlingsbar. Dette workflow tager id'et og samler den kontekst du skal bruge for at prioritere: CVSS-score fra NVD og EPSS-sandsynlighed for udnyttelse — og skriver det hele til dit dashboard så du har ét sted at triagere fra.

Forudsætninger: OpenCVE kørende med en webhook-notifikation, n8n, og en Postgres-tabel til dashboardet. Dette er et referencemønster — verificér node-typer mod din n8n-version.

Flow-overblik

Webhook validerer → parallel berigelse → merge → byg record → upsert.

webhookOpenCVE
ifValidér secret
httpNVD Lookup
httpEPSS Score
mergeMerge
codeBuild Record
postgresUpsert
Parallel berigelse: NVD- og EPSS-opslagene kører samtidig efter secret-valideringen og samles i Merge-noden (combine all). Det holder latency nede frem for sekventielle kald.

Node-for-node

#NodeTypeFunktion
1WebhookwebhookPOST-endpoint /webhook/opencve-cve som OpenCVE kalder
2Validate SecretifTjekker hemmelig header så kun OpenCVE kan trigge
3NVD LookuphttpRequestHenter CVSS + beskrivelse fra NVD CVE API 2.0
4EPSS ScorehttpRequestHenter EPSS-sandsynlighed fra FIRST.org
5MergemergeKombinerer de to berigelseskilder (combine all)
6Build RecordcodeSamler felterne til ét fladt dashboard-objekt
7UpsertpostgresINSERT … ON CONFLICT opdaterer eksisterende CVE-rækker
NVD rate-limits: NVD-API'et har stramme rate-limits uden API-nøgle (få kald pr. 30 sek). Anmod om en gratis NVD API-nøgle og tilføj den som header, hvis du forventer mange CVE'er — ellers risikerer du 403/429-fejl. Overvej en lille wait/retry-node ved travlhed.

Importérbar JSON

n8n import

Indsæt i n8n via Workflows → ⋯ → Import from clipboard. Erstat DIN_HEMMELIGHED, Postgres-credential og felt-stier så de matcher dit setup.

cve_enrichment_workflow.json
{
  "name": "OpenCVE Enrichment",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "opencve-cve",
        "responseMode": "lastNode",
        "options": {}
      },
      "name": "Webhook (OpenCVE)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {},
          "conditions": [
            {
              "leftValue": "={{ $json.headers['x-webhook-secret'] }}",
              "rightValue": "DIN_HEMMELIGHED",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "name": "Validate Secret",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "url": "=https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={{ $json.body.cve_id }}",
        "options": {}
      },
      "name": "NVD Lookup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        680,
        240
      ]
    },
    {
      "parameters": {
        "url": "=https://api.first.org/data/v1/epss?cve={{ $json.body.cve_id }}",
        "options": {}
      },
      "name": "EPSS Score",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        680,
        380
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "name": "Merge Enrichment",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Saml beriget CVE-objekt til dashboard\nconst cve = $input.first().json;\nreturn [{ json: {\n  cve_id: cve.body?.cve_id ?? cve.cve_id,\n  cvss: cve.vulnerabilities?.[0]?.cve?.metrics?.cvssMetricV31?.[0]?.cvssData?.baseScore ?? null,\n  epss: cve.data?.[0]?.epss ?? null,\n  description: cve.vulnerabilities?.[0]?.cve?.descriptions?.find(d=>d.lang==='en')?.value ?? '',\n  published: cve.vulnerabilities?.[0]?.cve?.published ?? null,\n  enriched_at: new Date().toISOString()\n}}];"
      },
      "name": "Build Record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO cves (cve_id, cvss, epss, description, published, enriched_at) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (cve_id) DO UPDATE SET cvss=EXCLUDED.cvss, epss=EXCLUDED.epss, enriched_at=EXCLUDED.enriched_at;",
        "options": {}
      },
      "name": "Upsert Dashboard DB",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1340,
        300
      ]
    }
  ],
  "connections": {
    "Webhook (OpenCVE)": {
      "main": [
        [
          {
            "node": "Validate Secret",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Secret": {
      "main": [
        [
          {
            "node": "NVD Lookup",
            "type": "main",
            "index": 0
          },
          {
            "node": "EPSS Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "NVD Lookup": {
      "main": [
        [
          {
            "node": "Merge Enrichment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "EPSS Score": {
      "main": [
        [
          {
            "node": "Merge Enrichment",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Enrichment": {
      "main": [
        [
          {
            "node": "Build Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Record": {
      "main": [
        [
          {
            "node": "Upsert Dashboard DB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
Efter import — tjek: webhook-stien matcher den URL du satte i OpenCVE, secret-headeren er ægte (ikke placeholderen), Postgres-credential er bundet, og JSON-stierne i Build Record passer til de faktiske API-svar (NVD's struktur er dybt nestet og kan ændre sig). Den parser-kode er et udgangspunkt — test mod et rigtigt svar.

Dashboard-tabel

Minimal Postgres-tabel som Upsert-noden skriver til. Tilpas felter efter dit dashboard.

schema.sql
CREATE TABLE cves (
  cve_id      TEXT PRIMARY KEY,
  cvss        NUMERIC,
  epss        NUMERIC,
  description TEXT,
  published   TIMESTAMPTZ,
  enriched_at TIMESTAMPTZ DEFAULT now()
);
Du nævnte tidligere et SQLite-baseret CVE-dashboard — så swap Postgres-noden ud med en SQLite/HTTP-node, men behold samme upsert-logik (INSERT … ON CONFLICT).

Credentials

CredentialOpsætning
Webhook-secretVilkårlig stærk streng; sættes i OpenCVE-webhookken som header og valideres i node 2
NVD API-nøgleGratis fra nvd.nist.gov; tilføj som header på NVD-noden for højere rate-limit
Postgresn8n Postgres-credential mod din dashboard-database

Tuning & udvidelser

UdvidelseEffekt
CISA KEVTilføj opslag mod CISA's Known Exploited Vulnerabilities — stærkeste prioriterings-signal
Severity-filterDrop/markér CVE'er under en CVSS- eller EPSS-tærskel før upsert
AlertingTilføj Slack/e-mail-node ved KEV-match eller CVSS ≥ 9.0
Berørte produkterGem CPE/produktliste fra NVD så dashboardet kan filtrere på dit miljø
RetrySæt retry-on-fail på HTTP-noderne mod NVD/EPSS-timeouts
IdempotensON CONFLICT DO UPDATE sikrer at gentagne webhooks for samme CVE ikke laver dubletter
Hele kæden: OpenCVE overvåger og trigger → dette workflow beriger → dashboardet triagerer. Nyheder/artikler kører separat via Miniflux-nyheds-workflowet.