diff --git a/utils/cves/index.js b/utils/cves/index.js index aab3f61ba6..541648bd72 100644 --- a/utils/cves/index.js +++ b/utils/cves/index.js @@ -8,9 +8,10 @@ const { escapeMDXSpecialChars } = require("../helpers/string"); const { generateMarkdownTable } = require("../helpers/affected-table"); const { generateRevisionHistory } = require("../helpers/revision-history"); const { generateCVEOfficialDetailsUrl } = require("../helpers/urls"); +const { generateCVEMap } = require("../helpers/cveHelpers"); async function getSecurityBulletins(payload) { - const limit = 100; + const limit = 300; const maxIterations = 1000; let results = []; @@ -210,47 +211,7 @@ async function generateMarkdownForCVEs(GlobalCVEData) { // To generate the Impact Product & Versions table we need to track all the instances of the same CVE // The following hashmap will store the data for each CVE and aggregate the impact data for each product - const cveImpactMap = {}; - - for (const item of allCVEs) { - // Let's add the CVE to the map if it doesn't exist - // We can take all of the values from the first instance of the CVE - // Future instances will update the values if they are true - if (!cveImpactMap[item.metadata.cve]) { - cveImpactMap[item.metadata.cve] = { - versions: item.spec.impact.impactedVersions, - impactsPaletteEnterprise: item.spec.impact.impactedProducts.palette, - impactsPaletteEnterpriseAirgap: item.spec.impact.impactedDeployments.airgap, - impactsVerteX: item.spec.impact.impactedProducts.vertex, - impactsVerteXAirgap: item.spec.impact.impactedDeployments.airgap, - }; - } - - // If the CVE already exists in the map, we need to update the values - // But only if the value is true. If the value is false, we don't need to update it. - if (cveImpactMap[item.metadata.cve]) { - cveImpactMap[item.metadata.cve].versions = [ - ...cveImpactMap[item.metadata.cve].versions, - ...item.spec.impact.impactedVersions, - ]; - - if (item.spec.impact.impactedProducts.palette) { - cveImpactMap[item.metadata.cve].impactsPaletteEnterprise = true; - } - - if (item.spec.impact.impactedDeployments.airgap) { - cveImpactMap[item.metadata.cve].impactsPaletteEnterpriseAirgap = true; - } - - if (item.spec.impact.impactedProducts.vertex) { - cveImpactMap[item.metadata.cve].impactsVerteX = true; - } - - if (item.spec.impact.impactedDeployments.airgap) { - cveImpactMap[item.metadata.cve].impactsVerteXAirgap = true; - } - } - } + const cveImpactMap = generateCVEMap(allCVEs); const markdownPromises = allCVEs.map((item) => createCveMarkdown(item, cveImpactMap[item.metadata.cve], "docs/docs-content/security-bulletins/reports/") diff --git a/utils/helpers/affected-table.js b/utils/helpers/affected-table.js index da23a3d27b..d1045400b7 100644 --- a/utils/helpers/affected-table.js +++ b/utils/helpers/affected-table.js @@ -5,43 +5,60 @@ function generateMarkdownTable(cveImpactMap) { throw new Error("Invalid input: cveImpactMap must be an object."); } + // Extract impact data and ensure consistency const impactData = { - "Palette Enterprise": cveImpactMap.impactsPaletteEnterprise, - "Palette Enterprise Airgap": cveImpactMap.impactsPaletteEnterpriseAirgap, - VerteX: cveImpactMap.impactsVerteX, - "VerteX Airgap": cveImpactMap.impactsVerteXAirgap, + "Palette Enterprise": cveImpactMap.palette, + "Palette Enterprise Airgap": cveImpactMap.paletteAirgap, + VerteX: cveImpactMap.vertex, + "VerteX Airgap": cveImpactMap.vertexAirgap, }; - const allProductsFalse = Object.values(impactData).every((value) => value === false); + // Check if all products are not impacted + const allProductsFalse = Object.values(impactData).every((product) => product.impacts === false); if (allProductsFalse) { - return "Investigation is ongoing to determine how this vulnerability affects our products"; + return "Investigation is ongoing to determine how this vulnerability affects our products."; } - const anyProductTrue = Object.values(impactData).some((value) => value === true); - if (anyProductTrue && (!cveImpactMap.versions || cveImpactMap.versions.length === 0)) { + // Check for any product impacted but no versions provided + const anyProductTrueNoVersions = Object.values(impactData).some( + (product) => product.impacts && (!product.versions || product.versions.length === 0) + ); + if (anyProductTrueNoVersions) { throw new Error("Error: Data inconsistency - Products impacted but no versions provided."); } - // Create the header row with the specified order + // Collect all unique versions across all products + const allVersions = Object.values(impactData) + .flatMap((product) => product.versions || []) + .filter((version) => semver.valid(version)); + const uniqueVersions = Array.from(new Set(allVersions)).sort(semver.rcompare); + + // Create the header row const header = `| Version | Palette Enterprise | Palette Enterprise Airgap | VerteX | VerteX Airgap |\n`; const separator = `| - | -------- | -------- | -------- | -------- |\n`; - // const uniqueVersions = Array.from(new Set(cveImpactMap.versions)).sort((a, b) => b.localeCompare(a)); - const uniqueVersions = Array.from(new Set(cveImpactMap.versions)).sort(semver.rcompare); - + // Create rows for each version const rows = uniqueVersions .map((version) => { const row = [ `| ${version}`, - impactData["Palette Enterprise"] ? "Impacted" : "No Impact", - impactData["Palette Enterprise Airgap"] ? "Impacted" : "No Impact", - impactData["VerteX"] ? "Impacted" : "No Impact", - impactData["VerteX Airgap"] ? "Impacted" : "No Impact", + impactData["Palette Enterprise"].impacts && impactData["Palette Enterprise"].versions.includes(version) + ? "⚠️ Impacted" + : "✅ No Impact", + impactData["Palette Enterprise Airgap"].impacts && + impactData["Palette Enterprise Airgap"].versions.includes(version) + ? "⚠️ Impacted" + : "✅ No Impact", + impactData["VerteX"].impacts && impactData["VerteX"].versions.includes(version) + ? "⚠️ Impacted" + : "✅ No Impact", + impactData["VerteX Airgap"].impacts && impactData["VerteX Airgap"].versions.includes(version) + ? "⚠️ Impacted" + : "✅ No Impact", ].join(" | "); return row + " |"; }) .join("\n"); - return header + separator + rows; } diff --git a/utils/helpers/affected-table.test.js b/utils/helpers/affected-table.test.js index 5cda941769..a0e27b9896 100644 --- a/utils/helpers/affected-table.test.js +++ b/utils/helpers/affected-table.test.js @@ -3,15 +3,14 @@ const { generateMarkdownTable } = require("./affected-table"); describe("generateMarkdownTable", () => { it("should generate a markdown table for two products with mixed impact", () => { const cveImpactMap = { - versions: ["4.4.20", "4.5.3"], - impactsPaletteEnterprise: true, - impactsPaletteEnterpriseAirgap: false, - impactsVerteX: false, - impactsVerteXAirgap: false, + palette: { impacts: true, versions: ["4.4.20", "4.5.3"] }, + paletteAirgap: { impacts: false, versions: [] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: false, versions: [] }, }; const expectedTable = `| Version | Palette Enterprise | Palette Enterprise Airgap | VerteX | VerteX Airgap | -|-|--------|--------|--------|--------| +| - | -------- | -------- | -------- | -------- | | 4.5.3 | Impacted | No Impact | No Impact | No Impact | | 4.4.20 | Impacted | No Impact | No Impact | No Impact |`; @@ -20,24 +19,22 @@ describe("generateMarkdownTable", () => { it("should return investigation message when all products are not impacted", () => { const cveImpactMap = { - versions: ["4.4.20", "4.5.3"], - impactsPaletteEnterprise: false, - impactsPaletteEnterpriseAirgap: false, - impactsVerteX: false, - impactsVerteXAirgap: false, + palette: { impacts: false, versions: [] }, + paletteAirgap: { impacts: false, versions: [] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: false, versions: [] }, }; - const expectedMessage = "Investigation is ongoing to determine how this vulnerability affects our products"; + const expectedMessage = "Investigation is ongoing to determine how this vulnerability affects our products."; expect(generateMarkdownTable(cveImpactMap)).toBe(expectedMessage); }); it("should throw an error when products are impacted but no versions are provided", () => { const cveImpactMap = { - versions: [], - impactsPaletteEnterprise: true, - impactsPaletteEnterpriseAirgap: false, - impactsVerteX: false, - impactsVerteXAirgap: false, + palette: { impacts: true, versions: [] }, + paletteAirgap: { impacts: false, versions: [] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: false, versions: [] }, }; expect(() => generateMarkdownTable(cveImpactMap)).toThrow( diff --git a/utils/helpers/cveHelpers.js b/utils/helpers/cveHelpers.js new file mode 100644 index 0000000000..b27a35c585 --- /dev/null +++ b/utils/helpers/cveHelpers.js @@ -0,0 +1,57 @@ +// generates a map of CVEs. Each CVE entry contains information about the impact of the CVE on different products and versions. +function generateCVEMap(cveData) { + const cveImpactMap = {}; + + for (const item of cveData) { + // Let's create a CVE entry in the map if it doesn't exist. + // By default, let's initailize all values to false or empty array. + if (!cveImpactMap[item.metadata.cve]) { + cveImpactMap[item.metadata.cve] = { + palette: { + impacts: false, + versions: [], + }, + paletteAirgap: { + impacts: false, + versions: [], + }, + vertex: { + impacts: false, + versions: [], + }, + vertexAirgap: { + impacts: false, + versions: [], + }, + }; + } + + // Palette Enterprise logic + if (item.spec.impact.impactedProducts.palette && !item.spec.impact.impactedDeployments.airgap) { + cveImpactMap[item.metadata.cve].palette.impacts = true; + cveImpactMap[item.metadata.cve].palette.versions = item.spec.impact.impactedVersions; + } + + // Palette Enterprise Airgap logic + if (item.spec.impact.impactedProducts.palette && item.spec.impact.impactedDeployments.airgap) { + cveImpactMap[item.metadata.cve].paletteAirgap.impacts = true; + cveImpactMap[item.metadata.cve].paletteAirgap.versions = item.spec.impact.impactedVersions; + } + + // Palette VerteX logic + if (item.spec.impact.impactedProducts.vertex && !item.spec.impact.impactedDeployments.airgap) { + cveImpactMap[item.metadata.cve].vertex.impacts = true; + cveImpactMap[item.metadata.cve].vertex.versions = item.spec.impact.impactedVersions; + } + + // Palette VerteX Airgap logic + if (item.spec.impact.impactedProducts.vertex && item.spec.impact.impactedDeployments.airgap) { + cveImpactMap[item.metadata.cve].vertexAirgap.impacts = true; + cveImpactMap[item.metadata.cve].vertexAirgap.versions = item.spec.impact.impactedVersions; + } + } + + return cveImpactMap; +} + +module.exports = { generateCVEMap }; diff --git a/utils/helpers/cveHelpers.test.js b/utils/helpers/cveHelpers.test.js new file mode 100644 index 0000000000..ff78a3e0cd --- /dev/null +++ b/utils/helpers/cveHelpers.test.js @@ -0,0 +1,160 @@ +// Import the function +const { generateCVEMap } = require("./cveHelpers"); + +describe("generateCVEMap", () => { + test("returns an empty map when given no CVE data", () => { + const input = []; + const result = generateCVEMap(input); + expect(result).toEqual({}); + }); + + test("processes a single CVE correctly for Palette Enterprise", () => { + const input = [ + { + metadata: { cve: "CVE-1234-5678" }, + spec: { + impact: { + impactedProducts: { palette: true, vertex: false }, + impactedDeployments: { airgap: false }, + impactedVersions: ["1.0", "1.1"], + }, + }, + }, + ]; + const result = generateCVEMap(input); + expect(result).toEqual({ + "CVE-1234-5678": { + palette: { impacts: true, versions: ["1.0", "1.1"] }, + paletteAirgap: { impacts: false, versions: [] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: false, versions: [] }, + }, + }); + }); + + test("processes a single CVE correctly for Palette Enterprise Airgap", () => { + const input = [ + { + metadata: { cve: "CVE-2345-6789" }, + spec: { + impact: { + impactedProducts: { palette: true, vertex: false }, + impactedDeployments: { airgap: true }, + impactedVersions: ["2.0"], + }, + }, + }, + ]; + const result = generateCVEMap(input); + expect(result).toEqual({ + "CVE-2345-6789": { + palette: { impacts: false, versions: [] }, + paletteAirgap: { impacts: true, versions: ["2.0"] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: false, versions: [] }, + }, + }); + }); + + test("processes multiple CVEs correctly", () => { + const input = [ + { + metadata: { cve: "CVE-3456-7890" }, + spec: { + impact: { + impactedProducts: { palette: true, vertex: false }, + impactedDeployments: { airgap: false }, + impactedVersions: ["3.0"], + }, + }, + }, + { + metadata: { cve: "CVE-3456-7890" }, + spec: { + impact: { + impactedProducts: { vertex: true }, + impactedDeployments: { airgap: true }, + impactedVersions: ["3.1"], + }, + }, + }, + ]; + const result = generateCVEMap(input); + expect(result).toEqual({ + "CVE-3456-7890": { + palette: { impacts: true, versions: ["3.0"] }, + paletteAirgap: { impacts: false, versions: [] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: true, versions: ["3.1"] }, + }, + }); + }); + + test("handles mixed product and airgap impacts", () => { + const input = [ + { + metadata: { cve: "CVE-4567-8901" }, + spec: { + impact: { + impactedProducts: { palette: true, vertex: true }, + impactedDeployments: { airgap: true }, + impactedVersions: ["4.0"], + }, + }, + }, + ]; + const result = generateCVEMap(input); + expect(result).toEqual({ + "CVE-4567-8901": { + palette: { impacts: false, versions: [] }, + paletteAirgap: { impacts: true, versions: ["4.0"] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: true, versions: ["4.0"] }, + }, + }); + }); + + test("handles different versions for each palette edition", () => { + const input = [ + { + metadata: { cve: "CVE-5678-9012" }, + spec: { + impact: { + impactedProducts: { palette: true }, + impactedDeployments: { airgap: false }, + impactedVersions: ["1.2", "1.3"], + }, + }, + }, + { + metadata: { cve: "CVE-5678-9012" }, + spec: { + impact: { + impactedProducts: { palette: true }, + impactedDeployments: { airgap: true }, + impactedVersions: ["1.4"], + }, + }, + }, + { + metadata: { cve: "CVE-5678-9012" }, + spec: { + impact: { + impactedProducts: { vertex: true }, + impactedDeployments: { airgap: true }, + impactedVersions: ["1.4", "2.3", "2.4"], + }, + }, + }, + ]; + const result = generateCVEMap(input); + expect(result).toEqual({ + "CVE-5678-9012": { + palette: { impacts: true, versions: ["1.2", "1.3"] }, + paletteAirgap: { impacts: true, versions: ["1.4"] }, + vertex: { impacts: false, versions: [] }, + vertexAirgap: { impacts: true, versions: ["1.4", "2.3", "2.4"] }, + }, + }); + }); +});