UNPKG

@scarlet-mesh/mcp-cve

Version:

CVE MCP Server providing security vulnerability analysis and Red Hat CVE data

436 lines (429 loc) 18 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Create MCP Server instance const server = new McpServer({ name: "cve", version: "1.0.0", capabilities: { resources: {}, }, }); async function fetchCveData(cveId) { const match = cveId.match(/^CVE-(\d{4})-(\d+)$/i); if (!match) { console.error("Invalid CVE ID format"); return null; } const [, year, number] = match; const url = `https://security.access.redhat.com/data/csaf/v2/vex/${year}/cve-${year}-${number}.json`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (err) { console.error(`Failed to fetch CVE data:`, err); return null; } } server.tool("summarize-cve", "Fetch and summarize a CVE from Red Hat's CSAF feed", { cveId: z.string().describe("The CVE ID to summarize (e.g. CVE-2024-53907)"), }, async ({ cveId }) => { const data = await fetchCveData(cveId); if (!data) { return { content: [{ type: "text", text: `Could not retrieve data for ${cveId}` }], }; } const title = data?.document?.title || "No title available"; const notes = data?.document?.notes?.map((n) => n.text).join("\n\n") || "No additional notes found."; const trackingId = data?.document?.tracking?.id || cveId; const severity = data?.vulnerabilities?.[0]?.scores?.[0]?.cvss_v3?.baseSeverity || "Unknown"; const productBranches = data?.product_tree?.branches || []; const affectedProducts = productBranches .map((branch) => { const subBranches = branch.branches || []; return [branch.name, ...subBranches.map((sb) => sb.name)].filter(Boolean); }) .flat() .filter(Boolean); const affectedList = affectedProducts.length ? affectedProducts.map((p) => `- ${p}`).join("\n") : "No affected products listed."; const references = data?.vulnerabilities?.[0]?.references?.map((ref) => `- [${ref.url}](${ref.url})`) || []; const referencesText = references.length ? references.join("\n") : "No references available."; return { content: [ { type: "text", text: `### ${title}\n\n**Tracking ID:** ${trackingId}\n**Severity:** ${severity}\n\n---\n\n### Notes\n${notes}\n\n---\n\n### Affected Products\n${affectedList}\n\n---\n\n### 🔗 References\n${referencesText}`, }, ], }; }); server.tool("get-cve-summary-html", "Returns a rich HTML card-style summary for a CVE with severity, products, links, and remediation info", { cveId: z.string().describe("The CVE ID to summarize (e.g. CVE-2024-53907)"), }, async ({ cveId }) => { const data = await fetchCveData(cveId); if (!data) { return { content: [ { type: "text", text: `Could not retrieve data for ${cveId}`, }, ], isError: true, }; } const title = data?.document?.title || "No title available"; const severity = data?.vulnerabilities?.[0]?.scores?.[0]?.cvss_v3?.baseSeverity || "Unknown"; const remediationNotes = data?.document?.notes ?.filter((n) => n.category === "remediation") .map((n) => n.text) .join("<br/>") || "No remediation steps available."; const productBranches = data?.product_tree?.branches || []; const affectedProducts = productBranches .map((branch) => { const subBranches = branch.branches || []; return [branch.name, ...subBranches.map((sb) => sb.name)].filter(Boolean); }) .flat() .filter(Boolean); const references = data?.vulnerabilities?.[0]?.references || []; const html = ` <div style="background-color: #f5f3f4; border: 1px solid #ccc; border-radius: 8px; padding: 16px; max-width: 600px; font-family: sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.1)"> <h2 style="margin-top: 0;">${title}</h2> <span style="display: inline-block; padding: 4px 10px; background-color: ${severity === "Critical" ? "#a30000" : severity === "Important" ? "#b85c00" : severity === "Moderated" ? "#f5c12e" : severity === "Low" ? "#316dc1" : "#90a4ae"}; color: white; border-radius: 4px; font-size: 14px;">${severity}</span> <h4 style="margin-top: 20px;">Affected Products:</h4> <ul> ${affectedProducts.length > 0 ? affectedProducts.map((p) => `<li>${p}</li>`).join("") : "<li>None listed.</li>"} </ul> <h4 style="margin-top: 20px;">Remediation:</h4> <p>${remediationNotes}</p> <h4 style="margin-top: 20px;">References:</h4> <ul> ${references.length > 0 ? references.map((ref) => `<li><a href="${ref.url}" target="_blank">${ref.url}</a></li>`).join("") : "<li>None listed.</li>"} </ul> </div> `; return { content: [ { type: "resource", resource: { text: html, mimeType: "text/html", uri: "data:text/html;base64," + Buffer.from(html).toString("base64"), }, }, ], isError: false, _meta: {}, }; }); // Add new tool: search-security-advisories server.tool('search-security-advisories', 'Get Red Hat Security Advisories (RHSA) with various filters. You can specify 10, 20, 50, or 100 results. Supports product, severity, date range filters and more', { query: z .string() .optional() .describe("Search query term (default: '*:*' for all)"), product: z.string().optional().describe('Filter by product name'), version: z.string().optional().describe('Filter by product version'), architecture: z.string().optional().describe('Filter by architecture'), variant: z.string().optional().describe('Filter by product variant'), severity: z .string() .optional() .describe('Filter by severity level. Case-insensitive. Valid values: Low, Moderate, Important, Critical'), startDate: z .string() .optional() .describe('Start date filter (YYYY-MM-DD format)'), endDate: z .string() .optional() .describe('End date filter (YYYY-MM-DD format)'), rows: z .union([z.number(), z.string(), z.coerce.number()]) .optional() .describe('Number of RHSA results to return. Common values: 10, 20, 50, 100 (default: 10)'), page: z .union([z.number(), z.string(), z.coerce.number()]) .optional() .describe('Page number (default: 1)'), sortBy: z .enum([ 'portal_publication_date desc', 'portal_update_date desc', 'portal_severity desc', ]) .optional() .describe('Sort order (default: portal_publication_date desc)'), }, async ({ query, product, version, architecture, variant, severity, startDate, endDate, rows, page, sortBy, }) => { // Normalize severity input to proper case let normalizedSeverity; if (severity) { const severityLower = severity.toLowerCase(); switch (severityLower) { case 'low': normalizedSeverity = 'Low'; break; case 'moderate': normalizedSeverity = 'Moderate'; break; case 'important': normalizedSeverity = 'Important'; break; case 'critical': normalizedSeverity = 'Critical'; break; default: return { content: [ { type: 'text', text: `Invalid severity value: '${severity}'. Valid values are: Low, Moderate, Important, Critical (case-insensitive)`, }, ], }; } } const baseUrl = 'https://access.redhat.com/hydra/rest/search/kcs'; const params = new URLSearchParams(); // Convert string inputs to numbers for calculations with better validation let numRows = 10; let numPage = 1; if (rows !== undefined) { if (Array.isArray(rows)) { numRows = parseInt(String(rows[0]), 10) || 10; } else if (typeof rows === 'string') { numRows = parseInt(rows, 10) || 10; } else if (typeof rows === 'number') { numRows = rows; } // Validate and cap the number of rows to reasonable limits if (numRows < 1) numRows = 10; if (numRows > 200) numRows = 200; // Cap at 200 for performance } if (page !== undefined) { if (Array.isArray(page)) { numPage = parseInt(String(page[0]), 10) || 1; } else if (typeof page === 'string') { numPage = parseInt(page, 10) || 1; } else if (typeof page === 'number') { numPage = page; } // Validate page number if (numPage < 1) numPage = 1; } // Basic query parameters params.append('q', query || '*:*'); params.append('start', String((numPage - 1) * numRows)); params.append('rows', String(numRows)); params.append('p', String(numPage)); params.append('sort', sortBy || 'portal_publication_date desc'); // Highlighting parameters params.append('hl', 'true'); params.append('hl.fl', 'lab_description'); params.append('hl.simple.pre', '%3Cmark%3E'); params.append('hl.simple.post', '%3C%2Fmark%3E'); // Faceting parameters params.append('facet', 'true'); params.append('facet.mincount', '1'); params.append('facet.field', 'portal_severity'); params.append('facet.field', 'portal_advisory_type'); // Field list to return - specific fields for Red Hat Security Advisories params.append('fl', 'id,portal_severity,portal_product_names,portal_CVE,portal_publication_date,portal_synopsis,view_uri,allTitle,portal_update_date,portal_advisory_type'); // Build filter query (fq) parameters - Updated to specifically target Red Hat Security Advisories const fqParts = [ 'portal_advisory_type:("Security Advisory")', 'documentKind:("Errata")', 'id:(RHSA-*)', // Only return RHSA advisories ]; if (product) { fqParts.push(`portal_product_names:("${product}")`); } if (normalizedSeverity) { fqParts.push(`portal_severity:("${normalizedSeverity}")`); } if (version) { fqParts.push(`portal_product_version:("${version}")`); } if (architecture) { fqParts.push(`portal_architecture:("${architecture}")`); } if (variant) { fqParts.push(`portal_product_variant:("${variant}")`); } // If no dates are specified, default to last 2 years to get latest advisories if (!startDate && !endDate) { const currentDate = new Date(); const twoYearsAgo = new Date(); twoYearsAgo.setFullYear(currentDate.getFullYear() - 2); const defaultStartDate = twoYearsAgo.toISOString().split('T')[0]; // YYYY-MM-DD format const defaultEndDate = currentDate.toISOString().split('T')[0]; // YYYY-MM-DD format fqParts.push(`portal_publication_date:[${defaultStartDate}T00:00:00Z TO ${defaultEndDate}T23:59:59Z]`); } else if (startDate || endDate) { const startDateStr = startDate ? `${startDate}T00:00:00Z` : '*'; const endDateStr = endDate ? `${endDate}T23:59:59Z` : '*'; fqParts.push(`portal_publication_date:[${startDateStr} TO ${endDateStr}]`); } params.append('fq', fqParts.join(' AND ')); const url = `${baseUrl}?${params.toString()}`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); const advisories = data?.response?.docs || []; const totalResults = data?.response?.numFound || 0; const facets = data?.facet_counts?.facet_fields || {}; if (!Array.isArray(advisories) || advisories.length === 0) { return { content: [ { type: 'text', text: 'No Red Hat Security Advisories found matching your criteria.', }, ], }; } // Format the results const summary = advisories .map((advisory, idx) => { const rawAdvisoryId = advisory.id || 'No ID'; const advisoryId = rawAdvisoryId.replace(':', '-'); const title = advisory.allTitle || advisory.portal_synopsis || 'No title'; const severity = advisory.portal_severity || 'Unknown'; const pubDate = advisory.portal_publication_date ? new Date(advisory.portal_publication_date).toLocaleDateString() : 'Unknown date'; const updateDate = advisory.portal_update_date ? new Date(advisory.portal_update_date).toLocaleDateString() : 'Unknown date'; const link = advisory.view_uri || '#'; const synopsis = advisory.portal_synopsis || 'No synopsis available'; const cves = advisory.portal_CVE ? `${advisory.portal_CVE.length} related CVEs` : 'No CVEs listed'; const products = advisory.portal_product_names ? advisory.portal_product_names.slice(0, 3).join(', ') + (advisory.portal_product_names.length > 3 ? '...' : '') : 'No products listed'; return `**${idx + 1}. [${advisoryId}](${link})** **${cves}** **${severity}: ${title}** **${severity}** **Products: ${products}** **${pubDate}**`; }) .join('\n\n'); // Format facet information const severityFacets = facets.portal_severity || []; const severityCounts = []; for (let i = 0; i < severityFacets.length; i += 2) { severityCounts.push(`${severityFacets[i]}: ${severityFacets[i + 1]}`); } const facetInfo = severityCounts.length > 0 ? `\n\n**Severity Distribution:**\n${severityCounts.join(' | ')}` : ''; return { content: [ { type: 'text', text: `# Red Hat Security Advisories (RHSA)\n\n**Total Results:** ${totalResults}\n**Showing:** ${advisories.length} results${facetInfo}\n\n---\n\n${summary}`, }, ], }; } catch (err) { return { content: [ { type: 'text', text: `Failed to fetch Red Hat Security Advisories: ${err}` }, ], }; } }); // Add new tool: get-latest-cves // server.tool( // "get-latest-cves", // "Fetch and summarize the latest 10 Red Hat CVEs from the Red Hat KCS API.", // {}, // async () => { // const url = // "https://access.redhat.com/hydra/rest/search/kcs?facet=true&facet.field=cve_threatSeverity&facet.mincount=1&facet.range=%7B%21ex%3Date%7Dcve_publicDate&facet.range.end=NOW&facet.range.gap=%2B1YEAR&facet.range.start=NOW%2FYEAR-15YEARS&fl=id%2Ccve_threatSeverity%2Ccve_publicDate%2Cview_uri%2CallTitle%2Ccve_details%2Cdetails_source%2Ccve_state_package&fq=documentKind%3A%28%22Cve%22%29+AND+language%3A%28%22en%22%29+AND+details_source%3A%28%22Red+Hat%22%29+AND+cve_state_package%3A%5B%22%7B%5C%5C%22%22+TO+*%5D&hl=true&hl.fl=lab_description&hl.simple.post=%253C%252Fmark%253E&hl.simple.pre=%253Cmark%253E&q=red+hat&rows=10&sort=cve_publicDate+desc%2CallTitle+desc&start=0&p=1&facet.field=platform_state_string"; // try { // const response = await fetch(url); // if (!response.ok) { // throw new Error(`HTTP ${response.status}: ${response.statusText}`); // } // const data = await response.json(); // const cves = data?.response?.docs || []; // if (!Array.isArray(cves) || cves.length === 0) { // return { // content: [ // { type: "text", text: "No recent CVEs found from Red Hat KCS API." }, // ], // }; // } // const summary = cves // .map((cve: any, idx: number) => { // const title = cve.allTitle || cve.id || "No title"; // const severity = cve.cve_threatSeverity || "Unknown"; // const date = cve.cve_publicDate ? new Date(cve.cve_publicDate).toLocaleDateString() : "Unknown date"; // const link = cve.view_uri || "#"; // const details = cve.cve_details || ""; // return `**${idx + 1}. [${title}](${link})**\n- Severity: ${severity}\n- Public Date: ${date}\n- Details: ${details}`; // }) // .join("\n\n"); // return { // content: [ // { // type: "text", // text: `# Latest 10 Red Hat CVEs\n\n${summary}`, // }, // ], // }; // } catch (err) { // return { // content: [ // { type: "text", text: `Failed to fetch latest CVEs: ${err}` }, // ], // }; // } // } // ); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("CVE MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });