This workflow corresponds to n8n.io template #8097 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "neJ7FcRJ9hJGJd5g",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "CVE-Monitor by ca7ai",
"tags": [],
"nodes": [
{
"id": "223a6937-0478-4835-83cd-64b59e8068e1",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
1120,
-352
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.message.content.html }}",
"options": {},
"subject": "={{ $json.message.content.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "4a2ed259-8552-46a0-bd97-e439b72ca073",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"position": [
96,
-352
],
"parameters": {
"url": "=https://services.nvd.nist.gov/rest/json/cves/2.0",
"options": {},
"jsonQuery": "={{ JSON.stringify({\n // pull enough to pick the newest 20; 7 days is safe\n pubStartDate: new Date(Date.now() - 7*24*60*60*1000).toISOString(),\n pubEndDate: new Date().toISOString(),\n resultsPerPage: 200,\n startIndex: 0\n}) }}\n",
"sendQuery": true,
"sendHeaders": true,
"specifyQuery": "json",
"headerParameters": {
"parameters": [
{
"name": "apiKey",
"value": "YOUR API KEY HERE"
},
{
"name": "pubStartDate",
"value": "={{ new Date(Date.now() - 24*60*60*1000).toISOString() }}"
},
{
"name": "pubEndDate",
"value": "={{ new Date().toISOString() }}"
},
{
"name": "resultsPerPage",
"value": "200"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "76e8e8ef-1a8b-452b-8fb7-4d8003b5f005",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-128,
-352
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 30
}
]
}
},
"typeVersion": 1.2
},
{
"id": "45da6138-b79e-4303-b11f-353c19f74185",
"name": "Parse NVD",
"type": "n8n-nodes-base.code",
"position": [
320,
-352
],
"parameters": {
"jsCode": "// Parse NVD list \u2192 newest top N CVEs with Vendor/Product/Version, severity/CVSS,\n// exploit signal, and brief English summary.\n\nconst LIMIT = 20; // how many to keep\n\nconst vulns = $json.vulnerabilities ?? [];\n\n/* ---------------- helpers ---------------- */\n\nfunction getCvssAndSeverity(cve) {\n const m31 = cve?.metrics?.cvssMetricV31?.[0];\n const m30 = cve?.metrics?.cvssMetricV30?.[0];\n const m2 = cve?.metrics?.cvssMetricV2 ?. [0];\n\n const cvss =\n m31?.cvssData?.baseScore ??\n m30?.cvssData?.baseScore ??\n m2 ?.cvssData?.baseScore ?? null;\n\n const severity =\n m31?.cvssData?.baseSeverity ??\n m30?.cvssData?.baseSeverity ??\n (cvss != null\n ? (cvss >= 9 ? 'CRITICAL' : cvss >= 7 ? 'HIGH' : cvss >= 4 ? 'MEDIUM' : 'LOW')\n : 'UNKNOWN');\n\n const vector =\n m31?.cvssData?.vectorString ??\n m30?.cvssData?.vectorString ??\n m2 ?.cvssData?.vectorString ?? null;\n\n return { cvss, severity, vector };\n}\n\n// Pull vendor/product/version from CPE 2.3 (when available)\nfunction getCpeTriples(cve) {\n const triples = [];\n const seen = new Set();\n\n const walk = (node) => {\n const matches = node?.cpeMatch || node?.cpeMatches || [];\n for (const m of matches) {\n const cpe = m.criteria || m.cpe23Uri || m.cpeName || '';\n // cpe:2.3:<part>:<vendor>:<product>:<version>:...\n const parts = cpe.split(':');\n if (parts.length >= 6) {\n const vendor = (parts[3] || '').toLowerCase();\n const product = (parts[4] || '').toLowerCase();\n const version = (parts[5] && parts[5] !== '*' && parts[5] !== '-') ? parts[5] : null;\n const key = `${vendor}|${product}|${version||''}`;\n if (vendor && product && !seen.has(key)) {\n seen.add(key);\n triples.push({ vendor, product, version });\n }\n }\n }\n (node?.children || node?.nodes || []).forEach(walk);\n };\n\n (cve?.configurations?.nodes || []).forEach(walk);\n return triples;\n}\n\n// Heuristic vendor/product from English summary (no version required)\nfunction vendorProductFromText(desc) {\n const text = String(desc || '');\n\n // Known vendors (extend as needed)\n const vendorCatalog = [\n 'ibm','microsoft','cisco','oracle','red hat','apache','nginx','wordpress',\n 'kapsch trafficcom','hpe','hp','dell','lenovo','sap','vmware','fortinet',\n 'palo alto','juniper','citrix','f5','adobe','atlassian','gitlab','github',\n 'grafana','jetbrains','sonicwall','progress','solarwinds','check point'\n ];\n\n let vendor = null, product = null;\n\n for (const v of vendorCatalog) {\n const re = new RegExp(`\\\\b${v.replace(/\\s+/g,'\\\\s+')}\\\\b`, 'i');\n if (re.test(text)) { vendor = v; break; }\n }\n\n // WordPress plugin: \"<Plugin Name> plugin for WordPress\"\n const mWp = text.match(/([\\w()[\\] .&\\-]{3,80}?)\\s+plugin\\s+for\\s+WordPress/i);\n if (mWp) {\n product = mWp[1].trim();\n vendor = vendor || 'wordpress';\n }\n\n // If we know the vendor, capture product phrase right after it\n if (vendor && !product) {\n const vRe = new RegExp(\n `\\\\b${vendor.replace(/\\s+/g,'\\\\s+')}\\\\b\\\\s+` +\n `([A-Z][\\\\w()&\\\\-]+(?:\\\\s[A-Z][\\\\w()&\\\\-]+){0,5})\\\\s+` +\n `(?:is|are|was|were|contains|includes|allows|vulnerable|prior|before|up|due)`,\n 'i'\n );\n const m = text.match(vRe);\n if (m) product = m[1].trim();\n }\n\n // Generic fallback: first capitalized phrase before a verb\n if (!product) {\n const m2 = text.match(\n /([A-Z][\\w()&\\-]+(?:\\s[A-Z][\\w()&\\-]+){1,5})\\s+(?:is|are|was|were|contains|includes|allows|vulnerable)/);\n if (m2) product = m2[1].trim();\n }\n\n return {\n vendor: (vendor || 'unknown').toLowerCase(),\n product: (product || 'unknown').toLowerCase(),\n };\n}\n\n// Collect version strings from description (e.g., \"v3.2.0\", \"version 4.6.0\")\nfunction collectTextVersions(desc) {\n const text = String(desc || '');\n const out = new Set();\n for (const m of text.matchAll(/\\b(?:v(?:ersion)?\\s*)(\\d[\\dA-Za-z.\\-_]+)\\b/gi)) {\n out.add(m[1]);\n }\n return [...out];\n}\n\n// Detect public exploit presence from references\nfunction exploitInfo(cve) {\n const refs = cve.references ?? [];\n const urls = [];\n for (const r of refs) {\n const tags = (r.tags || []).map(t => String(t).toLowerCase());\n const url = r.url || '';\n const tagHit = tags.includes('exploit') || tags.includes('proof of concept');\n const urlHit = /exploitdb|packetstorm|metasploit|github\\.com\\/.*(poc|exploit)/i.test(url);\n if (tagHit || urlHit) urls.push(url);\n }\n return { hasExploit: urls.length > 0, exploitUrls: urls.slice(0, 3) };\n}\n\n/* ---------------- main ---------------- */\n\n// newest first (published \u2192 lastModified)\nvulns.sort((a,b) =>\n Date.parse(b.cve?.published ?? b.cve?.lastModified ?? 0) -\n Date.parse(a.cve?.published ?? a.cve?.lastModified ?? 0)\n);\n\n// take top N\nconst selected = vulns.slice(0, LIMIT);\n\n// map to output items\nreturn selected.map(v => {\n const c = v.cve;\n const { cvss, severity, vector } = getCvssAndSeverity(c);\n\n const desc = (c.descriptions ?? []).find(d => d.lang === 'en')?.value\n ?? (c.descriptions?.[0]?.value ?? '');\n\n const triples = getCpeTriples(c);\n const fromText = vendorProductFromText(desc);\n\n const vendor = (triples[0]?.vendor) || fromText.vendor || 'unknown';\n const product = (triples[0]?.product) || fromText.product || 'unknown';\n\n // versions from CPE + description\n const versions = new Set();\n triples.forEach(t => { if (t.version) versions.add(t.version); });\n collectTextVersions(desc).forEach(vv => versions.add(vv));\n\n const { hasExploit, exploitUrls } = exploitInfo(c);\n const id = c.id;\n const link = `https://nvd.nist.gov/vuln/detail/${id}`;\n\n return {\n json: {\n cveId: id,\n link,\n vendor,\n product,\n versions: Array.from(versions).slice(0, 5), // keep tidy\n severity,\n cvss,\n cvssVector: vector,\n summary: desc.slice(0, 240),\n hasExploit,\n exploitUrls,\n published: c.published ?? null\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "c5b4fb12-ec88-4388-8d0a-a286c3a6d8ee",
"name": "Build Digest",
"type": "n8n-nodes-base.code",
"position": [
544,
-352
],
"parameters": {
"jsCode": "if (!items.length) {\n return [{ json: { send:false, subject:\"CVE Digest: 0 items\", text:\"No CVEs.\", html:\"<p>No CVEs.</p>\" } }];\n}\n\nfunction esc(s){return String(s??'').replace(/[&<>\"']/g,m=>({ '&':'&','<':'<','>':'>','\"':'"',\"'\":''' }[m]));}\n\nconst ord = { CRITICAL:0, HIGH:1, MEDIUM:2, LOW:3, UNKNOWN:4 };\nconst list = items.map(i=>i.json).sort((a,b)=>{\n const sa = ord[(a.severity||'UNKNOWN').toUpperCase()] ?? 9;\n const sb = ord[(b.severity||'UNKNOWN').toUpperCase()] ?? 9;\n return sa - sb || (b.cvss??-1) - (a.cvss??-1);\n});\n\nconst counts = list.reduce((acc,j)=>{\n const s=(j.severity||'UNKNOWN').toUpperCase();\n acc[s]=(acc[s]||0)+1; return acc;\n}, {});\n\nconst subject = `Top ${list.length} Latest CVEs \u2014 CRIT:${counts.CRITICAL||0} HIGH:${counts.HIGH||0}`;\n\nconst rows = list.map(j=>{\n const link = j.link ? `<a href=\"${esc(j.link)}\">${esc(j.cveId)}</a>` : esc(j.cveId);\n const versions = (j.versions||[]).join(', ') || '\u2014';\n const exploit = j.hasExploit ? 'Yes' : 'No';\n return `\n <tr>\n <td>${link}</td>\n <td>${esc(j.vendor||'unknown')}</td>\n <td>${esc(j.product||'unknown')}</td>\n <td>${versions}</td>\n <td>${esc(j.severity||'UNKNOWN')}</td>\n <td>${j.cvss!=null?esc(j.cvss):'n/a'}</td>\n <td>${exploit}</td>\n <td>${esc(j.summary||'')}</td>\n </tr>`;\n}).join('');\n\nconst html = `\n <div style=\"font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif\">\n <h3 style=\"margin:0 0 8px 0;\">${esc(subject)}</h3>\n <table border=\"1\" cellpadding=\"6\" cellspacing=\"0\" style=\"border-collapse:collapse;font-size:14px;min-width:900px\">\n <thead>\n <tr style=\"background:#f3f4f6;\">\n <th align=\"left\">CVE</th>\n <th align=\"left\">Vendor</th>\n <th align=\"left\">Product</th>\n <th align=\"left\">Version(s)</th>\n <th align=\"left\">Severity</th>\n <th align=\"left\">CVSS</th>\n <th align=\"left\">Exploit?</th>\n <th align=\"left\">Brief description</th>\n </tr>\n </thead>\n <tbody>${rows}</tbody>\n </table>\n </div>\n`;\n\n// plain text fallback\nconst textLines = list.map(j=>{\n const versions = (j.versions||[]).join(', ') || '\u2014';\n const sc = j.cvss!=null?j.cvss:'n/a';\n const ex = j.hasExploit?'Yes':'No';\n return `\u2022 ${j.cveId} | ${j.vendor||'unknown'} | ${j.product||'unknown'} | versions: ${versions} | ${j.severity||'UNKNOWN'} | CVSS ${sc} | Exploit: ${ex}\\n ${j.link}\\n ${j.summary||''}`;\n});\nconst text = [subject, '', ...textLines].join('\\n');\n\nreturn [{ json: { send:true, subject, html, text } }];\n"
},
"typeVersion": 2
},
{
"id": "fc4831af-3d0c-4bfd-a090-3c7155248d7c",
"name": "OpenAI Email Crafter",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
768,
-352
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {
"temperature": 0
},
"messages": {
"values": [
{
"content": "=Subject:\n{{$json.subject}}\n\nHTML:\n{{$json.html}}\n\nText:\n{{$json.text}}\n"
},
{
"role": "system",
"content": "=Rewrite this CVE digest email into a concise, professional format.\nReturn ONLY valid JSON: {\"subject\":\"...\",\"html\":\"...\",\"text\":\"...\"}.\n"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "23172815-2e17-43e8-b492-3328a3904d92",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-608,
-496
],
"parameters": {
"color": 5,
"width": 336,
"height": 144,
"content": "## Workflow Trigger\n\nWorkflow is triggered to execute every 30 min.\n\nYou can change this to run every day, etc. "
},
"typeVersion": 1
},
{
"id": "20f08eba-82e5-4ef5-98b8-02f168c2a9c7",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-608,
-256
],
"parameters": {
"width": 336,
"height": 144,
"content": "## HTTP Request\n\nCurrent data source is set to NVD and it uses a API key from NVD. \n\nMake sure to update the HTTP Request Node with your API Key"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "52a44b53-cc27-4740-adc2-ca4de10c9d04",
"connections": {
"Parse NVD": {
"main": [
[
{
"node": "Build Digest",
"type": "main",
"index": 0
}
]
]
},
"Build Digest": {
"main": [
[
{
"node": "OpenAI Email Crafter",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Parse NVD",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Email Crafter": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2openAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Summary
Source: https://n8n.io/workflows/8097/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation sign
This workflow monitors filesystem sync and backup jobs by validating their execution logs, not by running or inspecting the jobs themselves.
Stop wasting billable hours on manual time-tracking. AutoTimesheet Pro uses AI to collect emails, meetings, and GitHub work, then writes a clean timesheet straight into Google Sheets. Perfect for deve
Imagine a dedicated financial expert tirelessly working behind the scenes, sifting through every transaction, every investment move, and every accounting entry. That's exactly what this automated syst
Who is this for? AI creators, marketers, agencies, and researchers tracking YouTube trends who need weekly high-signal insights without 4+ hours manual research.