{
  "name": "Daily CVE Intelligence & Prioritization Notifier",
  "tags": [],
  "nodes": [
    {
      "id": "7dd8e05b-b223-4ff9-b8ce-5368bb4e866e",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        256
      ],
      "parameters": {
        "width": 720,
        "height": 320,
        "content": "## Daily CVE risk intelligence\n\nMonitors a technology watchlist, finds newly published CVEs, enriches them with EPSS and CISA KEV, removes duplicate alerts, and sends a prioritized daily digest.\n\nUse this when you want a lightweight vulnerability intelligence feed for the technologies your team cares about."
      },
      "typeVersion": 1
    },
    {
      "id": "55b2913b-4349-4055-83b7-d27f078972b2",
      "name": "Setup checklist",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        256
      ],
      "parameters": {
        "width": 720,
        "height": 320,
        "content": "## Setup checklist\n\n- Replace the sample CSV URL in **Fetch Technology Watchlist CSV**.\n- Add your NVD API key in **Fetch Recent CVEs from NVD**.\n- Connect Gmail credentials in **Send Email Digest**.\n- Connect Slack credentials and choose a channel in **Send Slack Digest**.\n- Test the workflow once, then activate it."
      },
      "typeVersion": 1
    },
    {
      "id": "5759e26e-92f7-4a4b-bffe-8de4dc180b05",
      "name": "Sample CSV",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1184,
        256
      ],
      "parameters": {
        "width": 720,
        "height": 320,
        "content": "## Watchlist CSV format\n\n```csv\nTechnology,Keywords,Severity,Notify,Enabled\nnginx,\"nginx,openresty\",\"HIGH,CRITICAL\",both,TRUE\nwordpress,\"wordpress,woocommerce\",\"MEDIUM,HIGH,CRITICAL\",both,TRUE\nkubernetes,\"kubernetes,k8s\",\"HIGH,CRITICAL\",slack,TRUE\napache,\"apache,httpd\",CRITICAL,email,TRUE\n```\n\n`Notify` supports `email`, `slack`, `both`, or `none`."
      },
      "typeVersion": 1
    },
    {
      "id": "7d18d45c-9363-455e-ba3b-d9cba669e3e0",
      "name": "Trigger annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 256,
        "content": "## Trigger\n\nRuns once daily and starts the CVE monitoring workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "755033d6-362d-4b7c-83d8-02d93612c284",
      "name": "Watchlist annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 256,
        "content": "## Load watchlist\n\nDownloads the public CSV or Google Sheet export and parses it into structured rows.\n\nEach row controls technology keywords, severity filters, notification route, and whether monitoring is enabled."
      },
      "typeVersion": 1
    },
    {
      "id": "9f2861f9-f3be-4a43-95c2-f03449a00d65",
      "name": "NVD annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 256,
        "content": "## Fetch recent CVEs\n\nRetrieves CVEs published during the last 24 hours from the NVD API.\n\nThis uses one request per run, instead of one request per technology."
      },
      "typeVersion": 1
    },
    {
      "id": "5c688b20-8158-4763-b0a3-abd247e52ccd",
      "name": "Match and dedup annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 256,
        "content": "## Match and deduplicate\n\nMatches CVEs against watchlist keywords, applies the configured severity filter, and skips CVEs that were already sent.\n\nDeduplication uses workflow static data."
      },
      "typeVersion": 1
    },
    {
      "id": "8a4d3328-28c1-4c42-baea-2b41f33e4779",
      "name": "EPSS annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 256,
        "content": "## EPSS enrichment\n\nLooks up EPSS scores from FIRST.org and attaches exploit probability data to each matched CVE.\n\nThis helps prioritize CVEs that are more likely to be exploited."
      },
      "typeVersion": 1
    },
    {
      "id": "059ee736-a64f-4e9f-bc2e-a869eeec19c0",
      "name": "KEV annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 256,
        "content": "## CISA KEV enrichment\n\nChecks matched CVEs against the CISA Known Exploited Vulnerabilities catalog.\n\nKEV matches are treated as higher priority in the final digest."
      },
      "typeVersion": 1
    },
    {
      "id": "b2842178-9043-4762-991f-a80780034192",
      "name": "Digest annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 224,
        "height": 256,
        "content": "## Prioritize and build digest\n\nSorts findings by KEV status, severity, and EPSS score.\n\nBuilds a concise HTML email digest and Slack-friendly summary."
      },
      "typeVersion": 1
    },
    {
      "id": "64a00e46-eec6-452a-a690-ca4646b1fd03",
      "name": "Notification annotation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        672
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 256,
        "content": "## Send notifications\n\nSends the daily digest to Gmail and Slack.\n\nBoth notification nodes continue on failure, so one channel can still work even if the other needs credential setup."
      },
      "typeVersion": 1
    },
    {
      "id": "93441795-cb0c-4ab3-9b16-15fca384d3a4",
      "name": "Feedback note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        1264
      ],
      "parameters": {
        "color": 4,
        "width": 720,
        "height": 112,
        "content": "### How can this template be improved?\n\nCustomize the watchlist, severity filters, and notification channels based on your team\u2019s triage process."
      },
      "typeVersion": 1
    },
    {
      "id": "af1791c0-f206-4e22-a6de-1b0f10750c49",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -320,
        976
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 11
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "b150a202-1948-4e64-af0d-2682c1fb3679",
      "name": "Read Watchlist CSV",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        160,
        976
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "fcee3438-4a6a-4c12-872c-63226b3c2f0a",
      "name": "Match CVEs to Watchlist",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        976
      ],
      "parameters": {
        "jsCode": "// 1. Get technologies from sheet\nconst technologies = $('Read Watchlist CSV').all()\n  .map(i => i.json)\n  .filter(row => String(row.Enabled).toUpperCase() === 'TRUE');\n\n// 2. Get CVEs\nconst cves = $('Fetch Recent CVEs from NVD').first().json.vulnerabilities || [];\n\nconst results = [];\n\n// Helper to get CVSS\nfunction getCvss(cve) {\n  return (\n    cve.metrics?.cvssMetricV31?.[0]?.cvssData ||\n    cve.metrics?.cvssMetricV30?.[0]?.cvssData ||\n    cve.metrics?.cvssMetricV2?.[0]?.cvssData ||\n    {}\n  );\n}\n\nfor (const vuln of cves) {\n  const cve = vuln.cve;\n  const cvss = getCvss(cve);\n\n  const severity = String(cvss.baseSeverity || 'N/A').toUpperCase();\n\n  // Combine searchable text\n  const text = [\n    cve.id,\n    ...(cve.descriptions || []).map(d => d.value),\n    ...(cve.configurations || []).flatMap(cfg =>\n      (cfg.nodes || []).flatMap(node =>\n        (node.cpeMatch || []).map(cpe => cpe.criteria || '')\n      )\n    )\n  ].join(' ').toLowerCase();\n\n  for (const tech of technologies) {\n    const keywords = String(tech.Keywords || tech.Technology || '')\n      .split(',')\n      .map(k => k.trim().toLowerCase())\n      .filter(Boolean);\n\n    const allowedSeverities = String(tech.Severity || 'HIGH,CRITICAL')\n      .split(',')\n      .map(s => s.trim().toUpperCase())\n      .filter(Boolean);\n\n    const matched = keywords.some(k => text.includes(k));\n    const severityAllowed = allowedSeverities.includes(severity);\n\n    if (matched && severityAllowed) {\n      results.push({\n        dedupKey: `${tech.Technology}-${cve.id}`,\n        technology: tech.Technology,\n        notify: String(tech.Notify || 'both').toLowerCase(),\n        cveId: cve.id,\n        severity,\n        score: cvss.baseScore || 'N/A',\n        published: cve.published,\n        summary: cve.descriptions?.[0]?.value || '',\n        url: `https://nvd.nist.gov/vuln/detail/${cve.id}`\n      });\n    }\n  }\n}\n\nreturn results.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "67c9ee45-cdd8-419d-bdce-a09c7519f996",
      "name": "Deduplicate Alerts",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        976
      ],
      "parameters": {
        "jsCode": "const staticData = $getWorkflowStaticData('global');\n\nif (!staticData.sentCves) {\n  staticData.sentCves = {};\n}\n\nconst fresh = [];\n\nfor (const item of $input.all()) {\n  const key = item.json.dedupKey;\n\n  if (!staticData.sentCves[key]) {\n    staticData.sentCves[key] = {\n      sentAt: new Date().toISOString(),\n      technology: item.json.technology,\n      cveId: item.json.cveId,\n      severity: item.json.severity,\n    };\n\n    fresh.push(item);\n  }\n}\n\nreturn fresh;"
      },
      "typeVersion": 2
    },
    {
      "id": "222ec401-80eb-4413-aea0-de60d51ca057",
      "name": "Send Email Digest",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueRegularOutput",
      "position": [
        2560,
        880
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{$json.body}}",
        "options": {},
        "subject": "={{$json.subject}}"
      },
      "typeVersion": 2.2
    },
    {
      "id": "7afcebd8-781b-4b5a-9f67-f4ed7ecb623a",
      "name": "Prioritize & Build Digest",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        976
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nif (items.length === 0) return [];\n\nconst severityRank = { CRITICAL: 1, HIGH: 2, MEDIUM: 3, LOW: 4, 'N/A': 5 };\n\nconst severityColor = {\n  CRITICAL: '#b91c1c',\n  HIGH: '#ea580c',\n  MEDIUM: '#ca8a04',\n  LOW: '#2563eb',\n  'N/A': '#6b7280',\n};\n\nconst sorted = items\n  .map(i => i.json)\n  .sort((a, b) => {\n  // KEV first\n  if (a.kev !== b.kev) return b.kev - a.kev;\n\n  // Then severity\n  const sevDiff = (severityRank[a.severity] || 9) - (severityRank[b.severity] || 9);\n  if (sevDiff !== 0) return sevDiff;\n\n  // Then EPSS\n  return (b.epss || 0) - (a.epss || 0);\n});\n\nconst topFindings = sorted.slice(0, 5);\n\nlet rows = '';\n\nfor (const cve of sorted) {\n  const color = severityColor[cve.severity] || '#6b7280';\n\n  rows += `\n    <tr>\n      <td style=\"padding:10px;border-bottom:1px solid #eee;\">${cve.technology}</td>\n      <td style=\"padding:10px;border-bottom:1px solid #eee;\">\n        <a href=\"${cve.url}\" style=\"color:#2563eb;text-decoration:none;font-weight:600;\">${cve.cveId}</a>\n      </td>\n      <td style=\"padding:10px;border-bottom:1px solid #eee;\">\n        <span style=\"background:${color};color:#fff;padding:4px 8px;border-radius:12px;font-size:12px;font-weight:600;\">\n          ${cve.severity} ${cve.kev ? '\u26a1' : ''}\n        </span>\n      </td>\n      <td style=\"padding:10px;border-bottom:1px solid #eee;\">   ${cve.score}<br/>   <span style=\"font-size:11px;color:#6b7280;\">EPSS: ${cve.epssPercent}</span> </td>\n      <td style=\"padding:10px;border-bottom:1px solid #eee;\">${(cve.summary || '').slice(0, 180)}...</td>\n    </tr>\n  `;\n}\n\nconst critical = sorted.filter(i => i.severity === 'CRITICAL').length;\nconst high = sorted.filter(i => i.severity === 'HIGH').length;\n\nconst techSummaryMap = {};\n\nfor (const item of sorted) {\n  const tech = item.technology;\n  const sev = item.severity;\n\n  if (!techSummaryMap[tech] || severityRank[sev] < severityRank[techSummaryMap[tech]]) {\n    techSummaryMap[tech] = sev;\n  }\n}\n\nconst techSummary = Object.entries(techSummaryMap)\n  .sort((a, b) => severityRank[a[1]] - severityRank[b[1]])\n  .map(([tech, sev]) => ({ tech, severity: sev }));\n\nconst body = `\n<div style=\"font-family:Arial,sans-serif;color:#111827;max-width:900px;\">\n  <h2 style=\"margin-bottom:4px;\">Daily CVE Digest</h2>\n  <p style=\"color:#6b7280;margin-top:0;\">New matched CVEs from your technology watchlist.</p>\n\n  <div style=\"margin:18px 0;\">\n    <strong>Summary:</strong>\n    ${critical} Critical, ${high} High, ${sorted.length} Total\n  </div>\n\n  <table style=\"width:100%;border-collapse:collapse;border:1px solid #eee;\">\n    <thead>\n      <tr style=\"background:#f9fafb;text-align:left;\">\n        <th style=\"padding:10px;\">Tech</th>\n        <th style=\"padding:10px;\">CVE</th>\n        <th style=\"padding:10px;\">Severity</th>\n        <th style=\"padding:10px;\">Score</th>\n        <th style=\"padding:10px;\">Summary</th>\n      </tr>\n    </thead>\n    <tbody>${rows}</tbody>\n  </table>\n\n  <p style=\"font-size:12px;color:#6b7280;margin-top:16px;\">\n    Deduplicated alert. Only newly matched CVEs are included.\n  </p>\n</div>\n`;\n\nreturn [\n  {\n    json: {\n      subject: `Daily CVE Digest: ${critical} Critical, ${high} High`,\n      body,\n      critical,\n      high,\n      total: sorted.length,\n      topFindings,\n      techSummary\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "3265d671-a57e-4dfc-9dc0-11a99069eee8",
      "name": "Send Slack Digest",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        2560,
        1072
      ],
      "parameters": {
        "text": "=\ud83d\udea8 *Daily CVE Digest*\n\n*Summary*\n\u2022 \ud83d\udd34 Critical: {{$json.critical}}\n\u2022 \ud83d\udfe0 High: {{$json.high}}\n\u2022 \ud83d\udcca Total: {{$json.total}}\n\n*Actively Exploited (KEV)*\n{{ $json.topFindings.filter(i => i.kev).map(i => \n`\u2022 \u26a1 *${i.technology}* | <${i.url}|${i.cveId}>`\n).join('\\n') || 'None' }}\n\n*Top Risks*\n{{ $json.topFindings.map(i => {\n  const emoji = i.kev ? '\u26a1' : (i.severity === 'CRITICAL' ? '\ud83d\udd34' : '\ud83d\udfe0');\n  return `\u2022 ${emoji} *${i.technology}* | <${i.url}|${i.cveId}> | ${i.severity} | EPSS ${(i.epss * 100).toFixed(1)}%`;\n}).join('\\n') }}\n\n_Full details sent via email._",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "security-alerts"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.4
    },
    {
      "id": "a3a932d8-08ec-4b23-9b41-d0d3a4c02c5a",
      "name": "Prepare EPSS Lookup",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        976
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nif (items.length === 0) return [];\n\nconst cves = [...new Set(items.map(i => i.json.cveId))];\n\nreturn [\n  {\n    json: {\n      cves: cves.join(','),\n      findings: items.map(i => i.json)\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "94753dc0-411a-4ee9-8419-612438d19fca",
      "name": "Fetch EPSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1360,
        976
      ],
      "parameters": {
        "url": "https://api.first.org/data/v1/epss",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "cve",
              "value": "={{$json.cves}}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "6783bc6a-9b96-4340-a7d0-88b8d88f75b4",
      "name": "Attach EPSS Score",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        976
      ],
      "parameters": {
        "jsCode": "const findings = $('Prepare EPSS Lookup').first().json.findings || [];\nconst epssData = $('Fetch EPSS').first().json.data || [];\n\nconst epssMap = {};\n\nfor (const item of epssData) {\n  epssMap[item.cve] = {\n    epss: Number(item.epss || 0),\n    percentile: Number(item.percentile || 0),\n  };\n}\n\nconst enriched = findings.map(finding => {\n  const epss = epssMap[finding.cveId] || {\n    epss: 0,\n    percentile: 0,\n  };\n\n  return {\n    json: {\n      ...finding,\n      epss: epss.epss,\n      epssPercentile: epss.percentile,\n      epssPercent: `${(epss.epss * 100).toFixed(2)}%`,\n    },\n  };\n});\n\nreturn enriched;"
      },
      "typeVersion": 2
    },
    {
      "id": "c23e8d71-3441-4969-a39e-f0527a9a9b1b",
      "name": "Fetch KEV from GitHub Mirror",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1840,
        976
      ],
      "parameters": {
        "url": "https://raw.githubusercontent.com/cisagov/kev-data/main/known_exploited_vulnerabilities.json",
        "options": {
          "redirect": {
            "redirect": {}
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "eca9207a-911c-4c08-b4a3-fd4699dd1e52",
      "name": "Attach KEV",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        976
      ],
      "parameters": {
        "jsCode": "const findings = $('Attach EPSS Score').all().map(i => i.json);\nconst kevData = $('Fetch KEV from GitHub Mirror').first().json.vulnerabilities || [];\n\nconst kevSet = new Set(\n  kevData.map(v => v.cveID)\n);\n\nconst enriched = findings.map(finding => {\n  const isKev = kevSet.has(finding.cveId);\n\n  return {\n    json: {\n      ...finding,\n      kev: isKev,\n      kevLabel: isKev ? 'YES' : 'NO'\n    }\n  };\n});\n\nreturn enriched;"
      },
      "typeVersion": 2
    },
    {
      "id": "fd2b1021-0848-4cad-902a-d7284d81f64e",
      "name": "Fetch Technology Watchlist CSV",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -80,
        976
      ],
      "parameters": {
        "url": "https://example.com/technology-watchlist.csv",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": false
    },
    {
      "id": "77306447-e06d-4afd-b185-46752c4e13a7",
      "name": "Fetch Recent CVEs from NVD",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        400,
        976
      ],
      "parameters": {
        "url": "https://services.nvd.nist.gov/rest/json/cves/2.0",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "pubStartDate",
              "value": "={{$now.minus({ days: 1 }).toISO()}}"
            },
            {
              "name": "pubEndDate",
              "value": "={{$now.toISO()}}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "apiKey",
              "value": "PASTE_YOUR_NVD_API_KEY_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.3
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "connections": {
    "Attach KEV": {
      "main": [
        [
          {
            "node": "Prioritize & Build Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch EPSS": {
      "main": [
        [
          {
            "node": "Attach EPSS Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Technology Watchlist CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach EPSS Score": {
      "main": [
        [
          {
            "node": "Fetch KEV from GitHub Mirror",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deduplicate Alerts": {
      "main": [
        [
          {
            "node": "Prepare EPSS Lookup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Watchlist CSV": {
      "main": [
        [
          {
            "node": "Fetch Recent CVEs from NVD",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare EPSS Lookup": {
      "main": [
        [
          {
            "node": "Fetch EPSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match CVEs to Watchlist": {
      "main": [
        [
          {
            "node": "Deduplicate Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prioritize & Build Digest": {
      "main": [
        [
          {
            "node": "Send Email Digest",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Slack Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Recent CVEs from NVD": {
      "main": [
        [
          {
            "node": "Match CVEs to Watchlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch KEV from GitHub Mirror": {
      "main": [
        [
          {
            "node": "Attach KEV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Technology Watchlist CSV": {
      "main": [
        [
          {
            "node": "Read Watchlist CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}